@hasna/mementos 0.4.41 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/cli/index.js +2247 -1548
  2. package/dist/db/database.d.ts.map +1 -1
  3. package/dist/db/memories.d.ts +1 -0
  4. package/dist/db/memories.d.ts.map +1 -1
  5. package/dist/index.d.ts +6 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +2041 -1336
  8. package/dist/lib/auto-memory-queue.d.ts +46 -0
  9. package/dist/lib/auto-memory-queue.d.ts.map +1 -0
  10. package/dist/lib/auto-memory.d.ts +18 -0
  11. package/dist/lib/auto-memory.d.ts.map +1 -0
  12. package/dist/lib/dedup.d.ts +33 -0
  13. package/dist/lib/dedup.d.ts.map +1 -0
  14. package/dist/lib/focus.d.ts +58 -0
  15. package/dist/lib/focus.d.ts.map +1 -0
  16. package/dist/lib/providers/anthropic.d.ts +21 -0
  17. package/dist/lib/providers/anthropic.d.ts.map +1 -0
  18. package/dist/lib/providers/base.d.ts +96 -0
  19. package/dist/lib/providers/base.d.ts.map +1 -0
  20. package/dist/lib/providers/cerebras.d.ts +20 -0
  21. package/dist/lib/providers/cerebras.d.ts.map +1 -0
  22. package/dist/lib/providers/grok.d.ts +19 -0
  23. package/dist/lib/providers/grok.d.ts.map +1 -0
  24. package/dist/lib/providers/index.d.ts +7 -0
  25. package/dist/lib/providers/index.d.ts.map +1 -0
  26. package/dist/lib/providers/openai-compat.d.ts +18 -0
  27. package/dist/lib/providers/openai-compat.d.ts.map +1 -0
  28. package/dist/lib/providers/openai.d.ts +20 -0
  29. package/dist/lib/providers/openai.d.ts.map +1 -0
  30. package/dist/lib/providers/registry.d.ts +38 -0
  31. package/dist/lib/providers/registry.d.ts.map +1 -0
  32. package/dist/lib/search.d.ts.map +1 -1
  33. package/dist/mcp/index.js +1781 -1101
  34. package/dist/server/index.d.ts.map +1 -1
  35. package/dist/server/index.js +1480 -941
  36. package/dist/types/index.d.ts +7 -0
  37. package/dist/types/index.d.ts.map +1 -1
  38. package/package.json +2 -2
package/dist/mcp/index.js CHANGED
@@ -4323,6 +4323,11 @@ var MIGRATIONS = [
4323
4323
  CREATE INDEX IF NOT EXISTS idx_resource_locks_agent ON resource_locks(agent_id);
4324
4324
  CREATE INDEX IF NOT EXISTS idx_resource_locks_expires ON resource_locks(expires_at);
4325
4325
  INSERT OR IGNORE INTO _migrations (id) VALUES (8);
4326
+ `,
4327
+ `
4328
+ ALTER TABLE memories ADD COLUMN recall_count INTEGER NOT NULL DEFAULT 0;
4329
+ CREATE INDEX IF NOT EXISTS idx_memories_recall_count ON memories(recall_count DESC);
4330
+ INSERT OR IGNORE INTO _migrations (id) VALUES (9);
4326
4331
  `
4327
4332
  ];
4328
4333
  var _db = null;
@@ -4404,1175 +4409,990 @@ function redactSecrets(text) {
4404
4409
  return result;
4405
4410
  }
4406
4411
 
4407
- // src/db/agents.ts
4408
- var CONFLICT_WINDOW_MS = 30 * 60 * 1000;
4409
- function parseAgentRow(row) {
4412
+ // src/db/entity-memories.ts
4413
+ function parseEntityMemoryRow(row) {
4410
4414
  return {
4411
- id: row["id"],
4412
- name: row["name"],
4413
- session_id: row["session_id"] || null,
4414
- description: row["description"] || null,
4415
- role: row["role"] || null,
4416
- metadata: JSON.parse(row["metadata"] || "{}"),
4417
- active_project_id: row["active_project_id"] || null,
4418
- created_at: row["created_at"],
4419
- last_seen_at: row["last_seen_at"]
4415
+ entity_id: row["entity_id"],
4416
+ memory_id: row["memory_id"],
4417
+ role: row["role"],
4418
+ created_at: row["created_at"]
4420
4419
  };
4421
4420
  }
4422
- function registerAgent(name, sessionId, description, role, projectId, db) {
4421
+ function linkEntityToMemory(entityId, memoryId, role = "context", db) {
4423
4422
  const d = db || getDatabase();
4424
4423
  const timestamp = now();
4425
- const normalizedName = name.trim().toLowerCase();
4426
- const existing = d.query("SELECT * FROM agents WHERE LOWER(name) = ?").get(normalizedName);
4427
- if (existing) {
4428
- const existingId = existing["id"];
4429
- const existingSessionId = existing["session_id"] || null;
4430
- const existingLastSeen = existing["last_seen_at"];
4431
- if (sessionId && existingSessionId && existingSessionId !== sessionId) {
4432
- const lastSeenMs = new Date(existingLastSeen).getTime();
4433
- const nowMs = Date.now();
4434
- if (nowMs - lastSeenMs < CONFLICT_WINDOW_MS) {
4435
- throw new AgentConflictError({
4436
- existing_id: existingId,
4437
- existing_name: normalizedName,
4438
- last_seen_at: existingLastSeen,
4439
- session_hint: existingSessionId.slice(0, 8),
4440
- working_dir: null
4441
- });
4442
- }
4443
- }
4444
- d.run("UPDATE agents SET last_seen_at = ?, session_id = ? WHERE id = ?", [
4445
- timestamp,
4446
- sessionId ?? existingSessionId,
4447
- existingId
4448
- ]);
4449
- if (description) {
4450
- d.run("UPDATE agents SET description = ? WHERE id = ?", [description, existingId]);
4451
- }
4452
- if (role) {
4453
- d.run("UPDATE agents SET role = ? WHERE id = ?", [role, existingId]);
4454
- }
4455
- if (projectId !== undefined) {
4456
- d.run("UPDATE agents SET active_project_id = ? WHERE id = ?", [projectId, existingId]);
4457
- }
4458
- return getAgent(existingId, d);
4459
- }
4460
- const id = shortUuid();
4461
- 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]);
4462
- return getAgent(id, d);
4463
- }
4464
- function getAgent(idOrName, db) {
4465
- const d = db || getDatabase();
4466
- let row = d.query("SELECT * FROM agents WHERE id = ?").get(idOrName);
4467
- if (row)
4468
- return parseAgentRow(row);
4469
- row = d.query("SELECT * FROM agents WHERE LOWER(name) = ?").get(idOrName.trim().toLowerCase());
4470
- if (row)
4471
- return parseAgentRow(row);
4472
- const rows = d.query("SELECT * FROM agents WHERE id LIKE ?").all(`${idOrName}%`);
4473
- if (rows.length === 1)
4474
- return parseAgentRow(rows[0]);
4475
- return null;
4476
- }
4477
- function listAgents(db) {
4478
- const d = db || getDatabase();
4479
- const rows = d.query("SELECT * FROM agents ORDER BY last_seen_at DESC").all();
4480
- return rows.map(parseAgentRow);
4424
+ d.run(`INSERT OR IGNORE INTO entity_memories (entity_id, memory_id, role, created_at)
4425
+ VALUES (?, ?, ?, ?)`, [entityId, memoryId, role, timestamp]);
4426
+ const row = d.query("SELECT * FROM entity_memories WHERE entity_id = ? AND memory_id = ?").get(entityId, memoryId);
4427
+ return parseEntityMemoryRow(row);
4481
4428
  }
4482
- function touchAgent(idOrName, db) {
4429
+ function unlinkEntityFromMemory(entityId, memoryId, db) {
4483
4430
  const d = db || getDatabase();
4484
- const agent = getAgent(idOrName, d);
4485
- if (!agent)
4486
- return;
4487
- d.run("UPDATE agents SET last_seen_at = ? WHERE id = ?", [now(), agent.id]);
4431
+ d.run("DELETE FROM entity_memories WHERE entity_id = ? AND memory_id = ?", [entityId, memoryId]);
4488
4432
  }
4489
- function listAgentsByProject(projectId, db) {
4433
+ function getMemoriesForEntity(entityId, db) {
4490
4434
  const d = db || getDatabase();
4491
- const rows = d.query("SELECT * FROM agents WHERE active_project_id = ? ORDER BY last_seen_at DESC").all(projectId);
4492
- return rows.map(parseAgentRow);
4435
+ const rows = d.query(`SELECT m.* FROM memories m
4436
+ INNER JOIN entity_memories em ON em.memory_id = m.id
4437
+ WHERE em.entity_id = ?
4438
+ ORDER BY m.importance DESC, m.created_at DESC`).all(entityId);
4439
+ return rows.map(parseMemoryRow);
4493
4440
  }
4494
- function updateAgent(id, updates, db) {
4441
+ function getEntityMemoryLinks(entityId, memoryId, db) {
4495
4442
  const d = db || getDatabase();
4496
- const agent = getAgent(id, d);
4497
- if (!agent)
4498
- return null;
4499
- const timestamp = now();
4500
- if (updates.name) {
4501
- const normalizedNewName = updates.name.trim().toLowerCase();
4502
- if (normalizedNewName !== agent.name) {
4503
- const existing = d.query("SELECT id FROM agents WHERE LOWER(name) = ? AND id != ?").get(normalizedNewName, agent.id);
4504
- if (existing) {
4505
- throw new Error(`Agent name already taken: ${normalizedNewName}`);
4506
- }
4507
- d.run("UPDATE agents SET name = ? WHERE id = ?", [normalizedNewName, agent.id]);
4508
- }
4509
- }
4510
- if (updates.description !== undefined) {
4511
- d.run("UPDATE agents SET description = ? WHERE id = ?", [updates.description, agent.id]);
4512
- }
4513
- if (updates.role !== undefined) {
4514
- d.run("UPDATE agents SET role = ? WHERE id = ?", [updates.role, agent.id]);
4443
+ const conditions = [];
4444
+ const params = [];
4445
+ if (entityId) {
4446
+ conditions.push("entity_id = ?");
4447
+ params.push(entityId);
4515
4448
  }
4516
- if (updates.metadata !== undefined) {
4517
- d.run("UPDATE agents SET metadata = ? WHERE id = ?", [JSON.stringify(updates.metadata), agent.id]);
4449
+ if (memoryId) {
4450
+ conditions.push("memory_id = ?");
4451
+ params.push(memoryId);
4518
4452
  }
4519
- if ("active_project_id" in updates) {
4520
- d.run("UPDATE agents SET active_project_id = ? WHERE id = ?", [updates.active_project_id ?? null, agent.id]);
4453
+ let sql = "SELECT * FROM entity_memories";
4454
+ if (conditions.length > 0) {
4455
+ sql += ` WHERE ${conditions.join(" AND ")}`;
4521
4456
  }
4522
- d.run("UPDATE agents SET last_seen_at = ? WHERE id = ?", [timestamp, agent.id]);
4523
- return getAgent(agent.id, d);
4457
+ sql += " ORDER BY created_at DESC";
4458
+ const rows = d.query(sql).all(...params);
4459
+ return rows.map(parseEntityMemoryRow);
4524
4460
  }
4525
4461
 
4526
- // src/db/projects.ts
4527
- function parseProjectRow(row) {
4462
+ // src/db/memories.ts
4463
+ function runEntityExtraction(_memory, _projectId, _d) {}
4464
+ function parseMemoryRow(row) {
4528
4465
  return {
4529
4466
  id: row["id"],
4530
- name: row["name"],
4531
- path: row["path"],
4532
- description: row["description"] || null,
4533
- memory_prefix: row["memory_prefix"] || null,
4467
+ key: row["key"],
4468
+ value: row["value"],
4469
+ category: row["category"],
4470
+ scope: row["scope"],
4471
+ summary: row["summary"] || null,
4472
+ tags: JSON.parse(row["tags"] || "[]"),
4473
+ importance: row["importance"],
4474
+ source: row["source"],
4475
+ status: row["status"],
4476
+ pinned: !!row["pinned"],
4477
+ agent_id: row["agent_id"] || null,
4478
+ project_id: row["project_id"] || null,
4479
+ session_id: row["session_id"] || null,
4480
+ metadata: JSON.parse(row["metadata"] || "{}"),
4481
+ access_count: row["access_count"],
4482
+ version: row["version"],
4483
+ expires_at: row["expires_at"] || null,
4534
4484
  created_at: row["created_at"],
4535
- updated_at: row["updated_at"]
4485
+ updated_at: row["updated_at"],
4486
+ accessed_at: row["accessed_at"] || null
4536
4487
  };
4537
4488
  }
4538
- function registerProject(name, path, description, memoryPrefix, db) {
4489
+ function createMemory(input, dedupeMode = "merge", db) {
4539
4490
  const d = db || getDatabase();
4540
4491
  const timestamp = now();
4541
- const existing = d.query("SELECT * FROM projects WHERE path = ?").get(path);
4542
- if (existing) {
4543
- const existingId = existing["id"];
4544
- d.run("UPDATE projects SET updated_at = ? WHERE id = ?", [
4545
- timestamp,
4546
- existingId
4547
- ]);
4548
- return parseProjectRow(existing);
4492
+ let expiresAt = input.expires_at || null;
4493
+ if (input.ttl_ms && !expiresAt) {
4494
+ expiresAt = new Date(Date.now() + input.ttl_ms).toISOString();
4549
4495
  }
4550
4496
  const id = uuid();
4551
- 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]);
4552
- return getProject(id, d);
4553
- }
4554
- function getProject(idOrPath, db) {
4555
- const d = db || getDatabase();
4556
- let row = d.query("SELECT * FROM projects WHERE id = ?").get(idOrPath);
4557
- if (row)
4558
- return parseProjectRow(row);
4559
- row = d.query("SELECT * FROM projects WHERE path = ?").get(idOrPath);
4560
- if (row)
4561
- return parseProjectRow(row);
4562
- row = d.query("SELECT * FROM projects WHERE LOWER(name) = ?").get(idOrPath.toLowerCase());
4563
- if (row)
4564
- return parseProjectRow(row);
4565
- return null;
4566
- }
4567
- function listProjects(db) {
4568
- const d = db || getDatabase();
4569
- const rows = d.query("SELECT * FROM projects ORDER BY updated_at DESC").all();
4570
- return rows.map(parseProjectRow);
4571
- }
4572
-
4573
- // src/lib/extractor.ts
4574
- var TECH_KEYWORDS = new Set([
4575
- "typescript",
4576
- "javascript",
4577
- "python",
4578
- "rust",
4579
- "go",
4580
- "java",
4581
- "ruby",
4582
- "swift",
4583
- "kotlin",
4584
- "react",
4585
- "vue",
4586
- "angular",
4587
- "svelte",
4588
- "nextjs",
4589
- "bun",
4590
- "node",
4591
- "deno",
4592
- "sqlite",
4593
- "postgres",
4594
- "mysql",
4595
- "redis",
4596
- "docker",
4597
- "kubernetes",
4598
- "git",
4599
- "npm",
4600
- "yarn",
4601
- "pnpm",
4602
- "webpack",
4603
- "vite",
4604
- "tailwind",
4605
- "prisma",
4606
- "drizzle",
4607
- "zod",
4608
- "commander",
4609
- "express",
4610
- "fastify",
4611
- "hono"
4612
- ]);
4613
- var FILE_PATH_RE = /(?:^|\s)((?:\/|\.\/|~\/)?(?:[\w.-]+\/)+[\w.-]+\.\w+)/g;
4614
- var URL_RE = /https?:\/\/[^\s)]+/g;
4615
- var NPM_PACKAGE_RE = /@[\w-]+\/[\w.-]+/g;
4616
- var PASCAL_CASE_RE = /\b([A-Z][a-z]+(?:[A-Z][a-z]+)+)\b/g;
4617
- function getSearchText(memory) {
4618
- const parts = [memory.key, memory.value];
4619
- if (memory.summary)
4620
- parts.push(memory.summary);
4621
- return parts.join(" ");
4622
- }
4623
- function extractEntities(memory, db) {
4624
- const text = getSearchText(memory);
4625
- const entityMap = new Map;
4626
- function add(name, type, confidence) {
4627
- const normalized = name.toLowerCase();
4628
- if (normalized.length < 3)
4629
- return;
4630
- const existing = entityMap.get(normalized);
4631
- if (!existing || existing.confidence < confidence) {
4632
- entityMap.set(normalized, { name: normalized, type, confidence });
4633
- }
4634
- }
4635
- for (const match of text.matchAll(FILE_PATH_RE)) {
4636
- add(match[1].trim(), "file", 0.9);
4637
- }
4638
- for (const match of text.matchAll(URL_RE)) {
4639
- add(match[0], "api", 0.8);
4640
- }
4641
- for (const match of text.matchAll(NPM_PACKAGE_RE)) {
4642
- add(match[0], "tool", 0.85);
4643
- }
4644
- try {
4645
- const d = db || getDatabase();
4646
- const agents = listAgents(d);
4647
- const textLower2 = text.toLowerCase();
4648
- for (const agent of agents) {
4649
- const nameLower = agent.name.toLowerCase();
4650
- if (nameLower.length >= 3 && textLower2.includes(nameLower)) {
4651
- add(agent.name, "person", 0.95);
4497
+ const tags = input.tags || [];
4498
+ const tagsJson = JSON.stringify(tags);
4499
+ const metadataJson = JSON.stringify(input.metadata || {});
4500
+ const safeValue = redactSecrets(input.value);
4501
+ const safeSummary = input.summary ? redactSecrets(input.summary) : null;
4502
+ if (dedupeMode === "merge") {
4503
+ const existing = d.query(`SELECT id, version FROM memories
4504
+ WHERE key = ? AND scope = ?
4505
+ AND COALESCE(agent_id, '') = ?
4506
+ AND COALESCE(project_id, '') = ?
4507
+ AND COALESCE(session_id, '') = ?`).get(input.key, input.scope || "private", input.agent_id || "", input.project_id || "", input.session_id || "");
4508
+ if (existing) {
4509
+ d.run(`UPDATE memories SET
4510
+ value = ?, category = ?, summary = ?, tags = ?,
4511
+ importance = ?, metadata = ?, expires_at = ?,
4512
+ pinned = COALESCE(pinned, 0),
4513
+ version = version + 1, updated_at = ?
4514
+ WHERE id = ?`, [
4515
+ safeValue,
4516
+ input.category || "knowledge",
4517
+ safeSummary,
4518
+ tagsJson,
4519
+ input.importance ?? 5,
4520
+ metadataJson,
4521
+ expiresAt,
4522
+ timestamp,
4523
+ existing.id
4524
+ ]);
4525
+ d.run("DELETE FROM memory_tags WHERE memory_id = ?", [existing.id]);
4526
+ const insertTag2 = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
4527
+ for (const tag of tags) {
4528
+ insertTag2.run(existing.id, tag);
4652
4529
  }
4530
+ const merged = getMemory(existing.id, d);
4531
+ try {
4532
+ const oldLinks = getEntityMemoryLinks(undefined, merged.id, d);
4533
+ for (const link of oldLinks) {
4534
+ unlinkEntityFromMemory(link.entity_id, merged.id, d);
4535
+ }
4536
+ runEntityExtraction(merged, input.project_id, d);
4537
+ } catch {}
4538
+ return merged;
4653
4539
  }
4654
- } catch {}
4655
- try {
4656
- const d = db || getDatabase();
4657
- const projects = listProjects(d);
4658
- const textLower2 = text.toLowerCase();
4659
- for (const project of projects) {
4660
- const nameLower = project.name.toLowerCase();
4661
- if (nameLower.length >= 3 && textLower2.includes(nameLower)) {
4662
- add(project.name, "project", 0.95);
4663
- }
4664
- }
4665
- } catch {}
4666
- const textLower = text.toLowerCase();
4667
- for (const keyword of TECH_KEYWORDS) {
4668
- const re = new RegExp(`\\b${keyword}\\b`, "i");
4669
- if (re.test(textLower)) {
4670
- add(keyword, "tool", 0.7);
4671
- }
4672
- }
4673
- for (const match of text.matchAll(PASCAL_CASE_RE)) {
4674
- add(match[1], "concept", 0.5);
4675
- }
4676
- return Array.from(entityMap.values()).sort((a, b) => b.confidence - a.confidence);
4677
- }
4678
-
4679
- // src/db/entities.ts
4680
- function parseEntityRow(row) {
4681
- return {
4682
- id: row["id"],
4683
- name: row["name"],
4684
- type: row["type"],
4685
- description: row["description"] || null,
4686
- metadata: JSON.parse(row["metadata"] || "{}"),
4687
- project_id: row["project_id"] || null,
4688
- created_at: row["created_at"],
4689
- updated_at: row["updated_at"]
4690
- };
4691
- }
4692
- function createEntity(input, db) {
4693
- const d = db || getDatabase();
4694
- const timestamp = now();
4695
- const metadataJson = JSON.stringify(input.metadata || {});
4696
- const existing = d.query(`SELECT * FROM entities
4697
- WHERE name = ? AND type = ? AND COALESCE(project_id, '') = ?`).get(input.name, input.type, input.project_id || "");
4698
- if (existing) {
4699
- const sets = ["updated_at = ?"];
4700
- const params = [timestamp];
4701
- if (input.description !== undefined) {
4702
- sets.push("description = ?");
4703
- params.push(input.description);
4704
- }
4705
- if (input.metadata !== undefined) {
4706
- sets.push("metadata = ?");
4707
- params.push(metadataJson);
4708
- }
4709
- const existingId = existing["id"];
4710
- params.push(existingId);
4711
- d.run(`UPDATE entities SET ${sets.join(", ")} WHERE id = ?`, params);
4712
- return getEntity(existingId, d);
4713
4540
  }
4714
- const id = shortUuid();
4715
- d.run(`INSERT INTO entities (id, name, type, description, metadata, project_id, created_at, updated_at)
4716
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
4541
+ 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)
4542
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', 0, ?, ?, ?, ?, 0, 1, ?, ?, ?)`, [
4717
4543
  id,
4718
- input.name,
4719
- input.type,
4720
- input.description || null,
4721
- metadataJson,
4544
+ input.key,
4545
+ input.value,
4546
+ input.category || "knowledge",
4547
+ input.scope || "private",
4548
+ input.summary || null,
4549
+ tagsJson,
4550
+ input.importance ?? 5,
4551
+ input.source || "agent",
4552
+ input.agent_id || null,
4722
4553
  input.project_id || null,
4554
+ input.session_id || null,
4555
+ metadataJson,
4556
+ expiresAt,
4723
4557
  timestamp,
4724
4558
  timestamp
4725
4559
  ]);
4726
- return getEntity(id, d);
4560
+ const insertTag = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
4561
+ for (const tag of tags) {
4562
+ insertTag.run(id, tag);
4563
+ }
4564
+ const memory = getMemory(id, d);
4565
+ try {
4566
+ runEntityExtraction(memory, input.project_id, d);
4567
+ } catch {}
4568
+ return memory;
4727
4569
  }
4728
- function getEntity(id, db) {
4570
+ function getMemory(id, db) {
4729
4571
  const d = db || getDatabase();
4730
- const row = d.query("SELECT * FROM entities WHERE id = ?").get(id);
4572
+ const row = d.query("SELECT * FROM memories WHERE id = ?").get(id);
4731
4573
  if (!row)
4732
- throw new EntityNotFoundError(id);
4733
- return parseEntityRow(row);
4574
+ return null;
4575
+ return parseMemoryRow(row);
4734
4576
  }
4735
- function getEntityByName(name, type, projectId, db) {
4577
+ function getMemoryByKey(key, scope, agentId, projectId, sessionId, db) {
4736
4578
  const d = db || getDatabase();
4737
- let sql = "SELECT * FROM entities WHERE name = ?";
4738
- const params = [name];
4739
- if (type) {
4740
- sql += " AND type = ?";
4741
- params.push(type);
4579
+ let sql = "SELECT * FROM memories WHERE key = ?";
4580
+ const params = [key];
4581
+ if (scope) {
4582
+ sql += " AND scope = ?";
4583
+ params.push(scope);
4742
4584
  }
4743
- if (projectId !== undefined) {
4585
+ if (agentId) {
4586
+ sql += " AND agent_id = ?";
4587
+ params.push(agentId);
4588
+ }
4589
+ if (projectId) {
4744
4590
  sql += " AND project_id = ?";
4745
4591
  params.push(projectId);
4746
4592
  }
4747
- sql += " LIMIT 1";
4593
+ if (sessionId) {
4594
+ sql += " AND session_id = ?";
4595
+ params.push(sessionId);
4596
+ }
4597
+ sql += " AND status = 'active' ORDER BY importance DESC LIMIT 1";
4748
4598
  const row = d.query(sql).get(...params);
4749
4599
  if (!row)
4750
4600
  return null;
4751
- return parseEntityRow(row);
4601
+ return parseMemoryRow(row);
4752
4602
  }
4753
- function listEntities(filter = {}, db) {
4603
+ function listMemories(filter, db) {
4754
4604
  const d = db || getDatabase();
4755
4605
  const conditions = [];
4756
4606
  const params = [];
4757
- if (filter.type) {
4758
- conditions.push("type = ?");
4759
- params.push(filter.type);
4760
- }
4761
- if (filter.project_id) {
4762
- conditions.push("project_id = ?");
4763
- params.push(filter.project_id);
4764
- }
4765
- if (filter.search) {
4766
- conditions.push("(name LIKE ? OR description LIKE ?)");
4767
- const term = `%${filter.search}%`;
4768
- params.push(term, term);
4607
+ if (filter) {
4608
+ if (filter.scope) {
4609
+ if (Array.isArray(filter.scope)) {
4610
+ conditions.push(`scope IN (${filter.scope.map(() => "?").join(",")})`);
4611
+ params.push(...filter.scope);
4612
+ } else {
4613
+ conditions.push("scope = ?");
4614
+ params.push(filter.scope);
4615
+ }
4616
+ }
4617
+ if (filter.category) {
4618
+ if (Array.isArray(filter.category)) {
4619
+ conditions.push(`category IN (${filter.category.map(() => "?").join(",")})`);
4620
+ params.push(...filter.category);
4621
+ } else {
4622
+ conditions.push("category = ?");
4623
+ params.push(filter.category);
4624
+ }
4625
+ }
4626
+ if (filter.source) {
4627
+ if (Array.isArray(filter.source)) {
4628
+ conditions.push(`source IN (${filter.source.map(() => "?").join(",")})`);
4629
+ params.push(...filter.source);
4630
+ } else {
4631
+ conditions.push("source = ?");
4632
+ params.push(filter.source);
4633
+ }
4634
+ }
4635
+ if (filter.status) {
4636
+ if (Array.isArray(filter.status)) {
4637
+ conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
4638
+ params.push(...filter.status);
4639
+ } else {
4640
+ conditions.push("status = ?");
4641
+ params.push(filter.status);
4642
+ }
4643
+ } else {
4644
+ conditions.push("status = 'active'");
4645
+ }
4646
+ if (filter.project_id) {
4647
+ conditions.push("project_id = ?");
4648
+ params.push(filter.project_id);
4649
+ }
4650
+ if (filter.agent_id) {
4651
+ conditions.push("agent_id = ?");
4652
+ params.push(filter.agent_id);
4653
+ }
4654
+ if (filter.session_id) {
4655
+ conditions.push("session_id = ?");
4656
+ params.push(filter.session_id);
4657
+ }
4658
+ if (filter.min_importance) {
4659
+ conditions.push("importance >= ?");
4660
+ params.push(filter.min_importance);
4661
+ }
4662
+ if (filter.pinned !== undefined) {
4663
+ conditions.push("pinned = ?");
4664
+ params.push(filter.pinned ? 1 : 0);
4665
+ }
4666
+ if (filter.tags && filter.tags.length > 0) {
4667
+ for (const tag of filter.tags) {
4668
+ conditions.push("id IN (SELECT memory_id FROM memory_tags WHERE tag = ?)");
4669
+ params.push(tag);
4670
+ }
4671
+ }
4672
+ if (filter.search) {
4673
+ conditions.push("(key LIKE ? OR value LIKE ? OR summary LIKE ?)");
4674
+ const term = `%${filter.search}%`;
4675
+ params.push(term, term, term);
4676
+ }
4677
+ } else {
4678
+ conditions.push("status = 'active'");
4769
4679
  }
4770
- let sql = "SELECT * FROM entities";
4680
+ let sql = "SELECT * FROM memories";
4771
4681
  if (conditions.length > 0) {
4772
4682
  sql += ` WHERE ${conditions.join(" AND ")}`;
4773
4683
  }
4774
- sql += " ORDER BY updated_at DESC";
4775
- if (filter.limit) {
4684
+ sql += " ORDER BY importance DESC, created_at DESC";
4685
+ if (filter?.limit) {
4776
4686
  sql += " LIMIT ?";
4777
4687
  params.push(filter.limit);
4778
4688
  }
4779
- if (filter.offset) {
4689
+ if (filter?.offset) {
4780
4690
  sql += " OFFSET ?";
4781
4691
  params.push(filter.offset);
4782
4692
  }
4783
4693
  const rows = d.query(sql).all(...params);
4784
- return rows.map(parseEntityRow);
4694
+ return rows.map(parseMemoryRow);
4785
4695
  }
4786
- function updateEntity(id, input, db) {
4696
+ function updateMemory(id, input, db) {
4787
4697
  const d = db || getDatabase();
4788
- const existing = d.query("SELECT id FROM entities WHERE id = ?").get(id);
4698
+ const existing = getMemory(id, d);
4789
4699
  if (!existing)
4790
- throw new EntityNotFoundError(id);
4791
- const sets = ["updated_at = ?"];
4700
+ throw new MemoryNotFoundError(id);
4701
+ if (existing.version !== input.version) {
4702
+ throw new VersionConflictError(id, input.version, existing.version);
4703
+ }
4704
+ try {
4705
+ d.run(`INSERT OR IGNORE INTO memory_versions (id, memory_id, version, value, importance, scope, category, tags, summary, pinned, status, created_at)
4706
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
4707
+ uuid(),
4708
+ existing.id,
4709
+ existing.version,
4710
+ existing.value,
4711
+ existing.importance,
4712
+ existing.scope,
4713
+ existing.category,
4714
+ JSON.stringify(existing.tags),
4715
+ existing.summary,
4716
+ existing.pinned ? 1 : 0,
4717
+ existing.status,
4718
+ existing.updated_at
4719
+ ]);
4720
+ } catch {}
4721
+ const sets = ["version = version + 1", "updated_at = ?"];
4792
4722
  const params = [now()];
4793
- if (input.name !== undefined) {
4794
- sets.push("name = ?");
4795
- params.push(input.name);
4723
+ if (input.value !== undefined) {
4724
+ sets.push("value = ?");
4725
+ params.push(redactSecrets(input.value));
4796
4726
  }
4797
- if (input.type !== undefined) {
4798
- sets.push("type = ?");
4799
- params.push(input.type);
4727
+ if (input.category !== undefined) {
4728
+ sets.push("category = ?");
4729
+ params.push(input.category);
4800
4730
  }
4801
- if (input.description !== undefined) {
4802
- sets.push("description = ?");
4803
- params.push(input.description);
4731
+ if (input.scope !== undefined) {
4732
+ sets.push("scope = ?");
4733
+ params.push(input.scope);
4804
4734
  }
4805
- if (input.metadata !== undefined) {
4806
- sets.push("metadata = ?");
4807
- params.push(JSON.stringify(input.metadata));
4735
+ if (input.summary !== undefined) {
4736
+ sets.push("summary = ?");
4737
+ params.push(input.summary);
4738
+ }
4739
+ if (input.importance !== undefined) {
4740
+ sets.push("importance = ?");
4741
+ params.push(input.importance);
4742
+ }
4743
+ if (input.pinned !== undefined) {
4744
+ sets.push("pinned = ?");
4745
+ params.push(input.pinned ? 1 : 0);
4746
+ }
4747
+ if (input.status !== undefined) {
4748
+ sets.push("status = ?");
4749
+ params.push(input.status);
4750
+ }
4751
+ if (input.metadata !== undefined) {
4752
+ sets.push("metadata = ?");
4753
+ params.push(JSON.stringify(input.metadata));
4754
+ }
4755
+ if (input.expires_at !== undefined) {
4756
+ sets.push("expires_at = ?");
4757
+ params.push(input.expires_at);
4758
+ }
4759
+ if (input.tags !== undefined) {
4760
+ sets.push("tags = ?");
4761
+ params.push(JSON.stringify(input.tags));
4762
+ d.run("DELETE FROM memory_tags WHERE memory_id = ?", [id]);
4763
+ const insertTag = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
4764
+ for (const tag of input.tags) {
4765
+ insertTag.run(id, tag);
4766
+ }
4808
4767
  }
4809
4768
  params.push(id);
4810
- d.run(`UPDATE entities SET ${sets.join(", ")} WHERE id = ?`, params);
4811
- return getEntity(id, d);
4812
- }
4813
- function deleteEntity(id, db) {
4814
- const d = db || getDatabase();
4815
- const result = d.run("DELETE FROM entities WHERE id = ?", [id]);
4816
- if (result.changes === 0)
4817
- throw new EntityNotFoundError(id);
4769
+ d.run(`UPDATE memories SET ${sets.join(", ")} WHERE id = ?`, params);
4770
+ const updated = getMemory(id, d);
4771
+ try {
4772
+ if (input.value !== undefined) {
4773
+ const oldLinks = getEntityMemoryLinks(undefined, updated.id, d);
4774
+ for (const link of oldLinks) {
4775
+ unlinkEntityFromMemory(link.entity_id, updated.id, d);
4776
+ }
4777
+ runEntityExtraction(updated, existing.project_id || undefined, d);
4778
+ }
4779
+ } catch {}
4780
+ return updated;
4818
4781
  }
4819
- function mergeEntities(sourceId, targetId, db) {
4782
+ function deleteMemory(id, db) {
4820
4783
  const d = db || getDatabase();
4821
- getEntity(sourceId, d);
4822
- getEntity(targetId, d);
4823
- d.run(`UPDATE OR IGNORE relations SET source_entity_id = ? WHERE source_entity_id = ?`, [targetId, sourceId]);
4824
- d.run(`UPDATE OR IGNORE relations SET target_entity_id = ? WHERE target_entity_id = ?`, [targetId, sourceId]);
4825
- d.run("DELETE FROM relations WHERE source_entity_id = ? OR target_entity_id = ?", [
4826
- sourceId,
4827
- sourceId
4828
- ]);
4829
- d.run(`UPDATE OR IGNORE entity_memories SET entity_id = ? WHERE entity_id = ?`, [targetId, sourceId]);
4830
- d.run("DELETE FROM entity_memories WHERE entity_id = ?", [sourceId]);
4831
- d.run("DELETE FROM entities WHERE id = ?", [sourceId]);
4832
- d.run("UPDATE entities SET updated_at = ? WHERE id = ?", [now(), targetId]);
4833
- return getEntity(targetId, d);
4834
- }
4835
-
4836
- // src/db/entity-memories.ts
4837
- function parseEntityMemoryRow(row) {
4838
- return {
4839
- entity_id: row["entity_id"],
4840
- memory_id: row["memory_id"],
4841
- role: row["role"],
4842
- created_at: row["created_at"]
4843
- };
4784
+ const result = d.run("DELETE FROM memories WHERE id = ?", [id]);
4785
+ return result.changes > 0;
4844
4786
  }
4845
- function linkEntityToMemory(entityId, memoryId, role = "context", db) {
4787
+ function bulkDeleteMemories(ids, db) {
4846
4788
  const d = db || getDatabase();
4847
- const timestamp = now();
4848
- d.run(`INSERT OR IGNORE INTO entity_memories (entity_id, memory_id, role, created_at)
4849
- VALUES (?, ?, ?, ?)`, [entityId, memoryId, role, timestamp]);
4850
- const row = d.query("SELECT * FROM entity_memories WHERE entity_id = ? AND memory_id = ?").get(entityId, memoryId);
4851
- return parseEntityMemoryRow(row);
4789
+ if (ids.length === 0)
4790
+ return 0;
4791
+ const placeholders = ids.map(() => "?").join(",");
4792
+ const countRow = d.query(`SELECT COUNT(*) as c FROM memories WHERE id IN (${placeholders})`).get(...ids);
4793
+ const count = countRow.c;
4794
+ if (count > 0) {
4795
+ d.run(`DELETE FROM memories WHERE id IN (${placeholders})`, ids);
4796
+ }
4797
+ return count;
4852
4798
  }
4853
- function unlinkEntityFromMemory(entityId, memoryId, db) {
4799
+ function touchMemory(id, db) {
4854
4800
  const d = db || getDatabase();
4855
- d.run("DELETE FROM entity_memories WHERE entity_id = ? AND memory_id = ?", [entityId, memoryId]);
4801
+ d.run("UPDATE memories SET access_count = access_count + 1, accessed_at = ? WHERE id = ?", [now(), id]);
4856
4802
  }
4857
- function getMemoriesForEntity(entityId, db) {
4803
+ function cleanExpiredMemories(db) {
4858
4804
  const d = db || getDatabase();
4859
- const rows = d.query(`SELECT m.* FROM memories m
4860
- INNER JOIN entity_memories em ON em.memory_id = m.id
4861
- WHERE em.entity_id = ?
4862
- ORDER BY m.importance DESC, m.created_at DESC`).all(entityId);
4863
- return rows.map(parseMemoryRow);
4805
+ const timestamp = now();
4806
+ const countRow = d.query("SELECT COUNT(*) as c FROM memories WHERE expires_at IS NOT NULL AND expires_at < ?").get(timestamp);
4807
+ const count = countRow.c;
4808
+ if (count > 0) {
4809
+ d.run("DELETE FROM memories WHERE expires_at IS NOT NULL AND expires_at < ?", [timestamp]);
4810
+ }
4811
+ return count;
4864
4812
  }
4865
- function getEntityMemoryLinks(entityId, memoryId, db) {
4813
+ function getMemoryVersions(memoryId, db) {
4866
4814
  const d = db || getDatabase();
4867
- const conditions = [];
4868
- const params = [];
4869
- if (entityId) {
4870
- conditions.push("entity_id = ?");
4871
- params.push(entityId);
4872
- }
4873
- if (memoryId) {
4874
- conditions.push("memory_id = ?");
4875
- params.push(memoryId);
4876
- }
4877
- let sql = "SELECT * FROM entity_memories";
4878
- if (conditions.length > 0) {
4879
- sql += ` WHERE ${conditions.join(" AND ")}`;
4815
+ try {
4816
+ const rows = d.query("SELECT * FROM memory_versions WHERE memory_id = ? ORDER BY version ASC").all(memoryId);
4817
+ return rows.map((row) => ({
4818
+ id: row["id"],
4819
+ memory_id: row["memory_id"],
4820
+ version: row["version"],
4821
+ value: row["value"],
4822
+ importance: row["importance"],
4823
+ scope: row["scope"],
4824
+ category: row["category"],
4825
+ tags: JSON.parse(row["tags"] || "[]"),
4826
+ summary: row["summary"] || null,
4827
+ pinned: !!row["pinned"],
4828
+ status: row["status"],
4829
+ created_at: row["created_at"]
4830
+ }));
4831
+ } catch {
4832
+ return [];
4880
4833
  }
4881
- sql += " ORDER BY created_at DESC";
4882
- const rows = d.query(sql).all(...params);
4883
- return rows.map(parseEntityMemoryRow);
4884
4834
  }
4885
4835
 
4886
- // src/db/relations.ts
4887
- function parseRelationRow(row) {
4888
- return {
4889
- id: row["id"],
4890
- source_entity_id: row["source_entity_id"],
4891
- target_entity_id: row["target_entity_id"],
4892
- relation_type: row["relation_type"],
4893
- weight: row["weight"],
4894
- metadata: JSON.parse(row["metadata"] || "{}"),
4895
- created_at: row["created_at"]
4896
- };
4897
- }
4898
- function parseEntityRow2(row) {
4836
+ // src/db/agents.ts
4837
+ var CONFLICT_WINDOW_MS = 30 * 60 * 1000;
4838
+ function parseAgentRow(row) {
4899
4839
  return {
4900
4840
  id: row["id"],
4901
4841
  name: row["name"],
4902
- type: row["type"],
4842
+ session_id: row["session_id"] || null,
4903
4843
  description: row["description"] || null,
4844
+ role: row["role"] || null,
4904
4845
  metadata: JSON.parse(row["metadata"] || "{}"),
4905
- project_id: row["project_id"] || null,
4846
+ active_project_id: row["active_project_id"] || null,
4906
4847
  created_at: row["created_at"],
4907
- updated_at: row["updated_at"]
4848
+ last_seen_at: row["last_seen_at"]
4908
4849
  };
4909
4850
  }
4910
- function createRelation(input, db) {
4851
+ function registerAgent(name, sessionId, description, role, projectId, db) {
4911
4852
  const d = db || getDatabase();
4912
- const id = shortUuid();
4913
4853
  const timestamp = now();
4914
- const weight = input.weight ?? 1;
4915
- const metadata = JSON.stringify(input.metadata ?? {});
4916
- d.run(`INSERT INTO relations (id, source_entity_id, target_entity_id, relation_type, weight, metadata, created_at)
4917
- VALUES (?, ?, ?, ?, ?, ?, ?)
4918
- ON CONFLICT(source_entity_id, target_entity_id, relation_type)
4919
- DO UPDATE SET weight = excluded.weight, metadata = excluded.metadata`, [id, input.source_entity_id, input.target_entity_id, input.relation_type, weight, metadata, timestamp]);
4920
- const row = d.query(`SELECT * FROM relations
4921
- WHERE source_entity_id = ? AND target_entity_id = ? AND relation_type = ?`).get(input.source_entity_id, input.target_entity_id, input.relation_type);
4922
- return parseRelationRow(row);
4923
- }
4924
- function getRelation(id, db) {
4925
- const d = db || getDatabase();
4926
- const row = d.query("SELECT * FROM relations WHERE id = ?").get(id);
4927
- if (!row)
4928
- throw new Error(`Relation not found: ${id}`);
4929
- return parseRelationRow(row);
4930
- }
4931
- function listRelations(filter, db) {
4932
- const d = db || getDatabase();
4933
- const conditions = [];
4934
- const params = [];
4935
- if (filter.entity_id) {
4936
- const dir = filter.direction || "both";
4937
- if (dir === "outgoing") {
4938
- conditions.push("source_entity_id = ?");
4939
- params.push(filter.entity_id);
4940
- } else if (dir === "incoming") {
4941
- conditions.push("target_entity_id = ?");
4942
- params.push(filter.entity_id);
4943
- } else {
4944
- conditions.push("(source_entity_id = ? OR target_entity_id = ?)");
4945
- params.push(filter.entity_id, filter.entity_id);
4854
+ const normalizedName = name.trim().toLowerCase();
4855
+ const existing = d.query("SELECT * FROM agents WHERE LOWER(name) = ?").get(normalizedName);
4856
+ if (existing) {
4857
+ const existingId = existing["id"];
4858
+ const existingSessionId = existing["session_id"] || null;
4859
+ const existingLastSeen = existing["last_seen_at"];
4860
+ if (sessionId && existingSessionId && existingSessionId !== sessionId) {
4861
+ const lastSeenMs = new Date(existingLastSeen).getTime();
4862
+ const nowMs = Date.now();
4863
+ if (nowMs - lastSeenMs < CONFLICT_WINDOW_MS) {
4864
+ throw new AgentConflictError({
4865
+ existing_id: existingId,
4866
+ existing_name: normalizedName,
4867
+ last_seen_at: existingLastSeen,
4868
+ session_hint: existingSessionId.slice(0, 8),
4869
+ working_dir: null
4870
+ });
4871
+ }
4946
4872
  }
4873
+ d.run("UPDATE agents SET last_seen_at = ?, session_id = ? WHERE id = ?", [
4874
+ timestamp,
4875
+ sessionId ?? existingSessionId,
4876
+ existingId
4877
+ ]);
4878
+ if (description) {
4879
+ d.run("UPDATE agents SET description = ? WHERE id = ?", [description, existingId]);
4880
+ }
4881
+ if (role) {
4882
+ d.run("UPDATE agents SET role = ? WHERE id = ?", [role, existingId]);
4883
+ }
4884
+ if (projectId !== undefined) {
4885
+ d.run("UPDATE agents SET active_project_id = ? WHERE id = ?", [projectId, existingId]);
4886
+ }
4887
+ return getAgent(existingId, d);
4947
4888
  }
4948
- if (filter.relation_type) {
4949
- conditions.push("relation_type = ?");
4950
- params.push(filter.relation_type);
4951
- }
4952
- const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
4953
- const rows = d.query(`SELECT * FROM relations ${where} ORDER BY created_at DESC`).all(...params);
4954
- return rows.map(parseRelationRow);
4955
- }
4956
- function deleteRelation(id, db) {
4957
- const d = db || getDatabase();
4958
- const result = d.run("DELETE FROM relations WHERE id = ?", [id]);
4959
- if (result.changes === 0)
4960
- throw new Error(`Relation not found: ${id}`);
4889
+ const id = shortUuid();
4890
+ 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]);
4891
+ return getAgent(id, d);
4961
4892
  }
4962
- function getEntityGraph(entityId, depth = 2, db) {
4893
+ function getAgent(idOrName, db) {
4963
4894
  const d = db || getDatabase();
4964
- const entityRows = d.query(`WITH RECURSIVE graph(id, depth) AS (
4965
- VALUES(?, 0)
4966
- UNION
4967
- SELECT CASE WHEN r.source_entity_id = g.id THEN r.target_entity_id ELSE r.source_entity_id END, g.depth + 1
4968
- FROM relations r JOIN graph g ON (r.source_entity_id = g.id OR r.target_entity_id = g.id)
4969
- WHERE g.depth < ?
4970
- )
4971
- SELECT DISTINCT e.* FROM entities e JOIN graph g ON e.id = g.id`).all(entityId, depth);
4972
- const entities = entityRows.map(parseEntityRow2);
4973
- const entityIds = new Set(entities.map((e) => e.id));
4974
- if (entityIds.size === 0) {
4975
- return { entities: [], relations: [] };
4976
- }
4977
- const placeholders = Array.from(entityIds).map(() => "?").join(",");
4978
- const relationRows = d.query(`SELECT * FROM relations
4979
- WHERE source_entity_id IN (${placeholders})
4980
- AND target_entity_id IN (${placeholders})`).all(...Array.from(entityIds), ...Array.from(entityIds));
4981
- const relations = relationRows.map(parseRelationRow);
4982
- return { entities, relations };
4895
+ let row = d.query("SELECT * FROM agents WHERE id = ?").get(idOrName);
4896
+ if (row)
4897
+ return parseAgentRow(row);
4898
+ row = d.query("SELECT * FROM agents WHERE LOWER(name) = ?").get(idOrName.trim().toLowerCase());
4899
+ if (row)
4900
+ return parseAgentRow(row);
4901
+ const rows = d.query("SELECT * FROM agents WHERE id LIKE ?").all(`${idOrName}%`);
4902
+ if (rows.length === 1)
4903
+ return parseAgentRow(rows[0]);
4904
+ return null;
4983
4905
  }
4984
- function findPath(fromEntityId, toEntityId, maxDepth = 5, db) {
4906
+ function listAgents(db) {
4985
4907
  const d = db || getDatabase();
4986
- const rows = d.query(`WITH RECURSIVE path(id, trail, depth) AS (
4987
- SELECT ?, ?, 0
4988
- UNION
4989
- SELECT
4990
- CASE WHEN r.source_entity_id = p.id THEN r.target_entity_id ELSE r.source_entity_id END,
4991
- p.trail || ',' || CASE WHEN r.source_entity_id = p.id THEN r.target_entity_id ELSE r.source_entity_id END,
4992
- p.depth + 1
4993
- FROM relations r JOIN path p ON (r.source_entity_id = p.id OR r.target_entity_id = p.id)
4994
- WHERE p.depth < ?
4995
- AND INSTR(p.trail, CASE WHEN r.source_entity_id = p.id THEN r.target_entity_id ELSE r.source_entity_id END) = 0
4996
- )
4997
- SELECT trail FROM path WHERE id = ? ORDER BY depth ASC LIMIT 1`).get(fromEntityId, fromEntityId, maxDepth, toEntityId);
4998
- if (!rows)
4908
+ const rows = d.query("SELECT * FROM agents ORDER BY last_seen_at DESC").all();
4909
+ return rows.map(parseAgentRow);
4910
+ }
4911
+ function touchAgent(idOrName, db) {
4912
+ const d = db || getDatabase();
4913
+ const agent = getAgent(idOrName, d);
4914
+ if (!agent)
4915
+ return;
4916
+ d.run("UPDATE agents SET last_seen_at = ? WHERE id = ?", [now(), agent.id]);
4917
+ }
4918
+ function listAgentsByProject(projectId, db) {
4919
+ const d = db || getDatabase();
4920
+ const rows = d.query("SELECT * FROM agents WHERE active_project_id = ? ORDER BY last_seen_at DESC").all(projectId);
4921
+ return rows.map(parseAgentRow);
4922
+ }
4923
+ function updateAgent(id, updates, db) {
4924
+ const d = db || getDatabase();
4925
+ const agent = getAgent(id, d);
4926
+ if (!agent)
4999
4927
  return null;
5000
- const ids = rows.trail.split(",");
5001
- const entities = [];
5002
- for (const id of ids) {
5003
- const row = d.query("SELECT * FROM entities WHERE id = ?").get(id);
5004
- if (row)
5005
- entities.push(parseEntityRow2(row));
4928
+ const timestamp = now();
4929
+ if (updates.name) {
4930
+ const normalizedNewName = updates.name.trim().toLowerCase();
4931
+ if (normalizedNewName !== agent.name) {
4932
+ const existing = d.query("SELECT id FROM agents WHERE LOWER(name) = ? AND id != ?").get(normalizedNewName, agent.id);
4933
+ if (existing) {
4934
+ throw new Error(`Agent name already taken: ${normalizedNewName}`);
4935
+ }
4936
+ d.run("UPDATE agents SET name = ? WHERE id = ?", [normalizedNewName, agent.id]);
4937
+ }
5006
4938
  }
5007
- return entities.length > 0 ? entities : null;
5008
- }
5009
-
5010
- // src/lib/config.ts
5011
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, readdirSync, writeFileSync, unlinkSync } from "fs";
5012
- import { homedir } from "os";
5013
- import { basename, dirname as dirname2, join as join2, resolve as resolve2 } from "path";
5014
- var DEFAULT_CONFIG = {
5015
- default_scope: "private",
5016
- default_category: "knowledge",
5017
- default_importance: 5,
5018
- max_entries: 1000,
5019
- max_entries_per_scope: {
5020
- global: 500,
5021
- shared: 300,
5022
- private: 200
5023
- },
5024
- injection: {
5025
- max_tokens: 500,
5026
- min_importance: 5,
5027
- categories: ["preference", "fact"],
5028
- refresh_interval: 5
5029
- },
5030
- extraction: {
5031
- enabled: true,
5032
- min_confidence: 0.5
5033
- },
5034
- sync_agents: ["claude", "codex", "gemini"],
5035
- auto_cleanup: {
5036
- enabled: true,
5037
- expired_check_interval: 3600,
5038
- unused_archive_days: 7,
5039
- stale_deprioritize_days: 14
4939
+ if (updates.description !== undefined) {
4940
+ d.run("UPDATE agents SET description = ? WHERE id = ?", [updates.description, agent.id]);
5040
4941
  }
5041
- };
5042
- function deepMerge(target, source) {
5043
- const result = { ...target };
5044
- for (const key of Object.keys(source)) {
5045
- const sourceVal = source[key];
5046
- const targetVal = result[key];
5047
- if (sourceVal !== null && typeof sourceVal === "object" && !Array.isArray(sourceVal) && targetVal !== null && typeof targetVal === "object" && !Array.isArray(targetVal)) {
5048
- result[key] = deepMerge(targetVal, sourceVal);
5049
- } else {
5050
- result[key] = sourceVal;
5051
- }
4942
+ if (updates.role !== undefined) {
4943
+ d.run("UPDATE agents SET role = ? WHERE id = ?", [updates.role, agent.id]);
5052
4944
  }
5053
- return result;
4945
+ if (updates.metadata !== undefined) {
4946
+ d.run("UPDATE agents SET metadata = ? WHERE id = ?", [JSON.stringify(updates.metadata), agent.id]);
4947
+ }
4948
+ if ("active_project_id" in updates) {
4949
+ d.run("UPDATE agents SET active_project_id = ? WHERE id = ?", [updates.active_project_id ?? null, agent.id]);
4950
+ }
4951
+ d.run("UPDATE agents SET last_seen_at = ? WHERE id = ?", [timestamp, agent.id]);
4952
+ return getAgent(agent.id, d);
5054
4953
  }
5055
- var VALID_SCOPES = ["global", "shared", "private"];
5056
- var VALID_CATEGORIES = [
5057
- "preference",
5058
- "fact",
5059
- "knowledge",
5060
- "history"
5061
- ];
5062
- function isValidScope(value) {
5063
- return VALID_SCOPES.includes(value);
4954
+
4955
+ // src/lib/focus.ts
4956
+ var sessionFocus = new Map;
4957
+ function setFocus(agentId, projectId) {
4958
+ sessionFocus.set(agentId, projectId);
4959
+ updateAgent(agentId, { active_project_id: projectId });
5064
4960
  }
5065
- function isValidCategory(value) {
5066
- return VALID_CATEGORIES.includes(value);
4961
+ function getFocus(agentId) {
4962
+ if (sessionFocus.has(agentId)) {
4963
+ return sessionFocus.get(agentId) ?? null;
4964
+ }
4965
+ const agent = getAgent(agentId);
4966
+ const projectId = agent?.active_project_id ?? null;
4967
+ sessionFocus.set(agentId, projectId);
4968
+ return projectId;
5067
4969
  }
5068
- function loadConfig() {
5069
- const configPath = join2(homedir(), ".mementos", "config.json");
5070
- let fileConfig = {};
5071
- if (existsSync2(configPath)) {
5072
- try {
5073
- const raw = readFileSync(configPath, "utf-8");
5074
- fileConfig = JSON.parse(raw);
5075
- } catch {}
4970
+ function unfocus(agentId) {
4971
+ setFocus(agentId, null);
4972
+ }
4973
+ function resolveProjectId(agentId, explicitProjectId) {
4974
+ if (explicitProjectId !== undefined && explicitProjectId !== null) {
4975
+ return explicitProjectId;
5076
4976
  }
5077
- const merged = deepMerge(DEFAULT_CONFIG, fileConfig);
5078
- const envScope = process.env["MEMENTOS_DEFAULT_SCOPE"];
5079
- if (envScope && isValidScope(envScope)) {
5080
- merged.default_scope = envScope;
4977
+ if (agentId) {
4978
+ return getFocus(agentId);
5081
4979
  }
5082
- const envCategory = process.env["MEMENTOS_DEFAULT_CATEGORY"];
5083
- if (envCategory && isValidCategory(envCategory)) {
5084
- merged.default_category = envCategory;
4980
+ return null;
4981
+ }
4982
+
4983
+ // src/db/locks.ts
4984
+ function parseLockRow(row) {
4985
+ return {
4986
+ id: row["id"],
4987
+ resource_type: row["resource_type"],
4988
+ resource_id: row["resource_id"],
4989
+ agent_id: row["agent_id"],
4990
+ lock_type: row["lock_type"],
4991
+ locked_at: row["locked_at"],
4992
+ expires_at: row["expires_at"]
4993
+ };
4994
+ }
4995
+ function acquireLock(agentId, resourceType, resourceId, lockType = "exclusive", ttlSeconds = 300, db) {
4996
+ const d = db || getDatabase();
4997
+ cleanExpiredLocks(d);
4998
+ 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);
4999
+ if (ownLock) {
5000
+ const newExpiry = new Date(Date.now() + ttlSeconds * 1000).toISOString();
5001
+ d.run("UPDATE resource_locks SET expires_at = ? WHERE id = ?", [
5002
+ newExpiry,
5003
+ ownLock["id"]
5004
+ ]);
5005
+ return parseLockRow({ ...ownLock, expires_at: newExpiry });
5085
5006
  }
5086
- const envImportance = process.env["MEMENTOS_DEFAULT_IMPORTANCE"];
5087
- if (envImportance) {
5088
- const parsed = parseInt(envImportance, 10);
5089
- if (!Number.isNaN(parsed) && parsed >= 1 && parsed <= 10) {
5090
- merged.default_importance = parsed;
5007
+ if (lockType === "exclusive") {
5008
+ 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);
5009
+ if (existing) {
5010
+ return null;
5091
5011
  }
5092
5012
  }
5093
- return merged;
5013
+ const id = shortUuid();
5014
+ const lockedAt = now();
5015
+ const expiresAt = new Date(Date.now() + ttlSeconds * 1000).toISOString();
5016
+ 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]);
5017
+ return {
5018
+ id,
5019
+ resource_type: resourceType,
5020
+ resource_id: resourceId,
5021
+ agent_id: agentId,
5022
+ lock_type: lockType,
5023
+ locked_at: lockedAt,
5024
+ expires_at: expiresAt
5025
+ };
5026
+ }
5027
+ function releaseLock(lockId, agentId, db) {
5028
+ const d = db || getDatabase();
5029
+ const result = d.run("DELETE FROM resource_locks WHERE id = ? AND agent_id = ?", [lockId, agentId]);
5030
+ return result.changes > 0;
5031
+ }
5032
+ function checkLock(resourceType, resourceId, lockType, db) {
5033
+ const d = db || getDatabase();
5034
+ cleanExpiredLocks(d);
5035
+ 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')";
5036
+ const rows = lockType ? d.query(query).all(resourceType, resourceId, lockType) : d.query(query).all(resourceType, resourceId);
5037
+ return rows.map(parseLockRow);
5038
+ }
5039
+ function listAgentLocks(agentId, db) {
5040
+ const d = db || getDatabase();
5041
+ cleanExpiredLocks(d);
5042
+ const rows = d.query("SELECT * FROM resource_locks WHERE agent_id = ? AND expires_at > datetime('now') ORDER BY locked_at DESC").all(agentId);
5043
+ return rows.map(parseLockRow);
5044
+ }
5045
+ function cleanExpiredLocks(db) {
5046
+ const d = db || getDatabase();
5047
+ const result = d.run("DELETE FROM resource_locks WHERE expires_at <= datetime('now')");
5048
+ return result.changes;
5094
5049
  }
5095
5050
 
5096
- // src/db/memories.ts
5097
- function runEntityExtraction(memory, projectId, d) {
5098
- const config = loadConfig();
5099
- if (config.extraction?.enabled === false)
5100
- return;
5101
- const extracted = extractEntities(memory, d);
5102
- const minConfidence = config.extraction?.min_confidence ?? 0.5;
5103
- const entityIds = [];
5104
- for (const ext of extracted) {
5105
- if (ext.confidence >= minConfidence) {
5106
- const entity = createEntity({ name: ext.name, type: ext.type, project_id: projectId }, d);
5107
- linkEntityToMemory(entity.id, memory.id, "context", d);
5108
- entityIds.push(entity.id);
5109
- }
5110
- }
5111
- for (let i = 0;i < entityIds.length; i++) {
5112
- for (let j = i + 1;j < entityIds.length; j++) {
5113
- try {
5114
- createRelation({ source_entity_id: entityIds[i], target_entity_id: entityIds[j], relation_type: "related_to" }, d);
5115
- } catch {}
5116
- }
5117
- }
5051
+ // src/lib/memory-lock.ts
5052
+ var MEMORY_WRITE_TTL = 30;
5053
+ function memoryLockId(key, scope, projectId) {
5054
+ return `${scope}:${key}:${projectId ?? ""}`;
5118
5055
  }
5119
- function parseMemoryRow(row) {
5056
+ function acquireMemoryWriteLock(agentId, key, scope, projectId, ttlSeconds = MEMORY_WRITE_TTL, db) {
5057
+ const d = db || getDatabase();
5058
+ return acquireLock(agentId, "memory", memoryLockId(key, scope, projectId), "exclusive", ttlSeconds, d);
5059
+ }
5060
+ function releaseMemoryWriteLock(lockId, agentId, db) {
5061
+ const d = db || getDatabase();
5062
+ return releaseLock(lockId, agentId, d);
5063
+ }
5064
+ function checkMemoryWriteLock(key, scope, projectId, db) {
5065
+ const d = db || getDatabase();
5066
+ const locks = checkLock("memory", memoryLockId(key, scope, projectId), "exclusive", d);
5067
+ return locks[0] ?? null;
5068
+ }
5069
+
5070
+ // src/db/projects.ts
5071
+ function parseProjectRow(row) {
5120
5072
  return {
5121
5073
  id: row["id"],
5122
- key: row["key"],
5123
- value: row["value"],
5124
- category: row["category"],
5125
- scope: row["scope"],
5126
- summary: row["summary"] || null,
5127
- tags: JSON.parse(row["tags"] || "[]"),
5128
- importance: row["importance"],
5129
- source: row["source"],
5130
- status: row["status"],
5131
- pinned: !!row["pinned"],
5132
- agent_id: row["agent_id"] || null,
5133
- project_id: row["project_id"] || null,
5134
- session_id: row["session_id"] || null,
5135
- metadata: JSON.parse(row["metadata"] || "{}"),
5136
- access_count: row["access_count"],
5137
- version: row["version"],
5138
- expires_at: row["expires_at"] || null,
5074
+ name: row["name"],
5075
+ path: row["path"],
5076
+ description: row["description"] || null,
5077
+ memory_prefix: row["memory_prefix"] || null,
5139
5078
  created_at: row["created_at"],
5140
- updated_at: row["updated_at"],
5141
- accessed_at: row["accessed_at"] || null
5079
+ updated_at: row["updated_at"]
5142
5080
  };
5143
5081
  }
5144
- function createMemory(input, dedupeMode = "merge", db) {
5082
+ function registerProject(name, path, description, memoryPrefix, db) {
5145
5083
  const d = db || getDatabase();
5146
5084
  const timestamp = now();
5147
- let expiresAt = input.expires_at || null;
5148
- if (input.ttl_ms && !expiresAt) {
5149
- expiresAt = new Date(Date.now() + input.ttl_ms).toISOString();
5085
+ const existing = d.query("SELECT * FROM projects WHERE path = ?").get(path);
5086
+ if (existing) {
5087
+ const existingId = existing["id"];
5088
+ d.run("UPDATE projects SET updated_at = ? WHERE id = ?", [
5089
+ timestamp,
5090
+ existingId
5091
+ ]);
5092
+ return parseProjectRow(existing);
5150
5093
  }
5151
5094
  const id = uuid();
5152
- const tags = input.tags || [];
5153
- const tagsJson = JSON.stringify(tags);
5154
- const metadataJson = JSON.stringify(input.metadata || {});
5155
- const safeValue = redactSecrets(input.value);
5156
- const safeSummary = input.summary ? redactSecrets(input.summary) : null;
5157
- if (dedupeMode === "merge") {
5158
- const existing = d.query(`SELECT id, version FROM memories
5159
- WHERE key = ? AND scope = ?
5160
- AND COALESCE(agent_id, '') = ?
5161
- AND COALESCE(project_id, '') = ?
5162
- AND COALESCE(session_id, '') = ?`).get(input.key, input.scope || "private", input.agent_id || "", input.project_id || "", input.session_id || "");
5163
- if (existing) {
5164
- d.run(`UPDATE memories SET
5165
- value = ?, category = ?, summary = ?, tags = ?,
5166
- importance = ?, metadata = ?, expires_at = ?,
5167
- pinned = COALESCE(pinned, 0),
5168
- version = version + 1, updated_at = ?
5169
- WHERE id = ?`, [
5170
- safeValue,
5171
- input.category || "knowledge",
5172
- safeSummary,
5173
- tagsJson,
5174
- input.importance ?? 5,
5175
- metadataJson,
5176
- expiresAt,
5177
- timestamp,
5178
- existing.id
5179
- ]);
5180
- d.run("DELETE FROM memory_tags WHERE memory_id = ?", [existing.id]);
5181
- const insertTag2 = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
5182
- for (const tag of tags) {
5183
- insertTag2.run(existing.id, tag);
5184
- }
5185
- const merged = getMemory(existing.id, d);
5186
- try {
5187
- const oldLinks = getEntityMemoryLinks(undefined, merged.id, d);
5188
- for (const link of oldLinks) {
5189
- unlinkEntityFromMemory(link.entity_id, merged.id, d);
5190
- }
5191
- runEntityExtraction(merged, input.project_id, d);
5192
- } catch {}
5193
- return merged;
5095
+ 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]);
5096
+ return getProject(id, d);
5097
+ }
5098
+ function getProject(idOrPath, db) {
5099
+ const d = db || getDatabase();
5100
+ let row = d.query("SELECT * FROM projects WHERE id = ?").get(idOrPath);
5101
+ if (row)
5102
+ return parseProjectRow(row);
5103
+ row = d.query("SELECT * FROM projects WHERE path = ?").get(idOrPath);
5104
+ if (row)
5105
+ return parseProjectRow(row);
5106
+ row = d.query("SELECT * FROM projects WHERE LOWER(name) = ?").get(idOrPath.toLowerCase());
5107
+ if (row)
5108
+ return parseProjectRow(row);
5109
+ return null;
5110
+ }
5111
+ function listProjects(db) {
5112
+ const d = db || getDatabase();
5113
+ const rows = d.query("SELECT * FROM projects ORDER BY updated_at DESC").all();
5114
+ return rows.map(parseProjectRow);
5115
+ }
5116
+
5117
+ // src/db/entities.ts
5118
+ function parseEntityRow(row) {
5119
+ return {
5120
+ id: row["id"],
5121
+ name: row["name"],
5122
+ type: row["type"],
5123
+ description: row["description"] || null,
5124
+ metadata: JSON.parse(row["metadata"] || "{}"),
5125
+ project_id: row["project_id"] || null,
5126
+ created_at: row["created_at"],
5127
+ updated_at: row["updated_at"]
5128
+ };
5129
+ }
5130
+ function createEntity(input, db) {
5131
+ const d = db || getDatabase();
5132
+ const timestamp = now();
5133
+ const metadataJson = JSON.stringify(input.metadata || {});
5134
+ const existing = d.query(`SELECT * FROM entities
5135
+ WHERE name = ? AND type = ? AND COALESCE(project_id, '') = ?`).get(input.name, input.type, input.project_id || "");
5136
+ if (existing) {
5137
+ const sets = ["updated_at = ?"];
5138
+ const params = [timestamp];
5139
+ if (input.description !== undefined) {
5140
+ sets.push("description = ?");
5141
+ params.push(input.description);
5142
+ }
5143
+ if (input.metadata !== undefined) {
5144
+ sets.push("metadata = ?");
5145
+ params.push(metadataJson);
5194
5146
  }
5147
+ const existingId = existing["id"];
5148
+ params.push(existingId);
5149
+ d.run(`UPDATE entities SET ${sets.join(", ")} WHERE id = ?`, params);
5150
+ return getEntity(existingId, d);
5195
5151
  }
5196
- 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)
5197
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', 0, ?, ?, ?, ?, 0, 1, ?, ?, ?)`, [
5152
+ const id = shortUuid();
5153
+ d.run(`INSERT INTO entities (id, name, type, description, metadata, project_id, created_at, updated_at)
5154
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
5198
5155
  id,
5199
- input.key,
5200
- input.value,
5201
- input.category || "knowledge",
5202
- input.scope || "private",
5203
- input.summary || null,
5204
- tagsJson,
5205
- input.importance ?? 5,
5206
- input.source || "agent",
5207
- input.agent_id || null,
5208
- input.project_id || null,
5209
- input.session_id || null,
5156
+ input.name,
5157
+ input.type,
5158
+ input.description || null,
5210
5159
  metadataJson,
5211
- expiresAt,
5160
+ input.project_id || null,
5212
5161
  timestamp,
5213
5162
  timestamp
5214
5163
  ]);
5215
- const insertTag = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
5216
- for (const tag of tags) {
5217
- insertTag.run(id, tag);
5218
- }
5219
- const memory = getMemory(id, d);
5220
- try {
5221
- runEntityExtraction(memory, input.project_id, d);
5222
- } catch {}
5223
- return memory;
5164
+ return getEntity(id, d);
5224
5165
  }
5225
- function getMemory(id, db) {
5166
+ function getEntity(id, db) {
5226
5167
  const d = db || getDatabase();
5227
- const row = d.query("SELECT * FROM memories WHERE id = ?").get(id);
5168
+ const row = d.query("SELECT * FROM entities WHERE id = ?").get(id);
5228
5169
  if (!row)
5229
- return null;
5230
- return parseMemoryRow(row);
5170
+ throw new EntityNotFoundError(id);
5171
+ return parseEntityRow(row);
5231
5172
  }
5232
- function getMemoryByKey(key, scope, agentId, projectId, sessionId, db) {
5173
+ function getEntityByName(name, type, projectId, db) {
5233
5174
  const d = db || getDatabase();
5234
- let sql = "SELECT * FROM memories WHERE key = ?";
5235
- const params = [key];
5236
- if (scope) {
5237
- sql += " AND scope = ?";
5238
- params.push(scope);
5239
- }
5240
- if (agentId) {
5241
- sql += " AND agent_id = ?";
5242
- params.push(agentId);
5175
+ let sql = "SELECT * FROM entities WHERE name = ?";
5176
+ const params = [name];
5177
+ if (type) {
5178
+ sql += " AND type = ?";
5179
+ params.push(type);
5243
5180
  }
5244
- if (projectId) {
5181
+ if (projectId !== undefined) {
5245
5182
  sql += " AND project_id = ?";
5246
5183
  params.push(projectId);
5247
5184
  }
5248
- if (sessionId) {
5249
- sql += " AND session_id = ?";
5250
- params.push(sessionId);
5251
- }
5252
- sql += " AND status = 'active' ORDER BY importance DESC LIMIT 1";
5185
+ sql += " LIMIT 1";
5253
5186
  const row = d.query(sql).get(...params);
5254
5187
  if (!row)
5255
5188
  return null;
5256
- return parseMemoryRow(row);
5189
+ return parseEntityRow(row);
5257
5190
  }
5258
- function listMemories(filter, db) {
5191
+ function listEntities(filter = {}, db) {
5259
5192
  const d = db || getDatabase();
5260
5193
  const conditions = [];
5261
5194
  const params = [];
5262
- if (filter) {
5263
- if (filter.scope) {
5264
- if (Array.isArray(filter.scope)) {
5265
- conditions.push(`scope IN (${filter.scope.map(() => "?").join(",")})`);
5266
- params.push(...filter.scope);
5267
- } else {
5268
- conditions.push("scope = ?");
5269
- params.push(filter.scope);
5270
- }
5271
- }
5272
- if (filter.category) {
5273
- if (Array.isArray(filter.category)) {
5274
- conditions.push(`category IN (${filter.category.map(() => "?").join(",")})`);
5275
- params.push(...filter.category);
5276
- } else {
5277
- conditions.push("category = ?");
5278
- params.push(filter.category);
5279
- }
5280
- }
5281
- if (filter.source) {
5282
- if (Array.isArray(filter.source)) {
5283
- conditions.push(`source IN (${filter.source.map(() => "?").join(",")})`);
5284
- params.push(...filter.source);
5285
- } else {
5286
- conditions.push("source = ?");
5287
- params.push(filter.source);
5288
- }
5289
- }
5290
- if (filter.status) {
5291
- if (Array.isArray(filter.status)) {
5292
- conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
5293
- params.push(...filter.status);
5294
- } else {
5295
- conditions.push("status = ?");
5296
- params.push(filter.status);
5297
- }
5298
- } else {
5299
- conditions.push("status = 'active'");
5300
- }
5301
- if (filter.project_id) {
5302
- conditions.push("project_id = ?");
5303
- params.push(filter.project_id);
5304
- }
5305
- if (filter.agent_id) {
5306
- conditions.push("agent_id = ?");
5307
- params.push(filter.agent_id);
5308
- }
5309
- if (filter.session_id) {
5310
- conditions.push("session_id = ?");
5311
- params.push(filter.session_id);
5312
- }
5313
- if (filter.min_importance) {
5314
- conditions.push("importance >= ?");
5315
- params.push(filter.min_importance);
5316
- }
5317
- if (filter.pinned !== undefined) {
5318
- conditions.push("pinned = ?");
5319
- params.push(filter.pinned ? 1 : 0);
5320
- }
5321
- if (filter.tags && filter.tags.length > 0) {
5322
- for (const tag of filter.tags) {
5323
- conditions.push("id IN (SELECT memory_id FROM memory_tags WHERE tag = ?)");
5324
- params.push(tag);
5325
- }
5326
- }
5327
- if (filter.search) {
5328
- conditions.push("(key LIKE ? OR value LIKE ? OR summary LIKE ?)");
5329
- const term = `%${filter.search}%`;
5330
- params.push(term, term, term);
5331
- }
5332
- } else {
5333
- conditions.push("status = 'active'");
5195
+ if (filter.type) {
5196
+ conditions.push("type = ?");
5197
+ params.push(filter.type);
5334
5198
  }
5335
- let sql = "SELECT * FROM memories";
5199
+ if (filter.project_id) {
5200
+ conditions.push("project_id = ?");
5201
+ params.push(filter.project_id);
5202
+ }
5203
+ if (filter.search) {
5204
+ conditions.push("(name LIKE ? OR description LIKE ?)");
5205
+ const term = `%${filter.search}%`;
5206
+ params.push(term, term);
5207
+ }
5208
+ let sql = "SELECT * FROM entities";
5336
5209
  if (conditions.length > 0) {
5337
5210
  sql += ` WHERE ${conditions.join(" AND ")}`;
5338
5211
  }
5339
- sql += " ORDER BY importance DESC, created_at DESC";
5340
- if (filter?.limit) {
5212
+ sql += " ORDER BY updated_at DESC";
5213
+ if (filter.limit) {
5341
5214
  sql += " LIMIT ?";
5342
5215
  params.push(filter.limit);
5343
5216
  }
5344
- if (filter?.offset) {
5217
+ if (filter.offset) {
5345
5218
  sql += " OFFSET ?";
5346
5219
  params.push(filter.offset);
5347
5220
  }
5348
5221
  const rows = d.query(sql).all(...params);
5349
- return rows.map(parseMemoryRow);
5222
+ return rows.map(parseEntityRow);
5350
5223
  }
5351
- function updateMemory(id, input, db) {
5224
+ function updateEntity(id, input, db) {
5352
5225
  const d = db || getDatabase();
5353
- const existing = getMemory(id, d);
5354
- if (!existing)
5355
- throw new MemoryNotFoundError(id);
5356
- if (existing.version !== input.version) {
5357
- throw new VersionConflictError(id, input.version, existing.version);
5358
- }
5359
- try {
5360
- d.run(`INSERT OR IGNORE INTO memory_versions (id, memory_id, version, value, importance, scope, category, tags, summary, pinned, status, created_at)
5361
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
5362
- uuid(),
5363
- existing.id,
5364
- existing.version,
5365
- existing.value,
5366
- existing.importance,
5367
- existing.scope,
5368
- existing.category,
5369
- JSON.stringify(existing.tags),
5370
- existing.summary,
5371
- existing.pinned ? 1 : 0,
5372
- existing.status,
5373
- existing.updated_at
5374
- ]);
5375
- } catch {}
5376
- const sets = ["version = version + 1", "updated_at = ?"];
5377
- const params = [now()];
5378
- if (input.value !== undefined) {
5379
- sets.push("value = ?");
5380
- params.push(redactSecrets(input.value));
5381
- }
5382
- if (input.category !== undefined) {
5383
- sets.push("category = ?");
5384
- params.push(input.category);
5385
- }
5386
- if (input.scope !== undefined) {
5387
- sets.push("scope = ?");
5388
- params.push(input.scope);
5389
- }
5390
- if (input.summary !== undefined) {
5391
- sets.push("summary = ?");
5392
- params.push(input.summary);
5393
- }
5394
- if (input.importance !== undefined) {
5395
- sets.push("importance = ?");
5396
- params.push(input.importance);
5226
+ const existing = d.query("SELECT id FROM entities WHERE id = ?").get(id);
5227
+ if (!existing)
5228
+ throw new EntityNotFoundError(id);
5229
+ const sets = ["updated_at = ?"];
5230
+ const params = [now()];
5231
+ if (input.name !== undefined) {
5232
+ sets.push("name = ?");
5233
+ params.push(input.name);
5397
5234
  }
5398
- if (input.pinned !== undefined) {
5399
- sets.push("pinned = ?");
5400
- params.push(input.pinned ? 1 : 0);
5235
+ if (input.type !== undefined) {
5236
+ sets.push("type = ?");
5237
+ params.push(input.type);
5401
5238
  }
5402
- if (input.status !== undefined) {
5403
- sets.push("status = ?");
5404
- params.push(input.status);
5239
+ if (input.description !== undefined) {
5240
+ sets.push("description = ?");
5241
+ params.push(input.description);
5405
5242
  }
5406
5243
  if (input.metadata !== undefined) {
5407
5244
  sets.push("metadata = ?");
5408
5245
  params.push(JSON.stringify(input.metadata));
5409
5246
  }
5410
- if (input.expires_at !== undefined) {
5411
- sets.push("expires_at = ?");
5412
- params.push(input.expires_at);
5413
- }
5414
- if (input.tags !== undefined) {
5415
- sets.push("tags = ?");
5416
- params.push(JSON.stringify(input.tags));
5417
- d.run("DELETE FROM memory_tags WHERE memory_id = ?", [id]);
5418
- const insertTag = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
5419
- for (const tag of input.tags) {
5420
- insertTag.run(id, tag);
5421
- }
5422
- }
5423
5247
  params.push(id);
5424
- d.run(`UPDATE memories SET ${sets.join(", ")} WHERE id = ?`, params);
5425
- const updated = getMemory(id, d);
5426
- try {
5427
- if (input.value !== undefined) {
5428
- const oldLinks = getEntityMemoryLinks(undefined, updated.id, d);
5429
- for (const link of oldLinks) {
5430
- unlinkEntityFromMemory(link.entity_id, updated.id, d);
5431
- }
5432
- runEntityExtraction(updated, existing.project_id || undefined, d);
5433
- }
5434
- } catch {}
5435
- return updated;
5436
- }
5437
- function deleteMemory(id, db) {
5438
- const d = db || getDatabase();
5439
- const result = d.run("DELETE FROM memories WHERE id = ?", [id]);
5440
- return result.changes > 0;
5441
- }
5442
- function bulkDeleteMemories(ids, db) {
5443
- const d = db || getDatabase();
5444
- if (ids.length === 0)
5445
- return 0;
5446
- const placeholders = ids.map(() => "?").join(",");
5447
- const countRow = d.query(`SELECT COUNT(*) as c FROM memories WHERE id IN (${placeholders})`).get(...ids);
5448
- const count = countRow.c;
5449
- if (count > 0) {
5450
- d.run(`DELETE FROM memories WHERE id IN (${placeholders})`, ids);
5451
- }
5452
- return count;
5453
- }
5454
- function touchMemory(id, db) {
5455
- const d = db || getDatabase();
5456
- d.run("UPDATE memories SET access_count = access_count + 1, accessed_at = ? WHERE id = ?", [now(), id]);
5248
+ d.run(`UPDATE entities SET ${sets.join(", ")} WHERE id = ?`, params);
5249
+ return getEntity(id, d);
5457
5250
  }
5458
- function cleanExpiredMemories(db) {
5251
+ function deleteEntity(id, db) {
5459
5252
  const d = db || getDatabase();
5460
- const timestamp = now();
5461
- const countRow = d.query("SELECT COUNT(*) as c FROM memories WHERE expires_at IS NOT NULL AND expires_at < ?").get(timestamp);
5462
- const count = countRow.c;
5463
- if (count > 0) {
5464
- d.run("DELETE FROM memories WHERE expires_at IS NOT NULL AND expires_at < ?", [timestamp]);
5465
- }
5466
- return count;
5253
+ const result = d.run("DELETE FROM entities WHERE id = ?", [id]);
5254
+ if (result.changes === 0)
5255
+ throw new EntityNotFoundError(id);
5467
5256
  }
5468
- function getMemoryVersions(memoryId, db) {
5257
+ function mergeEntities(sourceId, targetId, db) {
5469
5258
  const d = db || getDatabase();
5470
- try {
5471
- const rows = d.query("SELECT * FROM memory_versions WHERE memory_id = ? ORDER BY version ASC").all(memoryId);
5472
- return rows.map((row) => ({
5473
- id: row["id"],
5474
- memory_id: row["memory_id"],
5475
- version: row["version"],
5476
- value: row["value"],
5477
- importance: row["importance"],
5478
- scope: row["scope"],
5479
- category: row["category"],
5480
- tags: JSON.parse(row["tags"] || "[]"),
5481
- summary: row["summary"] || null,
5482
- pinned: !!row["pinned"],
5483
- status: row["status"],
5484
- created_at: row["created_at"]
5485
- }));
5486
- } catch {
5487
- return [];
5488
- }
5259
+ getEntity(sourceId, d);
5260
+ getEntity(targetId, d);
5261
+ d.run(`UPDATE OR IGNORE relations SET source_entity_id = ? WHERE source_entity_id = ?`, [targetId, sourceId]);
5262
+ d.run(`UPDATE OR IGNORE relations SET target_entity_id = ? WHERE target_entity_id = ?`, [targetId, sourceId]);
5263
+ d.run("DELETE FROM relations WHERE source_entity_id = ? OR target_entity_id = ?", [
5264
+ sourceId,
5265
+ sourceId
5266
+ ]);
5267
+ d.run(`UPDATE OR IGNORE entity_memories SET entity_id = ? WHERE entity_id = ?`, [targetId, sourceId]);
5268
+ d.run("DELETE FROM entity_memories WHERE entity_id = ?", [sourceId]);
5269
+ d.run("DELETE FROM entities WHERE id = ?", [sourceId]);
5270
+ d.run("UPDATE entities SET updated_at = ? WHERE id = ?", [now(), targetId]);
5271
+ return getEntity(targetId, d);
5489
5272
  }
5490
5273
 
5491
- // src/db/locks.ts
5492
- function parseLockRow(row) {
5274
+ // src/db/relations.ts
5275
+ function parseRelationRow(row) {
5493
5276
  return {
5494
5277
  id: row["id"],
5495
- resource_type: row["resource_type"],
5496
- resource_id: row["resource_id"],
5497
- agent_id: row["agent_id"],
5498
- lock_type: row["lock_type"],
5499
- locked_at: row["locked_at"],
5500
- expires_at: row["expires_at"]
5278
+ source_entity_id: row["source_entity_id"],
5279
+ target_entity_id: row["target_entity_id"],
5280
+ relation_type: row["relation_type"],
5281
+ weight: row["weight"],
5282
+ metadata: JSON.parse(row["metadata"] || "{}"),
5283
+ created_at: row["created_at"]
5501
5284
  };
5502
5285
  }
5503
- function acquireLock(agentId, resourceType, resourceId, lockType = "exclusive", ttlSeconds = 300, db) {
5504
- const d = db || getDatabase();
5505
- cleanExpiredLocks(d);
5506
- 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);
5507
- if (ownLock) {
5508
- const newExpiry = new Date(Date.now() + ttlSeconds * 1000).toISOString();
5509
- d.run("UPDATE resource_locks SET expires_at = ? WHERE id = ?", [
5510
- newExpiry,
5511
- ownLock["id"]
5512
- ]);
5513
- return parseLockRow({ ...ownLock, expires_at: newExpiry });
5514
- }
5515
- if (lockType === "exclusive") {
5516
- 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);
5517
- if (existing) {
5518
- return null;
5519
- }
5520
- }
5521
- const id = shortUuid();
5522
- const lockedAt = now();
5523
- const expiresAt = new Date(Date.now() + ttlSeconds * 1000).toISOString();
5524
- 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]);
5286
+ function parseEntityRow2(row) {
5525
5287
  return {
5526
- id,
5527
- resource_type: resourceType,
5528
- resource_id: resourceId,
5529
- agent_id: agentId,
5530
- lock_type: lockType,
5531
- locked_at: lockedAt,
5532
- expires_at: expiresAt
5288
+ id: row["id"],
5289
+ name: row["name"],
5290
+ type: row["type"],
5291
+ description: row["description"] || null,
5292
+ metadata: JSON.parse(row["metadata"] || "{}"),
5293
+ project_id: row["project_id"] || null,
5294
+ created_at: row["created_at"],
5295
+ updated_at: row["updated_at"]
5533
5296
  };
5534
5297
  }
5535
- function releaseLock(lockId, agentId, db) {
5536
- const d = db || getDatabase();
5537
- const result = d.run("DELETE FROM resource_locks WHERE id = ? AND agent_id = ?", [lockId, agentId]);
5538
- return result.changes > 0;
5539
- }
5540
- function checkLock(resourceType, resourceId, lockType, db) {
5298
+ function createRelation(input, db) {
5541
5299
  const d = db || getDatabase();
5542
- cleanExpiredLocks(d);
5543
- 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')";
5544
- const rows = lockType ? d.query(query).all(resourceType, resourceId, lockType) : d.query(query).all(resourceType, resourceId);
5545
- return rows.map(parseLockRow);
5300
+ const id = shortUuid();
5301
+ const timestamp = now();
5302
+ const weight = input.weight ?? 1;
5303
+ const metadata = JSON.stringify(input.metadata ?? {});
5304
+ d.run(`INSERT INTO relations (id, source_entity_id, target_entity_id, relation_type, weight, metadata, created_at)
5305
+ VALUES (?, ?, ?, ?, ?, ?, ?)
5306
+ ON CONFLICT(source_entity_id, target_entity_id, relation_type)
5307
+ DO UPDATE SET weight = excluded.weight, metadata = excluded.metadata`, [id, input.source_entity_id, input.target_entity_id, input.relation_type, weight, metadata, timestamp]);
5308
+ const row = d.query(`SELECT * FROM relations
5309
+ WHERE source_entity_id = ? AND target_entity_id = ? AND relation_type = ?`).get(input.source_entity_id, input.target_entity_id, input.relation_type);
5310
+ return parseRelationRow(row);
5546
5311
  }
5547
- function listAgentLocks(agentId, db) {
5312
+ function getRelation(id, db) {
5548
5313
  const d = db || getDatabase();
5549
- cleanExpiredLocks(d);
5550
- const rows = d.query("SELECT * FROM resource_locks WHERE agent_id = ? AND expires_at > datetime('now') ORDER BY locked_at DESC").all(agentId);
5551
- return rows.map(parseLockRow);
5314
+ const row = d.query("SELECT * FROM relations WHERE id = ?").get(id);
5315
+ if (!row)
5316
+ throw new Error(`Relation not found: ${id}`);
5317
+ return parseRelationRow(row);
5552
5318
  }
5553
- function cleanExpiredLocks(db) {
5319
+ function listRelations(filter, db) {
5554
5320
  const d = db || getDatabase();
5555
- const result = d.run("DELETE FROM resource_locks WHERE expires_at <= datetime('now')");
5556
- return result.changes;
5557
- }
5558
-
5559
- // src/lib/memory-lock.ts
5560
- var MEMORY_WRITE_TTL = 30;
5561
- function memoryLockId(key, scope, projectId) {
5562
- return `${scope}:${key}:${projectId ?? ""}`;
5321
+ const conditions = [];
5322
+ const params = [];
5323
+ if (filter.entity_id) {
5324
+ const dir = filter.direction || "both";
5325
+ if (dir === "outgoing") {
5326
+ conditions.push("source_entity_id = ?");
5327
+ params.push(filter.entity_id);
5328
+ } else if (dir === "incoming") {
5329
+ conditions.push("target_entity_id = ?");
5330
+ params.push(filter.entity_id);
5331
+ } else {
5332
+ conditions.push("(source_entity_id = ? OR target_entity_id = ?)");
5333
+ params.push(filter.entity_id, filter.entity_id);
5334
+ }
5335
+ }
5336
+ if (filter.relation_type) {
5337
+ conditions.push("relation_type = ?");
5338
+ params.push(filter.relation_type);
5339
+ }
5340
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
5341
+ const rows = d.query(`SELECT * FROM relations ${where} ORDER BY created_at DESC`).all(...params);
5342
+ return rows.map(parseRelationRow);
5563
5343
  }
5564
- function acquireMemoryWriteLock(agentId, key, scope, projectId, ttlSeconds = MEMORY_WRITE_TTL, db) {
5344
+ function deleteRelation(id, db) {
5565
5345
  const d = db || getDatabase();
5566
- return acquireLock(agentId, "memory", memoryLockId(key, scope, projectId), "exclusive", ttlSeconds, d);
5346
+ const result = d.run("DELETE FROM relations WHERE id = ?", [id]);
5347
+ if (result.changes === 0)
5348
+ throw new Error(`Relation not found: ${id}`);
5567
5349
  }
5568
- function releaseMemoryWriteLock(lockId, agentId, db) {
5350
+ function getEntityGraph(entityId, depth = 2, db) {
5569
5351
  const d = db || getDatabase();
5570
- return releaseLock(lockId, agentId, d);
5352
+ const entityRows = d.query(`WITH RECURSIVE graph(id, depth) AS (
5353
+ VALUES(?, 0)
5354
+ UNION
5355
+ SELECT CASE WHEN r.source_entity_id = g.id THEN r.target_entity_id ELSE r.source_entity_id END, g.depth + 1
5356
+ FROM relations r JOIN graph g ON (r.source_entity_id = g.id OR r.target_entity_id = g.id)
5357
+ WHERE g.depth < ?
5358
+ )
5359
+ SELECT DISTINCT e.* FROM entities e JOIN graph g ON e.id = g.id`).all(entityId, depth);
5360
+ const entities = entityRows.map(parseEntityRow2);
5361
+ const entityIds = new Set(entities.map((e) => e.id));
5362
+ if (entityIds.size === 0) {
5363
+ return { entities: [], relations: [] };
5364
+ }
5365
+ const placeholders = Array.from(entityIds).map(() => "?").join(",");
5366
+ const relationRows = d.query(`SELECT * FROM relations
5367
+ WHERE source_entity_id IN (${placeholders})
5368
+ AND target_entity_id IN (${placeholders})`).all(...Array.from(entityIds), ...Array.from(entityIds));
5369
+ const relations = relationRows.map(parseRelationRow);
5370
+ return { entities, relations };
5571
5371
  }
5572
- function checkMemoryWriteLock(key, scope, projectId, db) {
5372
+ function findPath(fromEntityId, toEntityId, maxDepth = 5, db) {
5573
5373
  const d = db || getDatabase();
5574
- const locks = checkLock("memory", memoryLockId(key, scope, projectId), "exclusive", d);
5575
- return locks[0] ?? null;
5374
+ const rows = d.query(`WITH RECURSIVE path(id, trail, depth) AS (
5375
+ SELECT ?, ?, 0
5376
+ UNION
5377
+ SELECT
5378
+ CASE WHEN r.source_entity_id = p.id THEN r.target_entity_id ELSE r.source_entity_id END,
5379
+ p.trail || ',' || CASE WHEN r.source_entity_id = p.id THEN r.target_entity_id ELSE r.source_entity_id END,
5380
+ p.depth + 1
5381
+ FROM relations r JOIN path p ON (r.source_entity_id = p.id OR r.target_entity_id = p.id)
5382
+ WHERE p.depth < ?
5383
+ AND INSTR(p.trail, CASE WHEN r.source_entity_id = p.id THEN r.target_entity_id ELSE r.source_entity_id END) = 0
5384
+ )
5385
+ SELECT trail FROM path WHERE id = ? ORDER BY depth ASC LIMIT 1`).get(fromEntityId, fromEntityId, maxDepth, toEntityId);
5386
+ if (!rows)
5387
+ return null;
5388
+ const ids = rows.trail.split(",");
5389
+ const entities = [];
5390
+ for (const id of ids) {
5391
+ const row = d.query("SELECT * FROM entities WHERE id = ?").get(id);
5392
+ if (row)
5393
+ entities.push(parseEntityRow2(row));
5394
+ }
5395
+ return entities.length > 0 ? entities : null;
5576
5396
  }
5577
5397
 
5578
5398
  // src/lib/search.ts
@@ -6056,143 +5876,835 @@ function scoreResults(rows, queryLower, graphBoostedIds) {
6056
5876
  highlights: extractHighlights(memory, queryLower)
6057
5877
  });
6058
5878
  }
6059
- scored.sort((a, b) => {
6060
- if (b.score !== a.score)
6061
- return b.score - a.score;
6062
- return b.memory.importance - a.memory.importance;
5879
+ scored.sort((a, b) => {
5880
+ if (b.score !== a.score)
5881
+ return b.score - a.score;
5882
+ return b.memory.importance - a.memory.importance;
5883
+ });
5884
+ return scored;
5885
+ }
5886
+ function searchMemories(query, filter, db) {
5887
+ const d = db || getDatabase();
5888
+ query = preprocessQuery(query);
5889
+ if (!query)
5890
+ return [];
5891
+ const queryLower = query.toLowerCase();
5892
+ const graphBoostedIds = getGraphBoostedMemoryIds(query, d);
5893
+ let scored;
5894
+ if (hasFts5Table(d)) {
5895
+ const ftsResult = searchWithFts5(d, query, queryLower, filter, graphBoostedIds);
5896
+ if (ftsResult !== null) {
5897
+ scored = ftsResult;
5898
+ } else {
5899
+ scored = searchWithLike(d, query, queryLower, filter, graphBoostedIds);
5900
+ }
5901
+ } else {
5902
+ scored = searchWithLike(d, query, queryLower, filter, graphBoostedIds);
5903
+ }
5904
+ if (scored.length < 3) {
5905
+ const fuzzyResults = searchWithFuzzy(d, query, filter, graphBoostedIds);
5906
+ const seenIds = new Set(scored.map((r) => r.memory.id));
5907
+ for (const fr of fuzzyResults) {
5908
+ if (!seenIds.has(fr.memory.id)) {
5909
+ scored.push(fr);
5910
+ seenIds.add(fr.memory.id);
5911
+ }
5912
+ }
5913
+ scored.sort((a, b) => {
5914
+ if (b.score !== a.score)
5915
+ return b.score - a.score;
5916
+ return b.memory.importance - a.memory.importance;
5917
+ });
5918
+ }
5919
+ const offset = filter?.offset ?? 0;
5920
+ const limit = filter?.limit ?? scored.length;
5921
+ const finalResults = scored.slice(offset, offset + limit);
5922
+ if (finalResults.length > 0 && scored.length > 0) {
5923
+ const topScore = scored[0]?.score ?? 0;
5924
+ const secondScore = scored[1]?.score ?? 0;
5925
+ const confidence = topScore > 0 ? Math.max(0, Math.min(1, (topScore - secondScore) / topScore)) : 0;
5926
+ finalResults[0] = { ...finalResults[0], confidence };
5927
+ }
5928
+ logSearchQuery(query, scored.length, filter?.agent_id, filter?.project_id, d);
5929
+ return finalResults;
5930
+ }
5931
+ function logSearchQuery(query, resultCount, agentId, projectId, db) {
5932
+ try {
5933
+ const d = db || getDatabase();
5934
+ const id = crypto.randomUUID().slice(0, 8);
5935
+ d.run("INSERT INTO search_history (id, query, result_count, agent_id, project_id) VALUES (?, ?, ?, ?, ?)", [id, query, resultCount, agentId || null, projectId || null]);
5936
+ } catch {}
5937
+ }
5938
+
5939
+ // src/lib/project-detect.ts
5940
+ import { existsSync as existsSync2 } from "fs";
5941
+ import { basename, dirname as dirname2, join as join2, resolve as resolve2 } from "path";
5942
+ function findGitRoot2(startDir) {
5943
+ let dir = resolve2(startDir);
5944
+ while (true) {
5945
+ if (existsSync2(join2(dir, ".git")))
5946
+ return dir;
5947
+ const parent = dirname2(dir);
5948
+ if (parent === dir)
5949
+ break;
5950
+ dir = parent;
5951
+ }
5952
+ return null;
5953
+ }
5954
+ var _cachedProject = undefined;
5955
+ function detectProject(db) {
5956
+ if (_cachedProject !== undefined)
5957
+ return _cachedProject;
5958
+ const d = db || getDatabase();
5959
+ const cwd = process.cwd();
5960
+ const gitRoot = findGitRoot2(cwd);
5961
+ if (!gitRoot) {
5962
+ _cachedProject = null;
5963
+ return null;
5964
+ }
5965
+ const repoName = basename(gitRoot);
5966
+ const absPath = resolve2(gitRoot);
5967
+ const existing = getProject(absPath, d);
5968
+ if (existing) {
5969
+ _cachedProject = existing;
5970
+ return existing;
5971
+ }
5972
+ const project = registerProject(repoName, absPath, undefined, undefined, d);
5973
+ _cachedProject = project;
5974
+ return project;
5975
+ }
5976
+
5977
+ // src/lib/duration.ts
5978
+ var UNIT_MS = {
5979
+ s: 1000,
5980
+ m: 60000,
5981
+ h: 3600000,
5982
+ d: 86400000,
5983
+ w: 604800000
5984
+ };
5985
+ var DURATION_RE = /^(\d+[smhdw])+$/;
5986
+ var SEGMENT_RE = /(\d+)([smhdw])/g;
5987
+ function parseDuration(input) {
5988
+ if (typeof input === "number")
5989
+ return input;
5990
+ const trimmed = input.trim();
5991
+ if (trimmed === "")
5992
+ throw new Error("Invalid duration: empty string");
5993
+ if (/^\d+$/.test(trimmed)) {
5994
+ return parseInt(trimmed, 10);
5995
+ }
5996
+ if (!DURATION_RE.test(trimmed)) {
5997
+ throw new Error(`Invalid duration format: "${trimmed}". Use combinations of Ns, Nm, Nh, Nd, Nw (e.g. "1d12h", "30m") or plain milliseconds.`);
5998
+ }
5999
+ let total = 0;
6000
+ let match;
6001
+ SEGMENT_RE.lastIndex = 0;
6002
+ while ((match = SEGMENT_RE.exec(trimmed)) !== null) {
6003
+ const value = parseInt(match[1], 10);
6004
+ const unit = match[2];
6005
+ total += value * UNIT_MS[unit];
6006
+ }
6007
+ if (total === 0) {
6008
+ throw new Error(`Invalid duration: "${trimmed}" resolves to 0ms`);
6009
+ }
6010
+ return total;
6011
+ }
6012
+ var FORMAT_UNITS = [
6013
+ ["w", UNIT_MS["w"]],
6014
+ ["d", UNIT_MS["d"]],
6015
+ ["h", UNIT_MS["h"]],
6016
+ ["m", UNIT_MS["m"]],
6017
+ ["s", UNIT_MS["s"]]
6018
+ ];
6019
+
6020
+ // src/mcp/index.ts
6021
+ import { createRequire } from "module";
6022
+
6023
+ // src/lib/providers/base.ts
6024
+ var DEFAULT_AUTO_MEMORY_CONFIG = {
6025
+ provider: "anthropic",
6026
+ model: "claude-haiku-4-5",
6027
+ enabled: true,
6028
+ minImportance: 4,
6029
+ autoEntityLink: true,
6030
+ fallback: ["cerebras", "openai"]
6031
+ };
6032
+
6033
+ class BaseProvider {
6034
+ config;
6035
+ constructor(config) {
6036
+ this.config = config;
6037
+ }
6038
+ parseJSON(raw) {
6039
+ try {
6040
+ const cleaned = raw.replace(/^```(?:json)?\s*/m, "").replace(/\s*```$/m, "").trim();
6041
+ return JSON.parse(cleaned);
6042
+ } catch {
6043
+ return null;
6044
+ }
6045
+ }
6046
+ clampImportance(value) {
6047
+ const n = Number(value);
6048
+ if (isNaN(n))
6049
+ return 5;
6050
+ return Math.max(0, Math.min(10, Math.round(n)));
6051
+ }
6052
+ normaliseMemory(raw) {
6053
+ if (!raw || typeof raw !== "object")
6054
+ return null;
6055
+ const m = raw;
6056
+ if (typeof m.content !== "string" || !m.content.trim())
6057
+ return null;
6058
+ const validScopes = ["private", "shared", "global"];
6059
+ const validCategories = [
6060
+ "preference",
6061
+ "fact",
6062
+ "knowledge",
6063
+ "history"
6064
+ ];
6065
+ return {
6066
+ content: m.content.trim(),
6067
+ category: validCategories.includes(m.category) ? m.category : "knowledge",
6068
+ importance: this.clampImportance(m.importance),
6069
+ tags: Array.isArray(m.tags) ? m.tags.filter((t) => typeof t === "string").map((t) => t.toLowerCase()) : [],
6070
+ suggestedScope: validScopes.includes(m.suggestedScope) ? m.suggestedScope : "shared",
6071
+ reasoning: typeof m.reasoning === "string" ? m.reasoning : undefined
6072
+ };
6073
+ }
6074
+ }
6075
+ var MEMORY_EXTRACTION_SYSTEM_PROMPT = `You are a precise memory extraction engine for an AI agent.
6076
+ Given text, extract facts worth remembering as structured JSON.
6077
+ Focus on: decisions made, preferences revealed, corrections, architectural choices, established facts, user preferences.
6078
+ Ignore: greetings, filler, questions without answers, temporary states.
6079
+ Output ONLY a JSON array \u2014 no markdown, no explanation.`;
6080
+ var MEMORY_EXTRACTION_USER_TEMPLATE = (text, context) => `Extract memories from this text.
6081
+ ${context.projectName ? `Project: ${context.projectName}` : ""}
6082
+ ${context.existingMemoriesSummary ? `Existing memories (avoid duplicates):
6083
+ ${context.existingMemoriesSummary}` : ""}
6084
+
6085
+ Text:
6086
+ ${text}
6087
+
6088
+ Return a JSON array of objects with these exact fields:
6089
+ - content: string (the memory, concise and specific)
6090
+ - category: "preference" | "fact" | "knowledge" | "history"
6091
+ - importance: number 0-10 (10 = critical, 0 = trivial)
6092
+ - tags: string[] (lowercase keywords)
6093
+ - suggestedScope: "private" | "shared" | "global"
6094
+ - reasoning: string (one sentence why this is worth remembering)
6095
+
6096
+ Return [] if nothing is worth remembering.`;
6097
+ var ENTITY_EXTRACTION_SYSTEM_PROMPT = `You are a knowledge graph entity extractor.
6098
+ Given text, identify named entities and their relationships.
6099
+ Output ONLY valid JSON \u2014 no markdown, no explanation.`;
6100
+ var ENTITY_EXTRACTION_USER_TEMPLATE = (text) => `Extract entities and relations from this text.
6101
+
6102
+ Text: ${text}
6103
+
6104
+ Return JSON with this exact shape:
6105
+ {
6106
+ "entities": [
6107
+ { "name": string, "type": "person"|"project"|"tool"|"concept"|"file"|"api"|"pattern"|"organization", "confidence": 0-1 }
6108
+ ],
6109
+ "relations": [
6110
+ { "from": string, "to": string, "type": "uses"|"knows"|"depends_on"|"created_by"|"related_to"|"contradicts"|"part_of"|"implements" }
6111
+ ]
6112
+ }`;
6113
+
6114
+ // src/lib/providers/anthropic.ts
6115
+ var ANTHROPIC_MODELS = {
6116
+ default: "claude-haiku-4-5",
6117
+ premium: "claude-sonnet-4-5"
6118
+ };
6119
+
6120
+ class AnthropicProvider extends BaseProvider {
6121
+ name = "anthropic";
6122
+ baseUrl = "https://api.anthropic.com/v1";
6123
+ constructor(config) {
6124
+ const apiKey = config?.apiKey ?? process.env.ANTHROPIC_API_KEY ?? "";
6125
+ super({
6126
+ apiKey,
6127
+ model: config?.model ?? ANTHROPIC_MODELS.default,
6128
+ maxTokens: config?.maxTokens ?? 1024,
6129
+ temperature: config?.temperature ?? 0,
6130
+ timeoutMs: config?.timeoutMs ?? 15000
6131
+ });
6132
+ }
6133
+ async extractMemories(text, context) {
6134
+ if (!this.config.apiKey)
6135
+ return [];
6136
+ try {
6137
+ const response = await this.callAPI(MEMORY_EXTRACTION_SYSTEM_PROMPT, MEMORY_EXTRACTION_USER_TEMPLATE(text, context));
6138
+ const parsed = this.parseJSON(response);
6139
+ if (!Array.isArray(parsed))
6140
+ return [];
6141
+ return parsed.map((item) => this.normaliseMemory(item)).filter((m) => m !== null);
6142
+ } catch (err) {
6143
+ console.error("[anthropic] extractMemories failed:", err);
6144
+ return [];
6145
+ }
6146
+ }
6147
+ async extractEntities(text) {
6148
+ const empty = { entities: [], relations: [] };
6149
+ if (!this.config.apiKey)
6150
+ return empty;
6151
+ try {
6152
+ const response = await this.callAPI(ENTITY_EXTRACTION_SYSTEM_PROMPT, ENTITY_EXTRACTION_USER_TEMPLATE(text));
6153
+ const parsed = this.parseJSON(response);
6154
+ if (!parsed || typeof parsed !== "object")
6155
+ return empty;
6156
+ return {
6157
+ entities: Array.isArray(parsed.entities) ? parsed.entities : [],
6158
+ relations: Array.isArray(parsed.relations) ? parsed.relations : []
6159
+ };
6160
+ } catch (err) {
6161
+ console.error("[anthropic] extractEntities failed:", err);
6162
+ return empty;
6163
+ }
6164
+ }
6165
+ async scoreImportance(content, _context) {
6166
+ if (!this.config.apiKey)
6167
+ return 5;
6168
+ try {
6169
+ 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?
6170
+
6171
+ "${content}"
6172
+
6173
+ Return only a number 0-10.`);
6174
+ return this.clampImportance(response.trim());
6175
+ } catch {
6176
+ return 5;
6177
+ }
6178
+ }
6179
+ async callAPI(systemPrompt, userMessage) {
6180
+ const controller = new AbortController;
6181
+ const timeout = setTimeout(() => controller.abort(), this.config.timeoutMs ?? 15000);
6182
+ try {
6183
+ const res = await fetch(`${this.baseUrl}/messages`, {
6184
+ method: "POST",
6185
+ headers: {
6186
+ "Content-Type": "application/json",
6187
+ "x-api-key": this.config.apiKey,
6188
+ "anthropic-version": "2023-06-01"
6189
+ },
6190
+ body: JSON.stringify({
6191
+ model: this.config.model,
6192
+ max_tokens: this.config.maxTokens ?? 1024,
6193
+ temperature: this.config.temperature ?? 0,
6194
+ system: systemPrompt,
6195
+ messages: [{ role: "user", content: userMessage }]
6196
+ }),
6197
+ signal: controller.signal
6198
+ });
6199
+ if (!res.ok) {
6200
+ const body = await res.text().catch(() => "");
6201
+ throw new Error(`Anthropic API ${res.status}: ${body.slice(0, 200)}`);
6202
+ }
6203
+ const data = await res.json();
6204
+ return data.content?.[0]?.text ?? "";
6205
+ } finally {
6206
+ clearTimeout(timeout);
6207
+ }
6208
+ }
6209
+ }
6210
+
6211
+ // src/lib/providers/openai-compat.ts
6212
+ class OpenAICompatProvider extends BaseProvider {
6213
+ constructor(config) {
6214
+ super(config);
6215
+ }
6216
+ async extractMemories(text, context) {
6217
+ if (!this.config.apiKey)
6218
+ return [];
6219
+ try {
6220
+ const response = await this.callWithRetry(MEMORY_EXTRACTION_SYSTEM_PROMPT, MEMORY_EXTRACTION_USER_TEMPLATE(text, context));
6221
+ const parsed = this.parseJSON(response);
6222
+ if (!Array.isArray(parsed))
6223
+ return [];
6224
+ return parsed.map((item) => this.normaliseMemory(item)).filter((m) => m !== null);
6225
+ } catch (err) {
6226
+ console.error(`[${this.name}] extractMemories failed:`, err);
6227
+ return [];
6228
+ }
6229
+ }
6230
+ async extractEntities(text) {
6231
+ const empty = { entities: [], relations: [] };
6232
+ if (!this.config.apiKey)
6233
+ return empty;
6234
+ try {
6235
+ const response = await this.callWithRetry(ENTITY_EXTRACTION_SYSTEM_PROMPT, ENTITY_EXTRACTION_USER_TEMPLATE(text));
6236
+ const parsed = this.parseJSON(response);
6237
+ if (!parsed || typeof parsed !== "object")
6238
+ return empty;
6239
+ return {
6240
+ entities: Array.isArray(parsed.entities) ? parsed.entities : [],
6241
+ relations: Array.isArray(parsed.relations) ? parsed.relations : []
6242
+ };
6243
+ } catch (err) {
6244
+ console.error(`[${this.name}] extractEntities failed:`, err);
6245
+ return empty;
6246
+ }
6247
+ }
6248
+ async scoreImportance(content, _context) {
6249
+ if (!this.config.apiKey)
6250
+ return 5;
6251
+ try {
6252
+ 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?
6253
+
6254
+ "${content}"
6255
+
6256
+ Return only a number 0-10.`);
6257
+ return this.clampImportance(response.trim());
6258
+ } catch {
6259
+ return 5;
6260
+ }
6261
+ }
6262
+ async callWithRetry(systemPrompt, userMessage, retries = 3) {
6263
+ let lastError = null;
6264
+ for (let attempt = 0;attempt < retries; attempt++) {
6265
+ try {
6266
+ return await this.callAPI(systemPrompt, userMessage);
6267
+ } catch (err) {
6268
+ lastError = err instanceof Error ? err : new Error(String(err));
6269
+ const isRateLimit = lastError.message.includes("429") || lastError.message.toLowerCase().includes("rate limit");
6270
+ if (!isRateLimit || attempt === retries - 1)
6271
+ throw lastError;
6272
+ await new Promise((r) => setTimeout(r, 1000 * Math.pow(2, attempt)));
6273
+ }
6274
+ }
6275
+ throw lastError ?? new Error("Unknown error");
6276
+ }
6277
+ async callAPI(systemPrompt, userMessage) {
6278
+ const controller = new AbortController;
6279
+ const timeout = setTimeout(() => controller.abort(), this.config.timeoutMs ?? 15000);
6280
+ try {
6281
+ const res = await fetch(`${this.baseUrl}/chat/completions`, {
6282
+ method: "POST",
6283
+ headers: {
6284
+ "Content-Type": "application/json",
6285
+ [this.authHeader]: `Bearer ${this.config.apiKey}`
6286
+ },
6287
+ body: JSON.stringify({
6288
+ model: this.config.model,
6289
+ max_tokens: this.config.maxTokens ?? 1024,
6290
+ temperature: this.config.temperature ?? 0,
6291
+ messages: [
6292
+ { role: "system", content: systemPrompt },
6293
+ { role: "user", content: userMessage }
6294
+ ]
6295
+ }),
6296
+ signal: controller.signal
6297
+ });
6298
+ if (!res.ok) {
6299
+ const body = await res.text().catch(() => "");
6300
+ throw new Error(`${this.name} API ${res.status}: ${body.slice(0, 200)}`);
6301
+ }
6302
+ const data = await res.json();
6303
+ return data.choices?.[0]?.message?.content ?? "";
6304
+ } finally {
6305
+ clearTimeout(timeout);
6306
+ }
6307
+ }
6308
+ }
6309
+
6310
+ // src/lib/providers/openai.ts
6311
+ var OPENAI_MODELS = {
6312
+ default: "gpt-4.1-nano",
6313
+ mini: "gpt-4.1-mini",
6314
+ full: "gpt-4.1"
6315
+ };
6316
+
6317
+ class OpenAIProvider extends OpenAICompatProvider {
6318
+ name = "openai";
6319
+ baseUrl = "https://api.openai.com/v1";
6320
+ authHeader = "Authorization";
6321
+ constructor(config) {
6322
+ super({
6323
+ apiKey: config?.apiKey ?? process.env.OPENAI_API_KEY ?? "",
6324
+ model: config?.model ?? OPENAI_MODELS.default,
6325
+ maxTokens: config?.maxTokens ?? 1024,
6326
+ temperature: config?.temperature ?? 0,
6327
+ timeoutMs: config?.timeoutMs ?? 15000
6328
+ });
6329
+ }
6330
+ }
6331
+
6332
+ // src/lib/providers/cerebras.ts
6333
+ var CEREBRAS_MODELS = {
6334
+ default: "llama-3.3-70b",
6335
+ fast: "llama3.1-8b"
6336
+ };
6337
+
6338
+ class CerebrasProvider extends OpenAICompatProvider {
6339
+ name = "cerebras";
6340
+ baseUrl = "https://api.cerebras.ai/v1";
6341
+ authHeader = "Authorization";
6342
+ constructor(config) {
6343
+ super({
6344
+ apiKey: config?.apiKey ?? process.env.CEREBRAS_API_KEY ?? "",
6345
+ model: config?.model ?? CEREBRAS_MODELS.default,
6346
+ maxTokens: config?.maxTokens ?? 1024,
6347
+ temperature: config?.temperature ?? 0,
6348
+ timeoutMs: config?.timeoutMs ?? 1e4
6349
+ });
6350
+ }
6351
+ }
6352
+
6353
+ // src/lib/providers/grok.ts
6354
+ var GROK_MODELS = {
6355
+ default: "grok-3-mini",
6356
+ premium: "grok-3"
6357
+ };
6358
+
6359
+ class GrokProvider extends OpenAICompatProvider {
6360
+ name = "grok";
6361
+ baseUrl = "https://api.x.ai/v1";
6362
+ authHeader = "Authorization";
6363
+ constructor(config) {
6364
+ super({
6365
+ apiKey: config?.apiKey ?? process.env.XAI_API_KEY ?? "",
6366
+ model: config?.model ?? GROK_MODELS.default,
6367
+ maxTokens: config?.maxTokens ?? 1024,
6368
+ temperature: config?.temperature ?? 0,
6369
+ timeoutMs: config?.timeoutMs ?? 15000
6370
+ });
6371
+ }
6372
+ }
6373
+
6374
+ // src/lib/providers/registry.ts
6375
+ class ProviderRegistry {
6376
+ config = { ...DEFAULT_AUTO_MEMORY_CONFIG };
6377
+ _instances = new Map;
6378
+ configure(partial) {
6379
+ this.config = { ...this.config, ...partial };
6380
+ this._instances.clear();
6381
+ }
6382
+ getConfig() {
6383
+ return this.config;
6384
+ }
6385
+ getPrimary() {
6386
+ return this.getProvider(this.config.provider);
6387
+ }
6388
+ getFallbacks() {
6389
+ const fallbackNames = this.config.fallback ?? [];
6390
+ return fallbackNames.filter((n) => n !== this.config.provider).map((n) => this.getProvider(n)).filter((p) => p !== null);
6391
+ }
6392
+ getAvailable() {
6393
+ const primary = this.getPrimary();
6394
+ if (primary)
6395
+ return primary;
6396
+ const fallbacks = this.getFallbacks();
6397
+ return fallbacks[0] ?? null;
6398
+ }
6399
+ getProvider(name) {
6400
+ const cached = this._instances.get(name);
6401
+ if (cached)
6402
+ return cached;
6403
+ const provider = this.createProvider(name);
6404
+ if (!provider)
6405
+ return null;
6406
+ if (!provider.config.apiKey)
6407
+ return null;
6408
+ this._instances.set(name, provider);
6409
+ return provider;
6410
+ }
6411
+ health() {
6412
+ const providers = ["anthropic", "openai", "cerebras", "grok"];
6413
+ const result = {};
6414
+ for (const name of providers) {
6415
+ const p = this.createProvider(name);
6416
+ result[name] = {
6417
+ available: Boolean(p?.config.apiKey),
6418
+ model: p?.config.model ?? "unknown"
6419
+ };
6420
+ }
6421
+ return result;
6422
+ }
6423
+ createProvider(name) {
6424
+ const modelOverride = name === this.config.provider ? this.config.model : undefined;
6425
+ switch (name) {
6426
+ case "anthropic":
6427
+ return new AnthropicProvider(modelOverride ? { model: modelOverride } : undefined);
6428
+ case "openai":
6429
+ return new OpenAIProvider(modelOverride ? { model: modelOverride } : undefined);
6430
+ case "cerebras":
6431
+ return new CerebrasProvider(modelOverride ? { model: modelOverride } : undefined);
6432
+ case "grok":
6433
+ return new GrokProvider(modelOverride ? { model: modelOverride } : undefined);
6434
+ default:
6435
+ return null;
6436
+ }
6437
+ }
6438
+ }
6439
+ var providerRegistry = new ProviderRegistry;
6440
+ function autoConfigureFromEnv() {
6441
+ const hasAnthropicKey = Boolean(process.env.ANTHROPIC_API_KEY);
6442
+ const hasCerebrasKey = Boolean(process.env.CEREBRAS_API_KEY);
6443
+ const hasOpenAIKey = Boolean(process.env.OPENAI_API_KEY);
6444
+ const hasGrokKey = Boolean(process.env.XAI_API_KEY);
6445
+ if (!hasAnthropicKey) {
6446
+ if (hasCerebrasKey) {
6447
+ providerRegistry.configure({ provider: "cerebras" });
6448
+ } else if (hasOpenAIKey) {
6449
+ providerRegistry.configure({ provider: "openai" });
6450
+ } else if (hasGrokKey) {
6451
+ providerRegistry.configure({ provider: "grok" });
6452
+ }
6453
+ }
6454
+ const allProviders = ["anthropic", "cerebras", "openai", "grok"];
6455
+ const available = allProviders.filter((p) => {
6456
+ switch (p) {
6457
+ case "anthropic":
6458
+ return hasAnthropicKey;
6459
+ case "cerebras":
6460
+ return hasCerebrasKey;
6461
+ case "openai":
6462
+ return hasOpenAIKey;
6463
+ case "grok":
6464
+ return hasGrokKey;
6465
+ }
6063
6466
  });
6064
- return scored;
6467
+ const primary = providerRegistry.getConfig().provider;
6468
+ const fallback = available.filter((p) => p !== primary);
6469
+ providerRegistry.configure({ fallback });
6065
6470
  }
6066
- function searchMemories(query, filter, db) {
6067
- const d = db || getDatabase();
6068
- query = preprocessQuery(query);
6069
- if (!query)
6070
- return [];
6071
- const queryLower = query.toLowerCase();
6072
- const graphBoostedIds = getGraphBoostedMemoryIds(query, d);
6073
- let scored;
6074
- if (hasFts5Table(d)) {
6075
- const ftsResult = searchWithFts5(d, query, queryLower, filter, graphBoostedIds);
6076
- if (ftsResult !== null) {
6077
- scored = ftsResult;
6078
- } else {
6079
- scored = searchWithLike(d, query, queryLower, filter, graphBoostedIds);
6471
+ autoConfigureFromEnv();
6472
+
6473
+ // src/lib/auto-memory-queue.ts
6474
+ var MAX_QUEUE_SIZE = 100;
6475
+ var CONCURRENCY = 3;
6476
+
6477
+ class AutoMemoryQueue {
6478
+ queue = [];
6479
+ handler = null;
6480
+ running = false;
6481
+ activeCount = 0;
6482
+ stats = {
6483
+ pending: 0,
6484
+ processing: 0,
6485
+ processed: 0,
6486
+ failed: 0,
6487
+ dropped: 0
6488
+ };
6489
+ setHandler(handler) {
6490
+ this.handler = handler;
6491
+ if (!this.running)
6492
+ this.startLoop();
6493
+ }
6494
+ enqueue(job) {
6495
+ if (this.queue.length >= MAX_QUEUE_SIZE) {
6496
+ this.queue.shift();
6497
+ this.stats.dropped++;
6498
+ this.stats.pending = Math.max(0, this.stats.pending - 1);
6499
+ }
6500
+ this.queue.push(job);
6501
+ this.stats.pending++;
6502
+ if (!this.running && this.handler)
6503
+ this.startLoop();
6504
+ }
6505
+ getStats() {
6506
+ return { ...this.stats, pending: this.queue.length };
6507
+ }
6508
+ startLoop() {
6509
+ this.running = true;
6510
+ this.loop();
6511
+ }
6512
+ async loop() {
6513
+ while (this.queue.length > 0 || this.activeCount > 0) {
6514
+ while (this.queue.length > 0 && this.activeCount < CONCURRENCY) {
6515
+ const job = this.queue.shift();
6516
+ if (!job)
6517
+ break;
6518
+ this.stats.pending = Math.max(0, this.stats.pending - 1);
6519
+ this.activeCount++;
6520
+ this.stats.processing = this.activeCount;
6521
+ this.processJob(job);
6522
+ }
6523
+ await new Promise((r) => setImmediate(r));
6080
6524
  }
6081
- } else {
6082
- scored = searchWithLike(d, query, queryLower, filter, graphBoostedIds);
6525
+ this.running = false;
6083
6526
  }
6084
- if (scored.length < 3) {
6085
- const fuzzyResults = searchWithFuzzy(d, query, filter, graphBoostedIds);
6086
- const seenIds = new Set(scored.map((r) => r.memory.id));
6087
- for (const fr of fuzzyResults) {
6088
- if (!seenIds.has(fr.memory.id)) {
6089
- scored.push(fr);
6090
- seenIds.add(fr.memory.id);
6091
- }
6527
+ async processJob(job) {
6528
+ if (!this.handler) {
6529
+ this.activeCount--;
6530
+ this.stats.processing = this.activeCount;
6531
+ return;
6532
+ }
6533
+ try {
6534
+ await this.handler(job);
6535
+ this.stats.processed++;
6536
+ } catch (err) {
6537
+ this.stats.failed++;
6538
+ console.error("[auto-memory-queue] job failed:", err);
6539
+ } finally {
6540
+ this.activeCount--;
6541
+ this.stats.processing = this.activeCount;
6092
6542
  }
6093
- scored.sort((a, b) => {
6094
- if (b.score !== a.score)
6095
- return b.score - a.score;
6096
- return b.memory.importance - a.memory.importance;
6097
- });
6098
6543
  }
6099
- const offset = filter?.offset ?? 0;
6100
- const limit = filter?.limit ?? scored.length;
6101
- const finalResults = scored.slice(offset, offset + limit);
6102
- logSearchQuery(query, scored.length, filter?.agent_id, filter?.project_id, d);
6103
- return finalResults;
6104
6544
  }
6105
- function logSearchQuery(query, resultCount, agentId, projectId, db) {
6545
+ var autoMemoryQueue = new AutoMemoryQueue;
6546
+
6547
+ // src/lib/auto-memory.ts
6548
+ var DEDUP_SIMILARITY_THRESHOLD = 0.85;
6549
+ function isDuplicate(content, agentId, projectId) {
6106
6550
  try {
6107
- const d = db || getDatabase();
6108
- const id = crypto.randomUUID().slice(0, 8);
6109
- d.run("INSERT INTO search_history (id, query, result_count, agent_id, project_id) VALUES (?, ?, ?, ?, ?)", [id, query, resultCount, agentId || null, projectId || null]);
6110
- } catch {}
6551
+ const query = content.split(/\s+/).filter((w) => w.length > 3).slice(0, 10).join(" ");
6552
+ if (!query)
6553
+ return false;
6554
+ const results = searchMemories(query, {
6555
+ agent_id: agentId,
6556
+ project_id: projectId,
6557
+ limit: 3
6558
+ });
6559
+ if (results.length === 0)
6560
+ return false;
6561
+ const contentWords = new Set(content.toLowerCase().split(/\W+/).filter((w) => w.length > 3));
6562
+ for (const result of results) {
6563
+ const existingWords = new Set(result.memory.value.toLowerCase().split(/\W+/).filter((w) => w.length > 3));
6564
+ if (contentWords.size === 0 || existingWords.size === 0)
6565
+ continue;
6566
+ const intersection = [...contentWords].filter((w) => existingWords.has(w)).length;
6567
+ const union = new Set([...contentWords, ...existingWords]).size;
6568
+ const similarity = intersection / union;
6569
+ if (similarity >= DEDUP_SIMILARITY_THRESHOLD)
6570
+ return true;
6571
+ }
6572
+ return false;
6573
+ } catch {
6574
+ return false;
6575
+ }
6111
6576
  }
6112
-
6113
- // src/lib/project-detect.ts
6114
- import { existsSync as existsSync3 } from "fs";
6115
- import { basename as basename2, dirname as dirname3, join as join3, resolve as resolve3 } from "path";
6116
- function findGitRoot2(startDir) {
6117
- let dir = resolve3(startDir);
6118
- while (true) {
6119
- if (existsSync3(join3(dir, ".git")))
6120
- return dir;
6121
- const parent = dirname3(dir);
6122
- if (parent === dir)
6123
- break;
6124
- dir = parent;
6577
+ async function linkEntitiesToMemory(memoryId, content, _agentId, projectId) {
6578
+ const provider = providerRegistry.getAvailable();
6579
+ if (!provider)
6580
+ return;
6581
+ try {
6582
+ const { entities, relations } = await provider.extractEntities(content);
6583
+ const entityIdMap = new Map;
6584
+ for (const extracted of entities) {
6585
+ if (extracted.confidence < 0.6)
6586
+ continue;
6587
+ try {
6588
+ const existing = getEntityByName(extracted.name);
6589
+ const entityId = existing ? existing.id : createEntity({
6590
+ name: extracted.name,
6591
+ type: extracted.type,
6592
+ project_id: projectId
6593
+ }).id;
6594
+ entityIdMap.set(extracted.name, entityId);
6595
+ linkEntityToMemory(entityId, memoryId, "subject");
6596
+ } catch {}
6597
+ }
6598
+ for (const rel of relations) {
6599
+ const fromId = entityIdMap.get(rel.from);
6600
+ const toId = entityIdMap.get(rel.to);
6601
+ if (!fromId || !toId)
6602
+ continue;
6603
+ try {
6604
+ createRelation({
6605
+ source_entity_id: fromId,
6606
+ target_entity_id: toId,
6607
+ relation_type: rel.type
6608
+ });
6609
+ } catch {}
6610
+ }
6611
+ } catch (err) {
6612
+ console.error("[auto-memory] entity linking failed:", err);
6125
6613
  }
6126
- return null;
6127
6614
  }
6128
- var _cachedProject = undefined;
6129
- function detectProject(db) {
6130
- if (_cachedProject !== undefined)
6131
- return _cachedProject;
6132
- const d = db || getDatabase();
6133
- const cwd = process.cwd();
6134
- const gitRoot = findGitRoot2(cwd);
6135
- if (!gitRoot) {
6136
- _cachedProject = null;
6615
+ async function saveExtractedMemory(extracted, context) {
6616
+ const minImportance = providerRegistry.getConfig().minImportance;
6617
+ if (extracted.importance < minImportance)
6618
+ return null;
6619
+ if (!extracted.content.trim())
6620
+ return null;
6621
+ if (isDuplicate(extracted.content, context.agentId, context.projectId)) {
6137
6622
  return null;
6138
6623
  }
6139
- const repoName = basename2(gitRoot);
6140
- const absPath = resolve3(gitRoot);
6141
- const existing = getProject(absPath, d);
6142
- if (existing) {
6143
- _cachedProject = existing;
6144
- return existing;
6624
+ try {
6625
+ const input = {
6626
+ key: extracted.content.slice(0, 120).replace(/\s+/g, "-").toLowerCase(),
6627
+ value: extracted.content,
6628
+ category: extracted.category,
6629
+ scope: extracted.suggestedScope,
6630
+ importance: extracted.importance,
6631
+ tags: [
6632
+ ...extracted.tags,
6633
+ "auto-extracted",
6634
+ ...context.sessionId ? [`session:${context.sessionId}`] : []
6635
+ ],
6636
+ agent_id: context.agentId,
6637
+ project_id: context.projectId,
6638
+ session_id: context.sessionId,
6639
+ metadata: {
6640
+ reasoning: extracted.reasoning,
6641
+ auto_extracted: true,
6642
+ extracted_at: new Date().toISOString()
6643
+ }
6644
+ };
6645
+ const memory = createMemory(input, "merge");
6646
+ return memory.id;
6647
+ } catch (err) {
6648
+ console.error("[auto-memory] saveExtractedMemory failed:", err);
6649
+ return null;
6145
6650
  }
6146
- const project = registerProject(repoName, absPath, undefined, undefined, d);
6147
- _cachedProject = project;
6148
- return project;
6149
6651
  }
6150
-
6151
- // src/lib/duration.ts
6152
- var UNIT_MS = {
6153
- s: 1000,
6154
- m: 60000,
6155
- h: 3600000,
6156
- d: 86400000,
6157
- w: 604800000
6158
- };
6159
- var DURATION_RE = /^(\d+[smhdw])+$/;
6160
- var SEGMENT_RE = /(\d+)([smhdw])/g;
6161
- function parseDuration(input) {
6162
- if (typeof input === "number")
6163
- return input;
6164
- const trimmed = input.trim();
6165
- if (trimmed === "")
6166
- throw new Error("Invalid duration: empty string");
6167
- if (/^\d+$/.test(trimmed)) {
6168
- return parseInt(trimmed, 10);
6169
- }
6170
- if (!DURATION_RE.test(trimmed)) {
6171
- throw new Error(`Invalid duration format: "${trimmed}". Use combinations of Ns, Nm, Nh, Nd, Nw (e.g. "1d12h", "30m") or plain milliseconds.`);
6172
- }
6173
- let total = 0;
6174
- let match;
6175
- SEGMENT_RE.lastIndex = 0;
6176
- while ((match = SEGMENT_RE.exec(trimmed)) !== null) {
6177
- const value = parseInt(match[1], 10);
6178
- const unit = match[2];
6179
- total += value * UNIT_MS[unit];
6652
+ async function processJob(job) {
6653
+ if (!providerRegistry.getConfig().enabled)
6654
+ return;
6655
+ const provider = providerRegistry.getAvailable();
6656
+ if (!provider)
6657
+ return;
6658
+ const context = {
6659
+ agentId: job.agentId,
6660
+ projectId: job.projectId,
6661
+ sessionId: job.sessionId
6662
+ };
6663
+ let extracted = [];
6664
+ try {
6665
+ extracted = await provider.extractMemories(job.turn, context);
6666
+ } catch {
6667
+ const fallbacks = providerRegistry.getFallbacks();
6668
+ for (const fallback of fallbacks) {
6669
+ try {
6670
+ extracted = await fallback.extractMemories(job.turn, context);
6671
+ if (extracted.length > 0)
6672
+ break;
6673
+ } catch {
6674
+ continue;
6675
+ }
6676
+ }
6180
6677
  }
6181
- if (total === 0) {
6182
- throw new Error(`Invalid duration: "${trimmed}" resolves to 0ms`);
6678
+ if (extracted.length === 0)
6679
+ return;
6680
+ for (const memory of extracted) {
6681
+ const memoryId = await saveExtractedMemory(memory, context);
6682
+ if (!memoryId)
6683
+ continue;
6684
+ if (providerRegistry.getConfig().autoEntityLink) {
6685
+ linkEntitiesToMemory(memoryId, memory.content, job.agentId, job.projectId);
6686
+ }
6183
6687
  }
6184
- return total;
6185
6688
  }
6186
- var FORMAT_UNITS = [
6187
- ["w", UNIT_MS["w"]],
6188
- ["d", UNIT_MS["d"]],
6189
- ["h", UNIT_MS["h"]],
6190
- ["m", UNIT_MS["m"]],
6191
- ["s", UNIT_MS["s"]]
6192
- ];
6689
+ autoMemoryQueue.setHandler(processJob);
6690
+ function processConversationTurn(turn, context, source = "turn") {
6691
+ if (!turn?.trim())
6692
+ return;
6693
+ autoMemoryQueue.enqueue({
6694
+ ...context,
6695
+ turn,
6696
+ timestamp: Date.now(),
6697
+ source
6698
+ });
6699
+ }
6700
+ function getAutoMemoryStats() {
6701
+ return autoMemoryQueue.getStats();
6702
+ }
6703
+ function configureAutoMemory(config) {
6704
+ providerRegistry.configure(config);
6705
+ }
6193
6706
 
6194
6707
  // src/mcp/index.ts
6195
- import { createRequire } from "module";
6196
6708
  var _require = createRequire(import.meta.url);
6197
6709
  var _pkg = _require("../../package.json");
6198
6710
  var server = new McpServer({
@@ -6283,6 +6795,11 @@ server.tool("memory_save", "Save/upsert a memory. scope: global=all agents, shar
6283
6795
  if (args.ttl_ms !== undefined) {
6284
6796
  input.ttl_ms = parseDuration(args.ttl_ms);
6285
6797
  }
6798
+ if (!input.project_id && input.agent_id) {
6799
+ const focusedProject = resolveProjectId(input.agent_id, null);
6800
+ if (focusedProject)
6801
+ input.project_id = focusedProject;
6802
+ }
6286
6803
  const memory = createMemory(input);
6287
6804
  if (args.agent_id)
6288
6805
  touchAgent(args.agent_id);
@@ -6300,7 +6817,11 @@ server.tool("memory_recall", "Recall a memory by key. Returns the best matching
6300
6817
  }, async (args) => {
6301
6818
  try {
6302
6819
  ensureAutoProject();
6303
- const memory = getMemoryByKey(args.key, args.scope, args.agent_id, args.project_id, args.session_id);
6820
+ let effectiveProjectId = args.project_id;
6821
+ if (!args.scope && !args.project_id && args.agent_id) {
6822
+ effectiveProjectId = resolveProjectId(args.agent_id, null) ?? undefined;
6823
+ }
6824
+ const memory = getMemoryByKey(args.key, args.scope, args.agent_id, effectiveProjectId, args.session_id);
6304
6825
  if (memory) {
6305
6826
  touchMemory(memory.id);
6306
6827
  if (args.agent_id)
@@ -6310,7 +6831,7 @@ server.tool("memory_recall", "Recall a memory by key. Returns the best matching
6310
6831
  const results = searchMemories(args.key, {
6311
6832
  scope: args.scope,
6312
6833
  agent_id: args.agent_id,
6313
- project_id: args.project_id,
6834
+ project_id: effectiveProjectId,
6314
6835
  session_id: args.session_id,
6315
6836
  limit: 1
6316
6837
  });
@@ -6391,9 +6912,15 @@ server.tool("memory_list", "List memories. Default: compact lines. full=true for
6391
6912
  }, async (args) => {
6392
6913
  try {
6393
6914
  const { full, fields, ...filterArgs } = args;
6915
+ let resolvedFilter = { ...filterArgs };
6916
+ if (!resolvedFilter.scope && !resolvedFilter.project_id && resolvedFilter.agent_id) {
6917
+ const focusedProject = resolveProjectId(resolvedFilter.agent_id, null);
6918
+ if (focusedProject)
6919
+ resolvedFilter.project_id = focusedProject;
6920
+ }
6394
6921
  const filter = {
6395
- ...filterArgs,
6396
- limit: filterArgs.limit || 10
6922
+ ...resolvedFilter,
6923
+ limit: resolvedFilter.limit || 10
6397
6924
  };
6398
6925
  const memories = listMemories(filter);
6399
6926
  if (memories.length === 0) {
@@ -6557,12 +7084,16 @@ server.tool("memory_search", "Search memories by keyword across key, value, summ
6557
7084
  limit: exports_external.coerce.number().optional()
6558
7085
  }, async (args) => {
6559
7086
  try {
7087
+ let effectiveProjectId = args.project_id;
7088
+ if (!args.scope && !args.project_id && args.agent_id) {
7089
+ effectiveProjectId = resolveProjectId(args.agent_id, null) ?? undefined;
7090
+ }
6560
7091
  const filter = {
6561
7092
  scope: args.scope,
6562
7093
  category: args.category,
6563
7094
  tags: args.tags,
6564
7095
  agent_id: args.agent_id,
6565
- project_id: args.project_id,
7096
+ project_id: effectiveProjectId,
6566
7097
  session_id: args.session_id,
6567
7098
  search: args.query,
6568
7099
  limit: args.limit || 20
@@ -8058,6 +8589,155 @@ server.tool("clean_expired_locks", "Delete all expired resource locks.", {}, asy
8058
8589
  const count = cleanExpiredLocks();
8059
8590
  return { content: [{ type: "text", text: `Cleaned ${count} expired lock(s).` }] };
8060
8591
  });
8592
+ server.tool("set_focus", "Set focus for an agent on a project. Memory ops will auto-scope to that project's shared + agent private + global memories.", {
8593
+ agent_id: exports_external.string(),
8594
+ project_id: exports_external.string().nullable().optional()
8595
+ }, async (args) => {
8596
+ try {
8597
+ const projectId = args.project_id ?? null;
8598
+ setFocus(args.agent_id, projectId);
8599
+ return {
8600
+ content: [{
8601
+ type: "text",
8602
+ text: projectId ? `Focus set: agent ${args.agent_id} is now focused on project ${projectId}. Memory ops will auto-scope.` : `Focus cleared for agent ${args.agent_id}.`
8603
+ }]
8604
+ };
8605
+ } catch (e) {
8606
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
8607
+ }
8608
+ });
8609
+ server.tool("get_focus", "Get the current focus project for an agent.", { agent_id: exports_external.string() }, async (args) => {
8610
+ try {
8611
+ const projectId = getFocus(args.agent_id);
8612
+ return {
8613
+ content: [{
8614
+ type: "text",
8615
+ text: projectId ? `Agent ${args.agent_id} is focused on project: ${projectId}` : `Agent ${args.agent_id} has no active focus.`
8616
+ }]
8617
+ };
8618
+ } catch (e) {
8619
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
8620
+ }
8621
+ });
8622
+ server.tool("unfocus", "Remove focus for an agent (clears project scoping).", { agent_id: exports_external.string() }, async (args) => {
8623
+ try {
8624
+ unfocus(args.agent_id);
8625
+ return {
8626
+ content: [{ type: "text", text: `Focus cleared for agent ${args.agent_id}. Memory ops will no longer auto-scope.` }]
8627
+ };
8628
+ } catch (e) {
8629
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
8630
+ }
8631
+ });
8632
+ server.tool("memory_auto_process", "Enqueue a conversation turn or text for async LLM memory extraction. Returns immediately (non-blocking). Set async=false to run synchronously and return extracted memories.", {
8633
+ turn: exports_external.string().describe("The text / conversation turn to extract memories from"),
8634
+ agent_id: exports_external.string().optional(),
8635
+ project_id: exports_external.string().optional(),
8636
+ session_id: exports_external.string().optional(),
8637
+ async: exports_external.boolean().optional().default(true)
8638
+ }, async (args) => {
8639
+ try {
8640
+ if (args.async !== false) {
8641
+ processConversationTurn(args.turn, {
8642
+ agentId: args.agent_id,
8643
+ projectId: args.project_id,
8644
+ sessionId: args.session_id
8645
+ });
8646
+ const stats = getAutoMemoryStats();
8647
+ return {
8648
+ content: [{
8649
+ type: "text",
8650
+ text: JSON.stringify({ queued: true, queue: stats })
8651
+ }]
8652
+ };
8653
+ }
8654
+ const provider = providerRegistry.getAvailable();
8655
+ if (!provider) {
8656
+ return { content: [{ type: "text", text: JSON.stringify({ error: "No LLM provider configured. Set ANTHROPIC_API_KEY, OPENAI_API_KEY, CEREBRAS_API_KEY, or XAI_API_KEY." }) }], isError: true };
8657
+ }
8658
+ const memories = await provider.extractMemories(args.turn, {
8659
+ agentId: args.agent_id,
8660
+ projectId: args.project_id,
8661
+ sessionId: args.session_id
8662
+ });
8663
+ return { content: [{ type: "text", text: JSON.stringify({ extracted: memories, count: memories.length }) }] };
8664
+ } catch (e) {
8665
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
8666
+ }
8667
+ });
8668
+ server.tool("memory_auto_status", "Get the current auto-memory extraction queue stats (pending, processing, processed, failed, dropped).", {}, async () => {
8669
+ try {
8670
+ const stats = getAutoMemoryStats();
8671
+ const config = providerRegistry.getConfig();
8672
+ const health = providerRegistry.health();
8673
+ return {
8674
+ content: [{
8675
+ type: "text",
8676
+ text: JSON.stringify({ queue: stats, config, providers: health })
8677
+ }]
8678
+ };
8679
+ } catch (e) {
8680
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
8681
+ }
8682
+ });
8683
+ server.tool("memory_auto_config", "Update auto-memory configuration at runtime (no restart needed). Set provider, model, enabled, minImportance, autoEntityLink.", {
8684
+ provider: exports_external.enum(["anthropic", "openai", "cerebras", "grok"]).optional(),
8685
+ model: exports_external.string().optional(),
8686
+ enabled: exports_external.boolean().optional(),
8687
+ min_importance: exports_external.number().min(0).max(10).optional(),
8688
+ auto_entity_link: exports_external.boolean().optional()
8689
+ }, async (args) => {
8690
+ try {
8691
+ configureAutoMemory({
8692
+ ...args.provider && { provider: args.provider },
8693
+ ...args.model && { model: args.model },
8694
+ ...args.enabled !== undefined && { enabled: args.enabled },
8695
+ ...args.min_importance !== undefined && { minImportance: args.min_importance },
8696
+ ...args.auto_entity_link !== undefined && { autoEntityLink: args.auto_entity_link }
8697
+ });
8698
+ return {
8699
+ content: [{
8700
+ type: "text",
8701
+ text: JSON.stringify({ updated: true, config: providerRegistry.getConfig() })
8702
+ }]
8703
+ };
8704
+ } catch (e) {
8705
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
8706
+ }
8707
+ });
8708
+ server.tool("memory_auto_test", "Test memory extraction on text WITHOUT saving anything. Returns what would be extracted. Useful for tuning prompts and checking provider output.", {
8709
+ turn: exports_external.string().describe("Text to test extraction on"),
8710
+ provider: exports_external.enum(["anthropic", "openai", "cerebras", "grok"]).optional(),
8711
+ agent_id: exports_external.string().optional(),
8712
+ project_id: exports_external.string().optional()
8713
+ }, async (args) => {
8714
+ try {
8715
+ let provider;
8716
+ if (args.provider) {
8717
+ provider = providerRegistry.getProvider(args.provider);
8718
+ if (!provider) {
8719
+ return { content: [{ type: "text", text: JSON.stringify({ error: `Provider '${args.provider}' not available \u2014 no API key configured.` }) }], isError: true };
8720
+ }
8721
+ } else {
8722
+ provider = providerRegistry.getAvailable();
8723
+ if (!provider) {
8724
+ return { content: [{ type: "text", text: JSON.stringify({ error: "No LLM provider configured." }) }], isError: true };
8725
+ }
8726
+ }
8727
+ const memories = await provider.extractMemories(args.turn, {
8728
+ agentId: args.agent_id,
8729
+ projectId: args.project_id
8730
+ });
8731
+ return {
8732
+ content: [{
8733
+ type: "text",
8734
+ text: JSON.stringify({ provider: provider.name, model: provider.config.model, extracted: memories, count: memories.length, note: "DRY RUN \u2014 nothing was saved" })
8735
+ }]
8736
+ };
8737
+ } catch (e) {
8738
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
8739
+ }
8740
+ });
8061
8741
  async function main() {
8062
8742
  const transport = new StdioServerTransport;
8063
8743
  await server.connect(transport);