@hasna/mementos 0.3.9 → 0.4.2

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.
package/dist/mcp/index.js CHANGED
@@ -3989,6 +3989,13 @@ var coerce = {
3989
3989
  };
3990
3990
  var NEVER = INVALID;
3991
3991
  // src/types/index.ts
3992
+ class EntityNotFoundError extends Error {
3993
+ constructor(id) {
3994
+ super(`Entity not found: ${id}`);
3995
+ this.name = "EntityNotFoundError";
3996
+ }
3997
+ }
3998
+
3992
3999
  class MemoryNotFoundError extends Error {
3993
4000
  constructor(id) {
3994
4001
  super(`Memory not found: ${id}`);
@@ -4164,6 +4171,113 @@ var MIGRATIONS = [
4164
4171
  );
4165
4172
 
4166
4173
  INSERT OR IGNORE INTO _migrations (id) VALUES (1);
4174
+ `,
4175
+ `
4176
+ CREATE TABLE IF NOT EXISTS memory_versions (
4177
+ id TEXT PRIMARY KEY,
4178
+ memory_id TEXT NOT NULL,
4179
+ version INTEGER NOT NULL,
4180
+ value TEXT NOT NULL,
4181
+ importance INTEGER NOT NULL,
4182
+ scope TEXT NOT NULL,
4183
+ category TEXT NOT NULL,
4184
+ tags TEXT NOT NULL DEFAULT '[]',
4185
+ summary TEXT,
4186
+ pinned INTEGER NOT NULL DEFAULT 0,
4187
+ status TEXT NOT NULL DEFAULT 'active',
4188
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
4189
+ UNIQUE(memory_id, version)
4190
+ );
4191
+
4192
+ CREATE INDEX IF NOT EXISTS idx_memory_versions_memory ON memory_versions(memory_id);
4193
+ CREATE INDEX IF NOT EXISTS idx_memory_versions_version ON memory_versions(memory_id, version);
4194
+
4195
+ INSERT OR IGNORE INTO _migrations (id) VALUES (2);
4196
+ `,
4197
+ `
4198
+ CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
4199
+ key, value, summary,
4200
+ content='memories',
4201
+ content_rowid='rowid'
4202
+ );
4203
+
4204
+ CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
4205
+ INSERT INTO memories_fts(rowid, key, value, summary) VALUES (new.rowid, new.key, new.value, new.summary);
4206
+ END;
4207
+
4208
+ CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
4209
+ INSERT INTO memories_fts(memories_fts, rowid, key, value, summary) VALUES('delete', old.rowid, old.key, old.value, old.summary);
4210
+ END;
4211
+
4212
+ CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
4213
+ INSERT INTO memories_fts(memories_fts, rowid, key, value, summary) VALUES('delete', old.rowid, old.key, old.value, old.summary);
4214
+ INSERT INTO memories_fts(rowid, key, value, summary) VALUES (new.rowid, new.key, new.value, new.summary);
4215
+ END;
4216
+
4217
+ INSERT INTO memories_fts(memories_fts) VALUES('rebuild');
4218
+
4219
+ INSERT OR IGNORE INTO _migrations (id) VALUES (3);
4220
+ `,
4221
+ `
4222
+ CREATE TABLE IF NOT EXISTS search_history (
4223
+ id TEXT PRIMARY KEY,
4224
+ query TEXT NOT NULL,
4225
+ result_count INTEGER NOT NULL DEFAULT 0,
4226
+ agent_id TEXT,
4227
+ project_id TEXT,
4228
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
4229
+ );
4230
+ CREATE INDEX IF NOT EXISTS idx_search_history_query ON search_history(query);
4231
+ CREATE INDEX IF NOT EXISTS idx_search_history_created ON search_history(created_at);
4232
+
4233
+ INSERT OR IGNORE INTO _migrations (id) VALUES (4);
4234
+ `,
4235
+ `
4236
+ CREATE TABLE IF NOT EXISTS entities (
4237
+ id TEXT PRIMARY KEY,
4238
+ name TEXT NOT NULL,
4239
+ type TEXT NOT NULL CHECK (type IN ('person','project','tool','concept','file','api','pattern','organization')),
4240
+ description TEXT,
4241
+ metadata TEXT NOT NULL DEFAULT '{}',
4242
+ project_id TEXT,
4243
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
4244
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
4245
+ FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE SET NULL
4246
+ );
4247
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_entities_unique_name_type_project
4248
+ ON entities(name, type, COALESCE(project_id, ''));
4249
+ CREATE INDEX IF NOT EXISTS idx_entities_name ON entities(name);
4250
+ CREATE INDEX IF NOT EXISTS idx_entities_type ON entities(type);
4251
+ CREATE INDEX IF NOT EXISTS idx_entities_project ON entities(project_id);
4252
+
4253
+ CREATE TABLE IF NOT EXISTS relations (
4254
+ id TEXT PRIMARY KEY,
4255
+ source_entity_id TEXT NOT NULL,
4256
+ target_entity_id TEXT NOT NULL,
4257
+ relation_type TEXT NOT NULL CHECK (relation_type IN ('uses','knows','depends_on','created_by','related_to','contradicts','part_of','implements')),
4258
+ weight REAL NOT NULL DEFAULT 1.0,
4259
+ metadata TEXT NOT NULL DEFAULT '{}',
4260
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
4261
+ UNIQUE(source_entity_id, target_entity_id, relation_type),
4262
+ FOREIGN KEY (source_entity_id) REFERENCES entities(id) ON DELETE CASCADE,
4263
+ FOREIGN KEY (target_entity_id) REFERENCES entities(id) ON DELETE CASCADE
4264
+ );
4265
+ CREATE INDEX IF NOT EXISTS idx_relations_source ON relations(source_entity_id);
4266
+ CREATE INDEX IF NOT EXISTS idx_relations_target ON relations(target_entity_id);
4267
+ CREATE INDEX IF NOT EXISTS idx_relations_type ON relations(relation_type);
4268
+
4269
+ CREATE TABLE IF NOT EXISTS entity_memories (
4270
+ entity_id TEXT NOT NULL,
4271
+ memory_id TEXT NOT NULL,
4272
+ role TEXT NOT NULL DEFAULT 'context' CHECK (role IN ('subject','object','context')),
4273
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
4274
+ PRIMARY KEY (entity_id, memory_id),
4275
+ FOREIGN KEY (entity_id) REFERENCES entities(id) ON DELETE CASCADE,
4276
+ FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE CASCADE
4277
+ );
4278
+ CREATE INDEX IF NOT EXISTS idx_entity_memories_memory ON entity_memories(memory_id);
4279
+
4280
+ INSERT OR IGNORE INTO _migrations (id) VALUES (5);
4167
4281
  `
4168
4282
  ];
4169
4283
  var _db = null;
@@ -4245,7 +4359,650 @@ function redactSecrets(text) {
4245
4359
  return result;
4246
4360
  }
4247
4361
 
4362
+ // src/db/agents.ts
4363
+ function parseAgentRow(row) {
4364
+ return {
4365
+ id: row["id"],
4366
+ name: row["name"],
4367
+ description: row["description"] || null,
4368
+ role: row["role"] || null,
4369
+ metadata: JSON.parse(row["metadata"] || "{}"),
4370
+ created_at: row["created_at"],
4371
+ last_seen_at: row["last_seen_at"]
4372
+ };
4373
+ }
4374
+ function registerAgent(name, description, role, db) {
4375
+ const d = db || getDatabase();
4376
+ const timestamp = now();
4377
+ const normalizedName = name.trim().toLowerCase();
4378
+ const existing = d.query("SELECT * FROM agents WHERE LOWER(name) = ?").get(normalizedName);
4379
+ if (existing) {
4380
+ const existingId = existing["id"];
4381
+ d.run("UPDATE agents SET last_seen_at = ? WHERE id = ?", [
4382
+ timestamp,
4383
+ existingId
4384
+ ]);
4385
+ if (description) {
4386
+ d.run("UPDATE agents SET description = ? WHERE id = ?", [
4387
+ description,
4388
+ existingId
4389
+ ]);
4390
+ }
4391
+ if (role) {
4392
+ d.run("UPDATE agents SET role = ? WHERE id = ?", [
4393
+ role,
4394
+ existingId
4395
+ ]);
4396
+ }
4397
+ return getAgent(existingId, d);
4398
+ }
4399
+ const id = shortUuid();
4400
+ d.run("INSERT INTO agents (id, name, description, role, created_at, last_seen_at) VALUES (?, ?, ?, ?, ?, ?)", [id, normalizedName, description || null, role || "agent", timestamp, timestamp]);
4401
+ return getAgent(id, d);
4402
+ }
4403
+ function getAgent(idOrName, db) {
4404
+ const d = db || getDatabase();
4405
+ let row = d.query("SELECT * FROM agents WHERE id = ?").get(idOrName);
4406
+ if (row)
4407
+ return parseAgentRow(row);
4408
+ row = d.query("SELECT * FROM agents WHERE LOWER(name) = ?").get(idOrName.trim().toLowerCase());
4409
+ if (row)
4410
+ return parseAgentRow(row);
4411
+ const rows = d.query("SELECT * FROM agents WHERE id LIKE ?").all(`${idOrName}%`);
4412
+ if (rows.length === 1)
4413
+ return parseAgentRow(rows[0]);
4414
+ return null;
4415
+ }
4416
+ function listAgents(db) {
4417
+ const d = db || getDatabase();
4418
+ const rows = d.query("SELECT * FROM agents ORDER BY last_seen_at DESC").all();
4419
+ return rows.map(parseAgentRow);
4420
+ }
4421
+ function updateAgent(id, updates, db) {
4422
+ const d = db || getDatabase();
4423
+ const agent = getAgent(id, d);
4424
+ if (!agent)
4425
+ return null;
4426
+ const timestamp = now();
4427
+ if (updates.name) {
4428
+ const normalizedNewName = updates.name.trim().toLowerCase();
4429
+ if (normalizedNewName !== agent.name) {
4430
+ const existing = d.query("SELECT id FROM agents WHERE LOWER(name) = ? AND id != ?").get(normalizedNewName, agent.id);
4431
+ if (existing) {
4432
+ throw new Error(`Agent name already taken: ${normalizedNewName}`);
4433
+ }
4434
+ d.run("UPDATE agents SET name = ? WHERE id = ?", [normalizedNewName, agent.id]);
4435
+ }
4436
+ }
4437
+ if (updates.description !== undefined) {
4438
+ d.run("UPDATE agents SET description = ? WHERE id = ?", [updates.description, agent.id]);
4439
+ }
4440
+ if (updates.role !== undefined) {
4441
+ d.run("UPDATE agents SET role = ? WHERE id = ?", [updates.role, agent.id]);
4442
+ }
4443
+ if (updates.metadata !== undefined) {
4444
+ d.run("UPDATE agents SET metadata = ? WHERE id = ?", [JSON.stringify(updates.metadata), agent.id]);
4445
+ }
4446
+ d.run("UPDATE agents SET last_seen_at = ? WHERE id = ?", [timestamp, agent.id]);
4447
+ return getAgent(agent.id, d);
4448
+ }
4449
+
4450
+ // src/db/projects.ts
4451
+ function parseProjectRow(row) {
4452
+ return {
4453
+ id: row["id"],
4454
+ name: row["name"],
4455
+ path: row["path"],
4456
+ description: row["description"] || null,
4457
+ memory_prefix: row["memory_prefix"] || null,
4458
+ created_at: row["created_at"],
4459
+ updated_at: row["updated_at"]
4460
+ };
4461
+ }
4462
+ function registerProject(name, path, description, memoryPrefix, db) {
4463
+ const d = db || getDatabase();
4464
+ const timestamp = now();
4465
+ const existing = d.query("SELECT * FROM projects WHERE path = ?").get(path);
4466
+ if (existing) {
4467
+ const existingId = existing["id"];
4468
+ d.run("UPDATE projects SET updated_at = ? WHERE id = ?", [
4469
+ timestamp,
4470
+ existingId
4471
+ ]);
4472
+ return parseProjectRow(existing);
4473
+ }
4474
+ const id = uuid();
4475
+ 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]);
4476
+ return getProject(id, d);
4477
+ }
4478
+ function getProject(idOrPath, db) {
4479
+ const d = db || getDatabase();
4480
+ let row = d.query("SELECT * FROM projects WHERE id = ?").get(idOrPath);
4481
+ if (row)
4482
+ return parseProjectRow(row);
4483
+ row = d.query("SELECT * FROM projects WHERE path = ?").get(idOrPath);
4484
+ if (row)
4485
+ return parseProjectRow(row);
4486
+ return null;
4487
+ }
4488
+ function listProjects(db) {
4489
+ const d = db || getDatabase();
4490
+ const rows = d.query("SELECT * FROM projects ORDER BY updated_at DESC").all();
4491
+ return rows.map(parseProjectRow);
4492
+ }
4493
+
4494
+ // src/lib/extractor.ts
4495
+ var TECH_KEYWORDS = new Set([
4496
+ "typescript",
4497
+ "javascript",
4498
+ "python",
4499
+ "rust",
4500
+ "go",
4501
+ "java",
4502
+ "ruby",
4503
+ "swift",
4504
+ "kotlin",
4505
+ "react",
4506
+ "vue",
4507
+ "angular",
4508
+ "svelte",
4509
+ "nextjs",
4510
+ "bun",
4511
+ "node",
4512
+ "deno",
4513
+ "sqlite",
4514
+ "postgres",
4515
+ "mysql",
4516
+ "redis",
4517
+ "docker",
4518
+ "kubernetes",
4519
+ "git",
4520
+ "npm",
4521
+ "yarn",
4522
+ "pnpm",
4523
+ "webpack",
4524
+ "vite",
4525
+ "tailwind",
4526
+ "prisma",
4527
+ "drizzle",
4528
+ "zod",
4529
+ "commander",
4530
+ "express",
4531
+ "fastify",
4532
+ "hono"
4533
+ ]);
4534
+ var FILE_PATH_RE = /(?:^|\s)((?:\/|\.\/|~\/)?(?:[\w.-]+\/)+[\w.-]+\.\w+)/g;
4535
+ var URL_RE = /https?:\/\/[^\s)]+/g;
4536
+ var NPM_PACKAGE_RE = /@[\w-]+\/[\w.-]+/g;
4537
+ var PASCAL_CASE_RE = /\b([A-Z][a-z]+(?:[A-Z][a-z]+)+)\b/g;
4538
+ function getSearchText(memory) {
4539
+ const parts = [memory.key, memory.value];
4540
+ if (memory.summary)
4541
+ parts.push(memory.summary);
4542
+ return parts.join(" ");
4543
+ }
4544
+ function extractEntities(memory, db) {
4545
+ const text = getSearchText(memory);
4546
+ const entityMap = new Map;
4547
+ function add(name, type, confidence) {
4548
+ const normalized = name.toLowerCase();
4549
+ if (normalized.length < 3)
4550
+ return;
4551
+ const existing = entityMap.get(normalized);
4552
+ if (!existing || existing.confidence < confidence) {
4553
+ entityMap.set(normalized, { name: normalized, type, confidence });
4554
+ }
4555
+ }
4556
+ for (const match of text.matchAll(FILE_PATH_RE)) {
4557
+ add(match[1].trim(), "file", 0.9);
4558
+ }
4559
+ for (const match of text.matchAll(URL_RE)) {
4560
+ add(match[0], "api", 0.8);
4561
+ }
4562
+ for (const match of text.matchAll(NPM_PACKAGE_RE)) {
4563
+ add(match[0], "tool", 0.85);
4564
+ }
4565
+ try {
4566
+ const d = db || getDatabase();
4567
+ const agents = listAgents(d);
4568
+ const textLower2 = text.toLowerCase();
4569
+ for (const agent of agents) {
4570
+ const nameLower = agent.name.toLowerCase();
4571
+ if (nameLower.length >= 3 && textLower2.includes(nameLower)) {
4572
+ add(agent.name, "person", 0.95);
4573
+ }
4574
+ }
4575
+ } catch {}
4576
+ try {
4577
+ const d = db || getDatabase();
4578
+ const projects = listProjects(d);
4579
+ const textLower2 = text.toLowerCase();
4580
+ for (const project of projects) {
4581
+ const nameLower = project.name.toLowerCase();
4582
+ if (nameLower.length >= 3 && textLower2.includes(nameLower)) {
4583
+ add(project.name, "project", 0.95);
4584
+ }
4585
+ }
4586
+ } catch {}
4587
+ const textLower = text.toLowerCase();
4588
+ for (const keyword of TECH_KEYWORDS) {
4589
+ const re = new RegExp(`\\b${keyword}\\b`, "i");
4590
+ if (re.test(textLower)) {
4591
+ add(keyword, "tool", 0.7);
4592
+ }
4593
+ }
4594
+ for (const match of text.matchAll(PASCAL_CASE_RE)) {
4595
+ add(match[1], "concept", 0.5);
4596
+ }
4597
+ return Array.from(entityMap.values()).sort((a, b) => b.confidence - a.confidence);
4598
+ }
4599
+
4600
+ // src/db/entities.ts
4601
+ function parseEntityRow(row) {
4602
+ return {
4603
+ id: row["id"],
4604
+ name: row["name"],
4605
+ type: row["type"],
4606
+ description: row["description"] || null,
4607
+ metadata: JSON.parse(row["metadata"] || "{}"),
4608
+ project_id: row["project_id"] || null,
4609
+ created_at: row["created_at"],
4610
+ updated_at: row["updated_at"]
4611
+ };
4612
+ }
4613
+ function createEntity(input, db) {
4614
+ const d = db || getDatabase();
4615
+ const timestamp = now();
4616
+ const metadataJson = JSON.stringify(input.metadata || {});
4617
+ const existing = d.query(`SELECT * FROM entities
4618
+ WHERE name = ? AND type = ? AND COALESCE(project_id, '') = ?`).get(input.name, input.type, input.project_id || "");
4619
+ if (existing) {
4620
+ const sets = ["updated_at = ?"];
4621
+ const params = [timestamp];
4622
+ if (input.description !== undefined) {
4623
+ sets.push("description = ?");
4624
+ params.push(input.description);
4625
+ }
4626
+ if (input.metadata !== undefined) {
4627
+ sets.push("metadata = ?");
4628
+ params.push(metadataJson);
4629
+ }
4630
+ const existingId = existing["id"];
4631
+ params.push(existingId);
4632
+ d.run(`UPDATE entities SET ${sets.join(", ")} WHERE id = ?`, params);
4633
+ return getEntity(existingId, d);
4634
+ }
4635
+ const id = shortUuid();
4636
+ d.run(`INSERT INTO entities (id, name, type, description, metadata, project_id, created_at, updated_at)
4637
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
4638
+ id,
4639
+ input.name,
4640
+ input.type,
4641
+ input.description || null,
4642
+ metadataJson,
4643
+ input.project_id || null,
4644
+ timestamp,
4645
+ timestamp
4646
+ ]);
4647
+ return getEntity(id, d);
4648
+ }
4649
+ function getEntity(id, db) {
4650
+ const d = db || getDatabase();
4651
+ const row = d.query("SELECT * FROM entities WHERE id = ?").get(id);
4652
+ if (!row)
4653
+ throw new EntityNotFoundError(id);
4654
+ return parseEntityRow(row);
4655
+ }
4656
+ function getEntityByName(name, type, projectId, db) {
4657
+ const d = db || getDatabase();
4658
+ let sql = "SELECT * FROM entities WHERE name = ?";
4659
+ const params = [name];
4660
+ if (type) {
4661
+ sql += " AND type = ?";
4662
+ params.push(type);
4663
+ }
4664
+ if (projectId !== undefined) {
4665
+ sql += " AND project_id = ?";
4666
+ params.push(projectId);
4667
+ }
4668
+ sql += " LIMIT 1";
4669
+ const row = d.query(sql).get(...params);
4670
+ if (!row)
4671
+ return null;
4672
+ return parseEntityRow(row);
4673
+ }
4674
+ function listEntities(filter = {}, db) {
4675
+ const d = db || getDatabase();
4676
+ const conditions = [];
4677
+ const params = [];
4678
+ if (filter.type) {
4679
+ conditions.push("type = ?");
4680
+ params.push(filter.type);
4681
+ }
4682
+ if (filter.project_id) {
4683
+ conditions.push("project_id = ?");
4684
+ params.push(filter.project_id);
4685
+ }
4686
+ if (filter.search) {
4687
+ conditions.push("(name LIKE ? OR description LIKE ?)");
4688
+ const term = `%${filter.search}%`;
4689
+ params.push(term, term);
4690
+ }
4691
+ let sql = "SELECT * FROM entities";
4692
+ if (conditions.length > 0) {
4693
+ sql += ` WHERE ${conditions.join(" AND ")}`;
4694
+ }
4695
+ sql += " ORDER BY updated_at DESC";
4696
+ if (filter.limit) {
4697
+ sql += " LIMIT ?";
4698
+ params.push(filter.limit);
4699
+ }
4700
+ if (filter.offset) {
4701
+ sql += " OFFSET ?";
4702
+ params.push(filter.offset);
4703
+ }
4704
+ const rows = d.query(sql).all(...params);
4705
+ return rows.map(parseEntityRow);
4706
+ }
4707
+ function deleteEntity(id, db) {
4708
+ const d = db || getDatabase();
4709
+ const result = d.run("DELETE FROM entities WHERE id = ?", [id]);
4710
+ if (result.changes === 0)
4711
+ throw new EntityNotFoundError(id);
4712
+ }
4713
+ function mergeEntities(sourceId, targetId, db) {
4714
+ const d = db || getDatabase();
4715
+ getEntity(sourceId, d);
4716
+ getEntity(targetId, d);
4717
+ d.run(`UPDATE OR IGNORE relations SET source_entity_id = ? WHERE source_entity_id = ?`, [targetId, sourceId]);
4718
+ d.run(`UPDATE OR IGNORE relations SET target_entity_id = ? WHERE target_entity_id = ?`, [targetId, sourceId]);
4719
+ d.run("DELETE FROM relations WHERE source_entity_id = ? OR target_entity_id = ?", [
4720
+ sourceId,
4721
+ sourceId
4722
+ ]);
4723
+ d.run(`UPDATE OR IGNORE entity_memories SET entity_id = ? WHERE entity_id = ?`, [targetId, sourceId]);
4724
+ d.run("DELETE FROM entity_memories WHERE entity_id = ?", [sourceId]);
4725
+ d.run("DELETE FROM entities WHERE id = ?", [sourceId]);
4726
+ d.run("UPDATE entities SET updated_at = ? WHERE id = ?", [now(), targetId]);
4727
+ return getEntity(targetId, d);
4728
+ }
4729
+
4730
+ // src/db/entity-memories.ts
4731
+ function parseEntityMemoryRow(row) {
4732
+ return {
4733
+ entity_id: row["entity_id"],
4734
+ memory_id: row["memory_id"],
4735
+ role: row["role"],
4736
+ created_at: row["created_at"]
4737
+ };
4738
+ }
4739
+ function linkEntityToMemory(entityId, memoryId, role = "context", db) {
4740
+ const d = db || getDatabase();
4741
+ const timestamp = now();
4742
+ d.run(`INSERT OR IGNORE INTO entity_memories (entity_id, memory_id, role, created_at)
4743
+ VALUES (?, ?, ?, ?)`, [entityId, memoryId, role, timestamp]);
4744
+ const row = d.query("SELECT * FROM entity_memories WHERE entity_id = ? AND memory_id = ?").get(entityId, memoryId);
4745
+ return parseEntityMemoryRow(row);
4746
+ }
4747
+ function unlinkEntityFromMemory(entityId, memoryId, db) {
4748
+ const d = db || getDatabase();
4749
+ d.run("DELETE FROM entity_memories WHERE entity_id = ? AND memory_id = ?", [entityId, memoryId]);
4750
+ }
4751
+ function getMemoriesForEntity(entityId, db) {
4752
+ const d = db || getDatabase();
4753
+ const rows = d.query(`SELECT m.* FROM memories m
4754
+ INNER JOIN entity_memories em ON em.memory_id = m.id
4755
+ WHERE em.entity_id = ?
4756
+ ORDER BY m.importance DESC, m.created_at DESC`).all(entityId);
4757
+ return rows.map(parseMemoryRow);
4758
+ }
4759
+ function getEntityMemoryLinks(entityId, memoryId, db) {
4760
+ const d = db || getDatabase();
4761
+ const conditions = [];
4762
+ const params = [];
4763
+ if (entityId) {
4764
+ conditions.push("entity_id = ?");
4765
+ params.push(entityId);
4766
+ }
4767
+ if (memoryId) {
4768
+ conditions.push("memory_id = ?");
4769
+ params.push(memoryId);
4770
+ }
4771
+ let sql = "SELECT * FROM entity_memories";
4772
+ if (conditions.length > 0) {
4773
+ sql += ` WHERE ${conditions.join(" AND ")}`;
4774
+ }
4775
+ sql += " ORDER BY created_at DESC";
4776
+ const rows = d.query(sql).all(...params);
4777
+ return rows.map(parseEntityMemoryRow);
4778
+ }
4779
+
4780
+ // src/db/relations.ts
4781
+ function parseRelationRow(row) {
4782
+ return {
4783
+ id: row["id"],
4784
+ source_entity_id: row["source_entity_id"],
4785
+ target_entity_id: row["target_entity_id"],
4786
+ relation_type: row["relation_type"],
4787
+ weight: row["weight"],
4788
+ metadata: JSON.parse(row["metadata"] || "{}"),
4789
+ created_at: row["created_at"]
4790
+ };
4791
+ }
4792
+ function parseEntityRow2(row) {
4793
+ return {
4794
+ id: row["id"],
4795
+ name: row["name"],
4796
+ type: row["type"],
4797
+ description: row["description"] || null,
4798
+ metadata: JSON.parse(row["metadata"] || "{}"),
4799
+ project_id: row["project_id"] || null,
4800
+ created_at: row["created_at"],
4801
+ updated_at: row["updated_at"]
4802
+ };
4803
+ }
4804
+ function createRelation(input, db) {
4805
+ const d = db || getDatabase();
4806
+ const id = shortUuid();
4807
+ const timestamp = now();
4808
+ const weight = input.weight ?? 1;
4809
+ const metadata = JSON.stringify(input.metadata ?? {});
4810
+ d.run(`INSERT INTO relations (id, source_entity_id, target_entity_id, relation_type, weight, metadata, created_at)
4811
+ VALUES (?, ?, ?, ?, ?, ?, ?)
4812
+ ON CONFLICT(source_entity_id, target_entity_id, relation_type)
4813
+ DO UPDATE SET weight = excluded.weight, metadata = excluded.metadata`, [id, input.source_entity_id, input.target_entity_id, input.relation_type, weight, metadata, timestamp]);
4814
+ const row = d.query(`SELECT * FROM relations
4815
+ WHERE source_entity_id = ? AND target_entity_id = ? AND relation_type = ?`).get(input.source_entity_id, input.target_entity_id, input.relation_type);
4816
+ return parseRelationRow(row);
4817
+ }
4818
+ function listRelations(filter, db) {
4819
+ const d = db || getDatabase();
4820
+ const conditions = [];
4821
+ const params = [];
4822
+ if (filter.entity_id) {
4823
+ const dir = filter.direction || "both";
4824
+ if (dir === "outgoing") {
4825
+ conditions.push("source_entity_id = ?");
4826
+ params.push(filter.entity_id);
4827
+ } else if (dir === "incoming") {
4828
+ conditions.push("target_entity_id = ?");
4829
+ params.push(filter.entity_id);
4830
+ } else {
4831
+ conditions.push("(source_entity_id = ? OR target_entity_id = ?)");
4832
+ params.push(filter.entity_id, filter.entity_id);
4833
+ }
4834
+ }
4835
+ if (filter.relation_type) {
4836
+ conditions.push("relation_type = ?");
4837
+ params.push(filter.relation_type);
4838
+ }
4839
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
4840
+ const rows = d.query(`SELECT * FROM relations ${where} ORDER BY created_at DESC`).all(...params);
4841
+ return rows.map(parseRelationRow);
4842
+ }
4843
+ function deleteRelation(id, db) {
4844
+ const d = db || getDatabase();
4845
+ const result = d.run("DELETE FROM relations WHERE id = ?", [id]);
4846
+ if (result.changes === 0)
4847
+ throw new Error(`Relation not found: ${id}`);
4848
+ }
4849
+ function getEntityGraph(entityId, depth = 2, db) {
4850
+ const d = db || getDatabase();
4851
+ const entityRows = d.query(`WITH RECURSIVE graph(id, depth) AS (
4852
+ VALUES(?, 0)
4853
+ UNION
4854
+ SELECT CASE WHEN r.source_entity_id = g.id THEN r.target_entity_id ELSE r.source_entity_id END, g.depth + 1
4855
+ FROM relations r JOIN graph g ON (r.source_entity_id = g.id OR r.target_entity_id = g.id)
4856
+ WHERE g.depth < ?
4857
+ )
4858
+ SELECT DISTINCT e.* FROM entities e JOIN graph g ON e.id = g.id`).all(entityId, depth);
4859
+ const entities = entityRows.map(parseEntityRow2);
4860
+ const entityIds = new Set(entities.map((e) => e.id));
4861
+ if (entityIds.size === 0) {
4862
+ return { entities: [], relations: [] };
4863
+ }
4864
+ const placeholders = Array.from(entityIds).map(() => "?").join(",");
4865
+ const relationRows = d.query(`SELECT * FROM relations
4866
+ WHERE source_entity_id IN (${placeholders})
4867
+ AND target_entity_id IN (${placeholders})`).all(...Array.from(entityIds), ...Array.from(entityIds));
4868
+ const relations = relationRows.map(parseRelationRow);
4869
+ return { entities, relations };
4870
+ }
4871
+ function findPath(fromEntityId, toEntityId, maxDepth = 5, db) {
4872
+ const d = db || getDatabase();
4873
+ const rows = d.query(`WITH RECURSIVE path(id, trail, depth) AS (
4874
+ SELECT ?, ?, 0
4875
+ UNION
4876
+ SELECT
4877
+ CASE WHEN r.source_entity_id = p.id THEN r.target_entity_id ELSE r.source_entity_id END,
4878
+ p.trail || ',' || CASE WHEN r.source_entity_id = p.id THEN r.target_entity_id ELSE r.source_entity_id END,
4879
+ p.depth + 1
4880
+ FROM relations r JOIN path p ON (r.source_entity_id = p.id OR r.target_entity_id = p.id)
4881
+ WHERE p.depth < ?
4882
+ AND INSTR(p.trail, CASE WHEN r.source_entity_id = p.id THEN r.target_entity_id ELSE r.source_entity_id END) = 0
4883
+ )
4884
+ SELECT trail FROM path WHERE id = ? ORDER BY depth ASC LIMIT 1`).get(fromEntityId, fromEntityId, maxDepth, toEntityId);
4885
+ if (!rows)
4886
+ return null;
4887
+ const ids = rows.trail.split(",");
4888
+ const entities = [];
4889
+ for (const id of ids) {
4890
+ const row = d.query("SELECT * FROM entities WHERE id = ?").get(id);
4891
+ if (row)
4892
+ entities.push(parseEntityRow2(row));
4893
+ }
4894
+ return entities.length > 0 ? entities : null;
4895
+ }
4896
+
4897
+ // src/lib/config.ts
4898
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync } from "fs";
4899
+ import { homedir } from "os";
4900
+ import { dirname as dirname2, join as join2, resolve as resolve2 } from "path";
4901
+ var DEFAULT_CONFIG = {
4902
+ default_scope: "private",
4903
+ default_category: "knowledge",
4904
+ default_importance: 5,
4905
+ max_entries: 1000,
4906
+ max_entries_per_scope: {
4907
+ global: 500,
4908
+ shared: 300,
4909
+ private: 200
4910
+ },
4911
+ injection: {
4912
+ max_tokens: 500,
4913
+ min_importance: 5,
4914
+ categories: ["preference", "fact"],
4915
+ refresh_interval: 5
4916
+ },
4917
+ extraction: {
4918
+ enabled: true,
4919
+ min_confidence: 0.5
4920
+ },
4921
+ sync_agents: ["claude", "codex", "gemini"],
4922
+ auto_cleanup: {
4923
+ enabled: true,
4924
+ expired_check_interval: 3600,
4925
+ unused_archive_days: 7,
4926
+ stale_deprioritize_days: 14
4927
+ }
4928
+ };
4929
+ function deepMerge(target, source) {
4930
+ const result = { ...target };
4931
+ for (const key of Object.keys(source)) {
4932
+ const sourceVal = source[key];
4933
+ const targetVal = result[key];
4934
+ if (sourceVal !== null && typeof sourceVal === "object" && !Array.isArray(sourceVal) && targetVal !== null && typeof targetVal === "object" && !Array.isArray(targetVal)) {
4935
+ result[key] = deepMerge(targetVal, sourceVal);
4936
+ } else {
4937
+ result[key] = sourceVal;
4938
+ }
4939
+ }
4940
+ return result;
4941
+ }
4942
+ var VALID_SCOPES = ["global", "shared", "private"];
4943
+ var VALID_CATEGORIES = [
4944
+ "preference",
4945
+ "fact",
4946
+ "knowledge",
4947
+ "history"
4948
+ ];
4949
+ function isValidScope(value) {
4950
+ return VALID_SCOPES.includes(value);
4951
+ }
4952
+ function isValidCategory(value) {
4953
+ return VALID_CATEGORIES.includes(value);
4954
+ }
4955
+ function loadConfig() {
4956
+ const configPath = join2(homedir(), ".mementos", "config.json");
4957
+ let fileConfig = {};
4958
+ if (existsSync2(configPath)) {
4959
+ try {
4960
+ const raw = readFileSync(configPath, "utf-8");
4961
+ fileConfig = JSON.parse(raw);
4962
+ } catch {}
4963
+ }
4964
+ const merged = deepMerge(DEFAULT_CONFIG, fileConfig);
4965
+ const envScope = process.env["MEMENTOS_DEFAULT_SCOPE"];
4966
+ if (envScope && isValidScope(envScope)) {
4967
+ merged.default_scope = envScope;
4968
+ }
4969
+ const envCategory = process.env["MEMENTOS_DEFAULT_CATEGORY"];
4970
+ if (envCategory && isValidCategory(envCategory)) {
4971
+ merged.default_category = envCategory;
4972
+ }
4973
+ const envImportance = process.env["MEMENTOS_DEFAULT_IMPORTANCE"];
4974
+ if (envImportance) {
4975
+ const parsed = parseInt(envImportance, 10);
4976
+ if (!Number.isNaN(parsed) && parsed >= 1 && parsed <= 10) {
4977
+ merged.default_importance = parsed;
4978
+ }
4979
+ }
4980
+ return merged;
4981
+ }
4982
+
4248
4983
  // src/db/memories.ts
4984
+ function runEntityExtraction(memory, projectId, d) {
4985
+ const config = loadConfig();
4986
+ if (config.extraction?.enabled === false)
4987
+ return;
4988
+ const extracted = extractEntities(memory, d);
4989
+ const minConfidence = config.extraction?.min_confidence ?? 0.5;
4990
+ const entityIds = [];
4991
+ for (const ext of extracted) {
4992
+ if (ext.confidence >= minConfidence) {
4993
+ const entity = createEntity({ name: ext.name, type: ext.type, project_id: projectId }, d);
4994
+ linkEntityToMemory(entity.id, memory.id, "context", d);
4995
+ entityIds.push(entity.id);
4996
+ }
4997
+ }
4998
+ for (let i = 0;i < entityIds.length; i++) {
4999
+ for (let j = i + 1;j < entityIds.length; j++) {
5000
+ try {
5001
+ createRelation({ source_entity_id: entityIds[i], target_entity_id: entityIds[j], relation_type: "related_to" }, d);
5002
+ } catch {}
5003
+ }
5004
+ }
5005
+ }
4249
5006
  function parseMemoryRow(row) {
4250
5007
  return {
4251
5008
  id: row["id"],
@@ -4312,7 +5069,15 @@ function createMemory(input, dedupeMode = "merge", db) {
4312
5069
  for (const tag of tags) {
4313
5070
  insertTag2.run(existing.id, tag);
4314
5071
  }
4315
- return getMemory(existing.id, d);
5072
+ const merged = getMemory(existing.id, d);
5073
+ try {
5074
+ const oldLinks = getEntityMemoryLinks(undefined, merged.id, d);
5075
+ for (const link of oldLinks) {
5076
+ unlinkEntityFromMemory(link.entity_id, merged.id, d);
5077
+ }
5078
+ runEntityExtraction(merged, input.project_id, d);
5079
+ } catch {}
5080
+ return merged;
4316
5081
  }
4317
5082
  }
4318
5083
  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)
@@ -4338,7 +5103,11 @@ function createMemory(input, dedupeMode = "merge", db) {
4338
5103
  for (const tag of tags) {
4339
5104
  insertTag.run(id, tag);
4340
5105
  }
4341
- return getMemory(id, d);
5106
+ const memory = getMemory(id, d);
5107
+ try {
5108
+ runEntityExtraction(memory, input.project_id, d);
5109
+ } catch {}
5110
+ return memory;
4342
5111
  }
4343
5112
  function getMemory(id, db) {
4344
5113
  const d = db || getDatabase();
@@ -4474,6 +5243,23 @@ function updateMemory(id, input, db) {
4474
5243
  if (existing.version !== input.version) {
4475
5244
  throw new VersionConflictError(id, input.version, existing.version);
4476
5245
  }
5246
+ try {
5247
+ d.run(`INSERT OR IGNORE INTO memory_versions (id, memory_id, version, value, importance, scope, category, tags, summary, pinned, status, created_at)
5248
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
5249
+ uuid(),
5250
+ existing.id,
5251
+ existing.version,
5252
+ existing.value,
5253
+ existing.importance,
5254
+ existing.scope,
5255
+ existing.category,
5256
+ JSON.stringify(existing.tags),
5257
+ existing.summary,
5258
+ existing.pinned ? 1 : 0,
5259
+ existing.status,
5260
+ existing.updated_at
5261
+ ]);
5262
+ } catch {}
4477
5263
  const sets = ["version = version + 1", "updated_at = ?"];
4478
5264
  const params = [now()];
4479
5265
  if (input.value !== undefined) {
@@ -4523,7 +5309,17 @@ function updateMemory(id, input, db) {
4523
5309
  }
4524
5310
  params.push(id);
4525
5311
  d.run(`UPDATE memories SET ${sets.join(", ")} WHERE id = ?`, params);
4526
- return getMemory(id, d);
5312
+ const updated = getMemory(id, d);
5313
+ try {
5314
+ if (input.value !== undefined) {
5315
+ const oldLinks = getEntityMemoryLinks(undefined, updated.id, d);
5316
+ for (const link of oldLinks) {
5317
+ unlinkEntityFromMemory(link.entity_id, updated.id, d);
5318
+ }
5319
+ runEntityExtraction(updated, existing.project_id || undefined, d);
5320
+ }
5321
+ } catch {}
5322
+ return updated;
4527
5323
  }
4528
5324
  function deleteMemory(id, db) {
4529
5325
  const d = db || getDatabase();
@@ -4535,8 +5331,12 @@ function bulkDeleteMemories(ids, db) {
4535
5331
  if (ids.length === 0)
4536
5332
  return 0;
4537
5333
  const placeholders = ids.map(() => "?").join(",");
4538
- const result = d.run(`DELETE FROM memories WHERE id IN (${placeholders})`, ids);
4539
- return result.changes;
5334
+ const countRow = d.query(`SELECT COUNT(*) as c FROM memories WHERE id IN (${placeholders})`).get(...ids);
5335
+ const count = countRow.c;
5336
+ if (count > 0) {
5337
+ d.run(`DELETE FROM memories WHERE id IN (${placeholders})`, ids);
5338
+ }
5339
+ return count;
4540
5340
  }
4541
5341
  function touchMemory(id, db) {
4542
5342
  const d = db || getDatabase();
@@ -4545,136 +5345,12 @@ function touchMemory(id, db) {
4545
5345
  function cleanExpiredMemories(db) {
4546
5346
  const d = db || getDatabase();
4547
5347
  const timestamp = now();
4548
- const result = d.run("DELETE FROM memories WHERE expires_at IS NOT NULL AND expires_at < ?", [timestamp]);
4549
- return result.changes;
4550
- }
4551
-
4552
- // src/db/agents.ts
4553
- function parseAgentRow(row) {
4554
- return {
4555
- id: row["id"],
4556
- name: row["name"],
4557
- description: row["description"] || null,
4558
- role: row["role"] || null,
4559
- metadata: JSON.parse(row["metadata"] || "{}"),
4560
- created_at: row["created_at"],
4561
- last_seen_at: row["last_seen_at"]
4562
- };
4563
- }
4564
- function registerAgent(name, description, role, db) {
4565
- const d = db || getDatabase();
4566
- const timestamp = now();
4567
- const existing = d.query("SELECT * FROM agents WHERE name = ?").get(name);
4568
- if (existing) {
4569
- const existingId = existing["id"];
4570
- d.run("UPDATE agents SET last_seen_at = ? WHERE id = ?", [
4571
- timestamp,
4572
- existingId
4573
- ]);
4574
- if (description) {
4575
- d.run("UPDATE agents SET description = ? WHERE id = ?", [
4576
- description,
4577
- existingId
4578
- ]);
4579
- }
4580
- if (role) {
4581
- d.run("UPDATE agents SET role = ? WHERE id = ?", [
4582
- role,
4583
- existingId
4584
- ]);
4585
- }
4586
- return getAgent(existingId, d);
4587
- }
4588
- const id = shortUuid();
4589
- d.run("INSERT INTO agents (id, name, description, role, created_at, last_seen_at) VALUES (?, ?, ?, ?, ?, ?)", [id, name, description || null, role || "agent", timestamp, timestamp]);
4590
- return getAgent(id, d);
4591
- }
4592
- function getAgent(idOrName, db) {
4593
- const d = db || getDatabase();
4594
- let row = d.query("SELECT * FROM agents WHERE id = ?").get(idOrName);
4595
- if (row)
4596
- return parseAgentRow(row);
4597
- row = d.query("SELECT * FROM agents WHERE name = ?").get(idOrName);
4598
- if (row)
4599
- return parseAgentRow(row);
4600
- const rows = d.query("SELECT * FROM agents WHERE id LIKE ?").all(`${idOrName}%`);
4601
- if (rows.length === 1)
4602
- return parseAgentRow(rows[0]);
4603
- return null;
4604
- }
4605
- function listAgents(db) {
4606
- const d = db || getDatabase();
4607
- const rows = d.query("SELECT * FROM agents ORDER BY last_seen_at DESC").all();
4608
- return rows.map(parseAgentRow);
4609
- }
4610
- function updateAgent(id, updates, db) {
4611
- const d = db || getDatabase();
4612
- const agent = getAgent(id, d);
4613
- if (!agent)
4614
- return null;
4615
- const timestamp = now();
4616
- if (updates.name && updates.name !== agent.name) {
4617
- const existing = d.query("SELECT id FROM agents WHERE name = ? AND id != ?").get(updates.name, agent.id);
4618
- if (existing) {
4619
- throw new Error(`Agent name already taken: ${updates.name}`);
4620
- }
4621
- d.run("UPDATE agents SET name = ? WHERE id = ?", [updates.name, agent.id]);
5348
+ const countRow = d.query("SELECT COUNT(*) as c FROM memories WHERE expires_at IS NOT NULL AND expires_at < ?").get(timestamp);
5349
+ const count = countRow.c;
5350
+ if (count > 0) {
5351
+ d.run("DELETE FROM memories WHERE expires_at IS NOT NULL AND expires_at < ?", [timestamp]);
4622
5352
  }
4623
- if (updates.description !== undefined) {
4624
- d.run("UPDATE agents SET description = ? WHERE id = ?", [updates.description, agent.id]);
4625
- }
4626
- if (updates.role !== undefined) {
4627
- d.run("UPDATE agents SET role = ? WHERE id = ?", [updates.role, agent.id]);
4628
- }
4629
- if (updates.metadata !== undefined) {
4630
- d.run("UPDATE agents SET metadata = ? WHERE id = ?", [JSON.stringify(updates.metadata), agent.id]);
4631
- }
4632
- d.run("UPDATE agents SET last_seen_at = ? WHERE id = ?", [timestamp, agent.id]);
4633
- return getAgent(agent.id, d);
4634
- }
4635
-
4636
- // src/db/projects.ts
4637
- function parseProjectRow(row) {
4638
- return {
4639
- id: row["id"],
4640
- name: row["name"],
4641
- path: row["path"],
4642
- description: row["description"] || null,
4643
- memory_prefix: row["memory_prefix"] || null,
4644
- created_at: row["created_at"],
4645
- updated_at: row["updated_at"]
4646
- };
4647
- }
4648
- function registerProject(name, path, description, memoryPrefix, db) {
4649
- const d = db || getDatabase();
4650
- const timestamp = now();
4651
- const existing = d.query("SELECT * FROM projects WHERE path = ?").get(path);
4652
- if (existing) {
4653
- const existingId = existing["id"];
4654
- d.run("UPDATE projects SET updated_at = ? WHERE id = ?", [
4655
- timestamp,
4656
- existingId
4657
- ]);
4658
- return parseProjectRow(existing);
4659
- }
4660
- const id = uuid();
4661
- 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]);
4662
- return getProject(id, d);
4663
- }
4664
- function getProject(idOrPath, db) {
4665
- const d = db || getDatabase();
4666
- let row = d.query("SELECT * FROM projects WHERE id = ?").get(idOrPath);
4667
- if (row)
4668
- return parseProjectRow(row);
4669
- row = d.query("SELECT * FROM projects WHERE path = ?").get(idOrPath);
4670
- if (row)
4671
- return parseProjectRow(row);
4672
- return null;
4673
- }
4674
- function listProjects(db) {
4675
- const d = db || getDatabase();
4676
- const rows = d.query("SELECT * FROM projects ORDER BY updated_at DESC").all();
4677
- return rows.map(parseProjectRow);
5353
+ return count;
4678
5354
  }
4679
5355
 
4680
5356
  // src/lib/search.ts
@@ -4703,109 +5379,442 @@ function parseMemoryRow2(row) {
4703
5379
  accessed_at: row["accessed_at"] || null
4704
5380
  };
4705
5381
  }
5382
+ function preprocessQuery(query) {
5383
+ let q = query.trim();
5384
+ q = q.replace(/\s+/g, " ");
5385
+ q = q.normalize("NFC");
5386
+ return q;
5387
+ }
5388
+ function escapeLikePattern(s) {
5389
+ return s.replace(/%/g, "\\%").replace(/_/g, "\\_");
5390
+ }
5391
+ var STOP_WORDS = new Set([
5392
+ "a",
5393
+ "an",
5394
+ "the",
5395
+ "is",
5396
+ "are",
5397
+ "was",
5398
+ "were",
5399
+ "be",
5400
+ "been",
5401
+ "being",
5402
+ "have",
5403
+ "has",
5404
+ "had",
5405
+ "do",
5406
+ "does",
5407
+ "did",
5408
+ "will",
5409
+ "would",
5410
+ "could",
5411
+ "should",
5412
+ "may",
5413
+ "might",
5414
+ "shall",
5415
+ "can",
5416
+ "need",
5417
+ "dare",
5418
+ "ought",
5419
+ "used",
5420
+ "to",
5421
+ "of",
5422
+ "in",
5423
+ "for",
5424
+ "on",
5425
+ "with",
5426
+ "at",
5427
+ "by",
5428
+ "from",
5429
+ "as",
5430
+ "into",
5431
+ "through",
5432
+ "during",
5433
+ "before",
5434
+ "after",
5435
+ "above",
5436
+ "below",
5437
+ "between",
5438
+ "out",
5439
+ "off",
5440
+ "over",
5441
+ "under",
5442
+ "again",
5443
+ "further",
5444
+ "then",
5445
+ "once",
5446
+ "here",
5447
+ "there",
5448
+ "when",
5449
+ "where",
5450
+ "why",
5451
+ "how",
5452
+ "all",
5453
+ "each",
5454
+ "every",
5455
+ "both",
5456
+ "few",
5457
+ "more",
5458
+ "most",
5459
+ "other",
5460
+ "some",
5461
+ "such",
5462
+ "no",
5463
+ "not",
5464
+ "only",
5465
+ "own",
5466
+ "same",
5467
+ "so",
5468
+ "than",
5469
+ "too",
5470
+ "very",
5471
+ "just",
5472
+ "because",
5473
+ "but",
5474
+ "and",
5475
+ "or",
5476
+ "if",
5477
+ "while",
5478
+ "that",
5479
+ "this",
5480
+ "it"
5481
+ ]);
5482
+ function removeStopWords(tokens) {
5483
+ if (tokens.length <= 1)
5484
+ return tokens;
5485
+ const filtered = tokens.filter((t) => !STOP_WORDS.has(t.toLowerCase()));
5486
+ return filtered.length > 0 ? filtered : tokens;
5487
+ }
5488
+ function extractHighlights(memory, queryLower) {
5489
+ const highlights = [];
5490
+ const tokens = queryLower.split(/\s+/).filter(Boolean);
5491
+ for (const field of ["key", "value", "summary"]) {
5492
+ const text = field === "summary" ? memory.summary : memory[field];
5493
+ if (!text)
5494
+ continue;
5495
+ const textLower = text.toLowerCase();
5496
+ const searchTerms = [queryLower, ...tokens].filter(Boolean);
5497
+ for (const term of searchTerms) {
5498
+ const idx = textLower.indexOf(term);
5499
+ if (idx !== -1) {
5500
+ const start = Math.max(0, idx - 30);
5501
+ const end = Math.min(text.length, idx + term.length + 30);
5502
+ const prefix = start > 0 ? "..." : "";
5503
+ const suffix = end < text.length ? "..." : "";
5504
+ highlights.push({
5505
+ field,
5506
+ snippet: prefix + text.slice(start, end) + suffix
5507
+ });
5508
+ break;
5509
+ }
5510
+ }
5511
+ }
5512
+ for (const tag of memory.tags) {
5513
+ if (tag.toLowerCase().includes(queryLower) || tokens.some((t) => tag.toLowerCase().includes(t))) {
5514
+ highlights.push({ field: "tag", snippet: tag });
5515
+ }
5516
+ }
5517
+ return highlights;
5518
+ }
4706
5519
  function determineMatchType(memory, queryLower) {
4707
5520
  if (memory.key.toLowerCase() === queryLower)
4708
5521
  return "exact";
4709
5522
  if (memory.tags.some((t) => t.toLowerCase() === queryLower))
4710
5523
  return "tag";
5524
+ if (memory.tags.some((t) => t.toLowerCase().includes(queryLower)))
5525
+ return "tag";
4711
5526
  return "fuzzy";
4712
5527
  }
4713
5528
  function computeScore(memory, queryLower) {
4714
- let score = 0;
5529
+ const fieldScores = [];
4715
5530
  const keyLower = memory.key.toLowerCase();
4716
5531
  if (keyLower === queryLower) {
4717
- score += 10;
5532
+ fieldScores.push(10);
4718
5533
  } else if (keyLower.includes(queryLower)) {
4719
- score += 7;
5534
+ fieldScores.push(7);
4720
5535
  }
4721
5536
  if (memory.tags.some((t) => t.toLowerCase() === queryLower)) {
4722
- score += 6;
5537
+ fieldScores.push(6);
5538
+ } else if (memory.tags.some((t) => t.toLowerCase().includes(queryLower))) {
5539
+ fieldScores.push(3);
4723
5540
  }
4724
5541
  if (memory.summary && memory.summary.toLowerCase().includes(queryLower)) {
4725
- score += 4;
5542
+ fieldScores.push(4);
4726
5543
  }
4727
5544
  if (memory.value.toLowerCase().includes(queryLower)) {
4728
- score += 3;
5545
+ fieldScores.push(3);
5546
+ }
5547
+ const metadataStr = JSON.stringify(memory.metadata).toLowerCase();
5548
+ if (metadataStr !== "{}" && metadataStr.includes(queryLower)) {
5549
+ fieldScores.push(2);
5550
+ }
5551
+ fieldScores.sort((a, b) => b - a);
5552
+ const diminishingMultipliers = [1, 0.5, 0.25, 0.15, 0.15];
5553
+ let score = 0;
5554
+ for (let i = 0;i < fieldScores.length; i++) {
5555
+ score += fieldScores[i] * (diminishingMultipliers[i] ?? 0.15);
5556
+ }
5557
+ const { phrases } = extractQuotedPhrases(queryLower);
5558
+ for (const phrase of phrases) {
5559
+ if (keyLower.includes(phrase))
5560
+ score += 8;
5561
+ if (memory.value.toLowerCase().includes(phrase))
5562
+ score += 5;
5563
+ if (memory.summary && memory.summary.toLowerCase().includes(phrase))
5564
+ score += 4;
5565
+ }
5566
+ const { remainder } = extractQuotedPhrases(queryLower);
5567
+ const tokens = removeStopWords(remainder.split(/\s+/).filter(Boolean));
5568
+ if (tokens.length > 1) {
5569
+ let tokenScore = 0;
5570
+ for (const token of tokens) {
5571
+ if (keyLower === token) {
5572
+ tokenScore += 10 / tokens.length;
5573
+ } else if (keyLower.includes(token)) {
5574
+ tokenScore += 7 / tokens.length;
5575
+ }
5576
+ if (memory.tags.some((t) => t.toLowerCase() === token)) {
5577
+ tokenScore += 6 / tokens.length;
5578
+ } else if (memory.tags.some((t) => t.toLowerCase().includes(token))) {
5579
+ tokenScore += 3 / tokens.length;
5580
+ }
5581
+ if (memory.summary && memory.summary.toLowerCase().includes(token)) {
5582
+ tokenScore += 4 / tokens.length;
5583
+ }
5584
+ if (memory.value.toLowerCase().includes(token)) {
5585
+ tokenScore += 3 / tokens.length;
5586
+ }
5587
+ if (metadataStr !== "{}" && metadataStr.includes(token)) {
5588
+ tokenScore += 2 / tokens.length;
5589
+ }
5590
+ }
5591
+ if (score > 0) {
5592
+ score += tokenScore * 0.3;
5593
+ } else {
5594
+ score += tokenScore;
5595
+ }
4729
5596
  }
4730
5597
  return score;
4731
5598
  }
4732
- function searchMemories(query, filter, db) {
4733
- const d = db || getDatabase();
4734
- const queryLower = query.toLowerCase();
4735
- const queryParam = `%${query}%`;
5599
+ function extractQuotedPhrases(query) {
5600
+ const phrases = [];
5601
+ const remainder = query.replace(/"([^"]+)"/g, (_match, phrase) => {
5602
+ phrases.push(phrase);
5603
+ return "";
5604
+ });
5605
+ return { phrases, remainder: remainder.trim() };
5606
+ }
5607
+ function escapeFts5Query(query) {
5608
+ const { phrases, remainder } = extractQuotedPhrases(query);
5609
+ const parts = [];
5610
+ for (const phrase of phrases) {
5611
+ parts.push(`"${phrase.replace(/"/g, '""')}"`);
5612
+ }
5613
+ const tokens = removeStopWords(remainder.split(/\s+/).filter(Boolean));
5614
+ for (const t of tokens) {
5615
+ parts.push(`"${t.replace(/"/g, '""')}"`);
5616
+ }
5617
+ return parts.join(" ");
5618
+ }
5619
+ function hasFts5Table(d) {
5620
+ try {
5621
+ const row = d.query("SELECT name FROM sqlite_master WHERE type='table' AND name='memories_fts'").get();
5622
+ return !!row;
5623
+ } catch {
5624
+ return false;
5625
+ }
5626
+ }
5627
+ function buildFilterConditions(filter) {
4736
5628
  const conditions = [];
4737
5629
  const params = [];
4738
5630
  conditions.push("m.status = 'active'");
4739
5631
  conditions.push("(m.expires_at IS NULL OR m.expires_at >= datetime('now'))");
4740
- conditions.push(`(m.key LIKE ? OR m.value LIKE ? OR m.summary LIKE ? OR m.id IN (SELECT memory_id FROM memory_tags WHERE tag LIKE ?))`);
4741
- params.push(queryParam, queryParam, queryParam, queryParam);
4742
- if (filter) {
4743
- if (filter.scope) {
4744
- if (Array.isArray(filter.scope)) {
4745
- conditions.push(`m.scope IN (${filter.scope.map(() => "?").join(",")})`);
4746
- params.push(...filter.scope);
4747
- } else {
4748
- conditions.push("m.scope = ?");
4749
- params.push(filter.scope);
4750
- }
4751
- }
4752
- if (filter.category) {
4753
- if (Array.isArray(filter.category)) {
4754
- conditions.push(`m.category IN (${filter.category.map(() => "?").join(",")})`);
4755
- params.push(...filter.category);
4756
- } else {
4757
- conditions.push("m.category = ?");
4758
- params.push(filter.category);
4759
- }
5632
+ if (!filter)
5633
+ return { conditions, params };
5634
+ if (filter.scope) {
5635
+ if (Array.isArray(filter.scope)) {
5636
+ conditions.push(`m.scope IN (${filter.scope.map(() => "?").join(",")})`);
5637
+ params.push(...filter.scope);
5638
+ } else {
5639
+ conditions.push("m.scope = ?");
5640
+ params.push(filter.scope);
4760
5641
  }
4761
- if (filter.source) {
4762
- if (Array.isArray(filter.source)) {
4763
- conditions.push(`m.source IN (${filter.source.map(() => "?").join(",")})`);
4764
- params.push(...filter.source);
4765
- } else {
4766
- conditions.push("m.source = ?");
4767
- params.push(filter.source);
4768
- }
5642
+ }
5643
+ if (filter.category) {
5644
+ if (Array.isArray(filter.category)) {
5645
+ conditions.push(`m.category IN (${filter.category.map(() => "?").join(",")})`);
5646
+ params.push(...filter.category);
5647
+ } else {
5648
+ conditions.push("m.category = ?");
5649
+ params.push(filter.category);
4769
5650
  }
4770
- if (filter.status) {
4771
- conditions.shift();
4772
- if (Array.isArray(filter.status)) {
4773
- conditions.push(`m.status IN (${filter.status.map(() => "?").join(",")})`);
4774
- params.push(...filter.status);
4775
- } else {
4776
- conditions.push("m.status = ?");
4777
- params.push(filter.status);
4778
- }
5651
+ }
5652
+ if (filter.source) {
5653
+ if (Array.isArray(filter.source)) {
5654
+ conditions.push(`m.source IN (${filter.source.map(() => "?").join(",")})`);
5655
+ params.push(...filter.source);
5656
+ } else {
5657
+ conditions.push("m.source = ?");
5658
+ params.push(filter.source);
4779
5659
  }
4780
- if (filter.project_id) {
4781
- conditions.push("m.project_id = ?");
4782
- params.push(filter.project_id);
5660
+ }
5661
+ if (filter.status) {
5662
+ conditions.shift();
5663
+ if (Array.isArray(filter.status)) {
5664
+ conditions.push(`m.status IN (${filter.status.map(() => "?").join(",")})`);
5665
+ params.push(...filter.status);
5666
+ } else {
5667
+ conditions.push("m.status = ?");
5668
+ params.push(filter.status);
4783
5669
  }
4784
- if (filter.agent_id) {
4785
- conditions.push("m.agent_id = ?");
4786
- params.push(filter.agent_id);
5670
+ }
5671
+ if (filter.project_id) {
5672
+ conditions.push("m.project_id = ?");
5673
+ params.push(filter.project_id);
5674
+ }
5675
+ if (filter.agent_id) {
5676
+ conditions.push("m.agent_id = ?");
5677
+ params.push(filter.agent_id);
5678
+ }
5679
+ if (filter.session_id) {
5680
+ conditions.push("m.session_id = ?");
5681
+ params.push(filter.session_id);
5682
+ }
5683
+ if (filter.min_importance) {
5684
+ conditions.push("m.importance >= ?");
5685
+ params.push(filter.min_importance);
5686
+ }
5687
+ if (filter.pinned !== undefined) {
5688
+ conditions.push("m.pinned = ?");
5689
+ params.push(filter.pinned ? 1 : 0);
5690
+ }
5691
+ if (filter.tags && filter.tags.length > 0) {
5692
+ for (const tag of filter.tags) {
5693
+ conditions.push("m.id IN (SELECT memory_id FROM memory_tags WHERE tag = ?)");
5694
+ params.push(tag);
4787
5695
  }
4788
- if (filter.session_id) {
4789
- conditions.push("m.session_id = ?");
4790
- params.push(filter.session_id);
5696
+ }
5697
+ return { conditions, params };
5698
+ }
5699
+ function searchWithFts5(d, query, queryLower, filter, graphBoostedIds) {
5700
+ const ftsQuery = escapeFts5Query(query);
5701
+ if (!ftsQuery)
5702
+ return null;
5703
+ try {
5704
+ const { conditions, params } = buildFilterConditions(filter);
5705
+ const queryParam = `%${query}%`;
5706
+ const ftsCondition = `(m.rowid IN (SELECT f.rowid FROM memories_fts f WHERE memories_fts MATCH ?) ` + `OR m.id IN (SELECT memory_id FROM memory_tags WHERE tag LIKE ?) ` + `OR m.metadata LIKE ?)`;
5707
+ const allConditions = [ftsCondition, ...conditions];
5708
+ const allParams = [ftsQuery, queryParam, queryParam, ...params];
5709
+ const candidateSql = `SELECT m.* FROM memories m WHERE ${allConditions.join(" AND ")}`;
5710
+ const rows = d.query(candidateSql).all(...allParams);
5711
+ return scoreResults(rows, queryLower, graphBoostedIds);
5712
+ } catch {
5713
+ return null;
5714
+ }
5715
+ }
5716
+ function searchWithLike(d, query, queryLower, filter, graphBoostedIds) {
5717
+ const { conditions, params } = buildFilterConditions(filter);
5718
+ const rawTokens = query.trim().split(/\s+/).filter(Boolean);
5719
+ const tokens = removeStopWords(rawTokens);
5720
+ const escapedQuery = escapeLikePattern(query);
5721
+ const likePatterns = [`%${escapedQuery}%`];
5722
+ if (tokens.length > 1) {
5723
+ for (const t of tokens)
5724
+ likePatterns.push(`%${escapeLikePattern(t)}%`);
5725
+ }
5726
+ const fieldClauses = [];
5727
+ for (const pattern of likePatterns) {
5728
+ fieldClauses.push("m.key LIKE ? ESCAPE '\\'");
5729
+ params.push(pattern);
5730
+ fieldClauses.push("m.value LIKE ? ESCAPE '\\'");
5731
+ params.push(pattern);
5732
+ fieldClauses.push("m.summary LIKE ? ESCAPE '\\'");
5733
+ params.push(pattern);
5734
+ fieldClauses.push("m.metadata LIKE ? ESCAPE '\\'");
5735
+ params.push(pattern);
5736
+ fieldClauses.push("m.id IN (SELECT memory_id FROM memory_tags WHERE tag LIKE ? ESCAPE '\\')");
5737
+ params.push(pattern);
5738
+ }
5739
+ conditions.push(`(${fieldClauses.join(" OR ")})`);
5740
+ const sql = `SELECT DISTINCT m.* FROM memories m WHERE ${conditions.join(" AND ")}`;
5741
+ const rows = d.query(sql).all(...params);
5742
+ return scoreResults(rows, queryLower, graphBoostedIds);
5743
+ }
5744
+ function generateTrigrams(s) {
5745
+ const lower = s.toLowerCase();
5746
+ const trigrams = new Set;
5747
+ for (let i = 0;i <= lower.length - 3; i++) {
5748
+ trigrams.add(lower.slice(i, i + 3));
5749
+ }
5750
+ return trigrams;
5751
+ }
5752
+ function trigramSimilarity(a, b) {
5753
+ const triA = generateTrigrams(a);
5754
+ const triB = generateTrigrams(b);
5755
+ if (triA.size === 0 || triB.size === 0)
5756
+ return 0;
5757
+ let intersection = 0;
5758
+ for (const t of triA) {
5759
+ if (triB.has(t))
5760
+ intersection++;
5761
+ }
5762
+ const union = triA.size + triB.size - intersection;
5763
+ return union === 0 ? 0 : intersection / union;
5764
+ }
5765
+ function searchWithFuzzy(d, query, filter, graphBoostedIds) {
5766
+ const { conditions, params } = buildFilterConditions(filter);
5767
+ const sql = `SELECT m.* FROM memories m WHERE ${conditions.join(" AND ")}`;
5768
+ const rows = d.query(sql).all(...params);
5769
+ const MIN_SIMILARITY = 0.3;
5770
+ const results = [];
5771
+ for (const row of rows) {
5772
+ const memory = parseMemoryRow2(row);
5773
+ let bestSimilarity = 0;
5774
+ bestSimilarity = Math.max(bestSimilarity, trigramSimilarity(query, memory.key));
5775
+ bestSimilarity = Math.max(bestSimilarity, trigramSimilarity(query, memory.value.slice(0, 200)));
5776
+ if (memory.summary) {
5777
+ bestSimilarity = Math.max(bestSimilarity, trigramSimilarity(query, memory.summary));
4791
5778
  }
4792
- if (filter.min_importance) {
4793
- conditions.push("m.importance >= ?");
4794
- params.push(filter.min_importance);
5779
+ for (const tag of memory.tags) {
5780
+ bestSimilarity = Math.max(bestSimilarity, trigramSimilarity(query, tag));
4795
5781
  }
4796
- if (filter.pinned !== undefined) {
4797
- conditions.push("m.pinned = ?");
4798
- params.push(filter.pinned ? 1 : 0);
5782
+ if (bestSimilarity >= MIN_SIMILARITY) {
5783
+ const graphBoost = graphBoostedIds?.has(memory.id) ? 2 : 0;
5784
+ const score = bestSimilarity * 5 * memory.importance / 10 + graphBoost;
5785
+ results.push({ memory, score, match_type: "fuzzy" });
4799
5786
  }
4800
- if (filter.tags && filter.tags.length > 0) {
4801
- for (const tag of filter.tags) {
4802
- conditions.push("m.id IN (SELECT memory_id FROM memory_tags WHERE tag = ?)");
4803
- params.push(tag);
5787
+ }
5788
+ results.sort((a, b) => b.score - a.score);
5789
+ return results;
5790
+ }
5791
+ function getGraphBoostedMemoryIds(query, d) {
5792
+ const boostedIds = new Set;
5793
+ try {
5794
+ const matchingEntities = listEntities({ search: query, limit: 10 }, d);
5795
+ const exactMatch = getEntityByName(query, undefined, undefined, d);
5796
+ if (exactMatch && !matchingEntities.find((e) => e.id === exactMatch.id)) {
5797
+ matchingEntities.push(exactMatch);
5798
+ }
5799
+ for (const entity of matchingEntities) {
5800
+ const memories = getMemoriesForEntity(entity.id, d);
5801
+ for (const mem of memories) {
5802
+ boostedIds.add(mem.id);
4804
5803
  }
4805
5804
  }
4806
- }
4807
- const sql = `SELECT m.* FROM memories m WHERE ${conditions.join(" AND ")}`;
4808
- const rows = d.query(sql).all(...params);
5805
+ } catch {}
5806
+ return boostedIds;
5807
+ }
5808
+ function computeRecencyBoost(memory) {
5809
+ if (memory.pinned)
5810
+ return 1;
5811
+ const mostRecent = memory.accessed_at || memory.updated_at;
5812
+ if (!mostRecent)
5813
+ return 0;
5814
+ const daysSinceAccess = (Date.now() - Date.parse(mostRecent)) / (1000 * 60 * 60 * 24);
5815
+ return Math.max(0, 1 - daysSinceAccess / 30);
5816
+ }
5817
+ function scoreResults(rows, queryLower, graphBoostedIds) {
4809
5818
  const scored = [];
4810
5819
  for (const row of rows) {
4811
5820
  const memory = parseMemoryRow2(row);
@@ -4813,11 +5822,16 @@ function searchMemories(query, filter, db) {
4813
5822
  if (rawScore === 0)
4814
5823
  continue;
4815
5824
  const weightedScore = rawScore * memory.importance / 10;
5825
+ const recencyBoost = computeRecencyBoost(memory);
5826
+ const accessBoost = Math.min(memory.access_count / 20, 0.2);
5827
+ const graphBoost = graphBoostedIds?.has(memory.id) ? 2 : 0;
5828
+ const finalScore = (weightedScore + graphBoost) * (1 + recencyBoost * 0.3) * (1 + accessBoost);
4816
5829
  const matchType = determineMatchType(memory, queryLower);
4817
5830
  scored.push({
4818
5831
  memory,
4819
- score: weightedScore,
4820
- match_type: matchType
5832
+ score: finalScore,
5833
+ match_type: matchType,
5834
+ highlights: extractHighlights(memory, queryLower)
4821
5835
  });
4822
5836
  }
4823
5837
  scored.sort((a, b) => {
@@ -4825,20 +5839,64 @@ function searchMemories(query, filter, db) {
4825
5839
  return b.score - a.score;
4826
5840
  return b.memory.importance - a.memory.importance;
4827
5841
  });
5842
+ return scored;
5843
+ }
5844
+ function searchMemories(query, filter, db) {
5845
+ const d = db || getDatabase();
5846
+ query = preprocessQuery(query);
5847
+ if (!query)
5848
+ return [];
5849
+ const queryLower = query.toLowerCase();
5850
+ const graphBoostedIds = getGraphBoostedMemoryIds(query, d);
5851
+ let scored;
5852
+ if (hasFts5Table(d)) {
5853
+ const ftsResult = searchWithFts5(d, query, queryLower, filter, graphBoostedIds);
5854
+ if (ftsResult !== null) {
5855
+ scored = ftsResult;
5856
+ } else {
5857
+ scored = searchWithLike(d, query, queryLower, filter, graphBoostedIds);
5858
+ }
5859
+ } else {
5860
+ scored = searchWithLike(d, query, queryLower, filter, graphBoostedIds);
5861
+ }
5862
+ if (scored.length < 3) {
5863
+ const fuzzyResults = searchWithFuzzy(d, query, filter, graphBoostedIds);
5864
+ const seenIds = new Set(scored.map((r) => r.memory.id));
5865
+ for (const fr of fuzzyResults) {
5866
+ if (!seenIds.has(fr.memory.id)) {
5867
+ scored.push(fr);
5868
+ seenIds.add(fr.memory.id);
5869
+ }
5870
+ }
5871
+ scored.sort((a, b) => {
5872
+ if (b.score !== a.score)
5873
+ return b.score - a.score;
5874
+ return b.memory.importance - a.memory.importance;
5875
+ });
5876
+ }
4828
5877
  const offset = filter?.offset ?? 0;
4829
5878
  const limit = filter?.limit ?? scored.length;
4830
- return scored.slice(offset, offset + limit);
5879
+ const finalResults = scored.slice(offset, offset + limit);
5880
+ logSearchQuery(query, scored.length, filter?.agent_id, filter?.project_id, d);
5881
+ return finalResults;
5882
+ }
5883
+ function logSearchQuery(query, resultCount, agentId, projectId, db) {
5884
+ try {
5885
+ const d = db || getDatabase();
5886
+ const id = crypto.randomUUID().slice(0, 8);
5887
+ d.run("INSERT INTO search_history (id, query, result_count, agent_id, project_id) VALUES (?, ?, ?, ?, ?)", [id, query, resultCount, agentId || null, projectId || null]);
5888
+ } catch {}
4831
5889
  }
4832
5890
 
4833
5891
  // src/lib/project-detect.ts
4834
- import { existsSync as existsSync2 } from "fs";
4835
- import { basename, dirname as dirname2, join as join2, resolve as resolve2 } from "path";
5892
+ import { existsSync as existsSync3 } from "fs";
5893
+ import { basename, dirname as dirname3, join as join3, resolve as resolve3 } from "path";
4836
5894
  function findGitRoot2(startDir) {
4837
- let dir = resolve2(startDir);
5895
+ let dir = resolve3(startDir);
4838
5896
  while (true) {
4839
- if (existsSync2(join2(dir, ".git")))
5897
+ if (existsSync3(join3(dir, ".git")))
4840
5898
  return dir;
4841
- const parent = dirname2(dir);
5899
+ const parent = dirname3(dir);
4842
5900
  if (parent === dir)
4843
5901
  break;
4844
5902
  dir = parent;
@@ -4857,7 +5915,7 @@ function detectProject(db) {
4857
5915
  return null;
4858
5916
  }
4859
5917
  const repoName = basename(gitRoot);
4860
- const absPath = resolve2(gitRoot);
5918
+ const absPath = resolve3(gitRoot);
4861
5919
  const existing = getProject(absPath, d);
4862
5920
  if (existing) {
4863
5921
  _cachedProject = existing;
@@ -4868,6 +5926,49 @@ function detectProject(db) {
4868
5926
  return project;
4869
5927
  }
4870
5928
 
5929
+ // src/lib/duration.ts
5930
+ var UNIT_MS = {
5931
+ s: 1000,
5932
+ m: 60000,
5933
+ h: 3600000,
5934
+ d: 86400000,
5935
+ w: 604800000
5936
+ };
5937
+ var DURATION_RE = /^(\d+[smhdw])+$/;
5938
+ var SEGMENT_RE = /(\d+)([smhdw])/g;
5939
+ function parseDuration(input) {
5940
+ if (typeof input === "number")
5941
+ return input;
5942
+ const trimmed = input.trim();
5943
+ if (trimmed === "")
5944
+ throw new Error("Invalid duration: empty string");
5945
+ if (/^\d+$/.test(trimmed)) {
5946
+ return parseInt(trimmed, 10);
5947
+ }
5948
+ if (!DURATION_RE.test(trimmed)) {
5949
+ throw new Error(`Invalid duration format: "${trimmed}". Use combinations of Ns, Nm, Nh, Nd, Nw (e.g. "1d12h", "30m") or plain milliseconds.`);
5950
+ }
5951
+ let total = 0;
5952
+ let match;
5953
+ SEGMENT_RE.lastIndex = 0;
5954
+ while ((match = SEGMENT_RE.exec(trimmed)) !== null) {
5955
+ const value = parseInt(match[1], 10);
5956
+ const unit = match[2];
5957
+ total += value * UNIT_MS[unit];
5958
+ }
5959
+ if (total === 0) {
5960
+ throw new Error(`Invalid duration: "${trimmed}" resolves to 0ms`);
5961
+ }
5962
+ return total;
5963
+ }
5964
+ var FORMAT_UNITS = [
5965
+ ["w", UNIT_MS["w"]],
5966
+ ["d", UNIT_MS["d"]],
5967
+ ["h", UNIT_MS["h"]],
5968
+ ["m", UNIT_MS["m"]],
5969
+ ["s", UNIT_MS["s"]]
5970
+ ];
5971
+
4871
5972
  // src/mcp/index.ts
4872
5973
  var server = new McpServer({
4873
5974
  name: "mementos",
@@ -4947,13 +6048,17 @@ server.tool("memory_save", "Save/upsert a memory. scope: global=all agents, shar
4947
6048
  agent_id: exports_external.string().optional(),
4948
6049
  project_id: exports_external.string().optional(),
4949
6050
  session_id: exports_external.string().optional(),
4950
- ttl_ms: exports_external.coerce.number().optional(),
6051
+ ttl_ms: exports_external.union([exports_external.string(), exports_external.number()]).optional(),
4951
6052
  source: exports_external.enum(["user", "agent", "system", "auto", "imported"]).optional(),
4952
6053
  metadata: exports_external.record(exports_external.unknown()).optional()
4953
6054
  }, async (args) => {
4954
6055
  try {
4955
6056
  ensureAutoProject();
4956
- const memory = createMemory(args);
6057
+ const input = { ...args };
6058
+ if (args.ttl_ms !== undefined) {
6059
+ input.ttl_ms = parseDuration(args.ttl_ms);
6060
+ }
6061
+ const memory = createMemory(input);
4957
6062
  return { content: [{ type: "text", text: `Saved: ${memory.key} (${memory.id.slice(0, 8)})` }] };
4958
6063
  } catch (e) {
4959
6064
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
@@ -5484,49 +6589,646 @@ server.tool("memory_context", "Get memories relevant to current context, filtere
5484
6589
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
5485
6590
  }
5486
6591
  });
5487
- var TOOL_REGISTRY = [
5488
- { name: "memory_save", description: "Save/upsert a memory. scope: global=all agents, shared=project, private=single agent.", category: "memory" },
5489
- { name: "memory_recall", description: "Recall a memory by key. Returns the best matching active memory.", category: "memory" },
5490
- { name: "memory_list", description: "List memories. Default: compact lines. full=true for complete JSON objects.", category: "memory" },
5491
- { name: "memory_update", description: "Update a memory's metadata (value, importance, tags, etc.)", category: "memory" },
5492
- { name: "memory_forget", description: "Delete a memory by ID or key", category: "memory" },
5493
- { name: "memory_search", description: "Search memories by keyword across key, value, summary, and tags", category: "memory" },
5494
- { name: "memory_stats", description: "Get aggregate statistics about stored memories", category: "memory" },
5495
- { name: "memory_export", description: "Export memories as JSON", category: "memory" },
5496
- { name: "memory_import", description: "Import memories from JSON array", category: "memory" },
5497
- { name: "memory_inject", description: "Get memory context for system prompt injection. Selects by scope, importance, recency.", category: "memory" },
5498
- { name: "memory_context", description: "Get memories relevant to current context, filtered by scope/importance/recency.", category: "memory" },
5499
- { name: "register_agent", description: "Register an agent. Idempotent \u2014 same name returns existing agent.", category: "agent" },
5500
- { name: "list_agents", description: "List all registered agents", category: "agent" },
5501
- { name: "get_agent", description: "Get agent details by ID or name", category: "agent" },
5502
- { name: "update_agent", description: "Update agent name, description, role, or metadata.", category: "agent" },
5503
- { name: "register_project", description: "Register a project for memory scoping", category: "project" },
5504
- { name: "list_projects", description: "List all registered projects", category: "project" },
5505
- { name: "bulk_forget", description: "Delete multiple memories by IDs", category: "bulk" },
5506
- { name: "bulk_update", description: "Update multiple memories with the same changes", category: "bulk" },
5507
- { name: "clean_expired", description: "Remove expired memories from the database", category: "utility" },
5508
- { name: "search_tools", description: "Search available tools by name or keyword. Returns names only.", category: "meta" },
5509
- { name: "describe_tools", description: "Get full schemas for specific tools by name.", category: "meta" }
5510
- ];
6592
+ function resolveEntityParam(nameOrId, type) {
6593
+ const byName = getEntityByName(nameOrId, type);
6594
+ if (byName)
6595
+ return byName;
6596
+ try {
6597
+ return getEntity(nameOrId);
6598
+ } catch {}
6599
+ const db = getDatabase();
6600
+ const id = resolvePartialId(db, "entities", nameOrId);
6601
+ if (id)
6602
+ return getEntity(id);
6603
+ throw new Error(`Entity not found: ${nameOrId}`);
6604
+ }
6605
+ server.tool("entity_create", "Create a knowledge graph entity (person, project, tool, concept, file, api, pattern, organization).", {
6606
+ name: exports_external.string(),
6607
+ type: exports_external.enum(["person", "project", "tool", "concept", "file", "api", "pattern", "organization"]),
6608
+ description: exports_external.string().optional(),
6609
+ project_id: exports_external.string().optional()
6610
+ }, async (args) => {
6611
+ try {
6612
+ const entity = createEntity(args);
6613
+ return { content: [{ type: "text", text: `Entity: ${entity.name} [${entity.type}] (${entity.id.slice(0, 8)})` }] };
6614
+ } catch (e) {
6615
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
6616
+ }
6617
+ });
6618
+ server.tool("entity_get", "Get entity details by name or ID, including relations summary and memory count.", {
6619
+ name_or_id: exports_external.string(),
6620
+ type: exports_external.enum(["person", "project", "tool", "concept", "file", "api", "pattern", "organization"]).optional()
6621
+ }, async (args) => {
6622
+ try {
6623
+ const entity = resolveEntityParam(args.name_or_id, args.type);
6624
+ const relations = listRelations({ entity_id: entity.id });
6625
+ const memories = getMemoriesForEntity(entity.id);
6626
+ const lines = [
6627
+ `ID: ${entity.id}`,
6628
+ `Name: ${entity.name}`,
6629
+ `Type: ${entity.type}`
6630
+ ];
6631
+ if (entity.description)
6632
+ lines.push(`Description: ${entity.description}`);
6633
+ if (entity.project_id)
6634
+ lines.push(`Project: ${entity.project_id}`);
6635
+ lines.push(`Relations: ${relations.length}`);
6636
+ lines.push(`Memories: ${memories.length}`);
6637
+ lines.push(`Created: ${entity.created_at}`);
6638
+ lines.push(`Updated: ${entity.updated_at}`);
6639
+ return { content: [{ type: "text", text: lines.join(`
6640
+ `) }] };
6641
+ } catch (e) {
6642
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
6643
+ }
6644
+ });
6645
+ server.tool("entity_list", "List entities. Optional filters: type, project_id, search, limit.", {
6646
+ type: exports_external.enum(["person", "project", "tool", "concept", "file", "api", "pattern", "organization"]).optional(),
6647
+ project_id: exports_external.string().optional(),
6648
+ search: exports_external.string().optional(),
6649
+ limit: exports_external.coerce.number().optional()
6650
+ }, async (args) => {
6651
+ try {
6652
+ const entities = listEntities({ ...args, limit: args.limit || 50 });
6653
+ if (entities.length === 0) {
6654
+ return { content: [{ type: "text", text: "No entities found." }] };
6655
+ }
6656
+ const lines = entities.map((e) => `${e.id.slice(0, 8)} | ${e.type} | ${e.name}`);
6657
+ return { content: [{ type: "text", text: `${entities.length} entit${entities.length === 1 ? "y" : "ies"}:
6658
+ ${lines.join(`
6659
+ `)}` }] };
6660
+ } catch (e) {
6661
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
6662
+ }
6663
+ });
6664
+ server.tool("entity_delete", "Delete an entity by name or ID.", {
6665
+ name_or_id: exports_external.string()
6666
+ }, async (args) => {
6667
+ try {
6668
+ const entity = resolveEntityParam(args.name_or_id);
6669
+ deleteEntity(entity.id);
6670
+ return { content: [{ type: "text", text: `Deleted: ${entity.name} [${entity.type}] (${entity.id.slice(0, 8)})` }] };
6671
+ } catch (e) {
6672
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
6673
+ }
6674
+ });
6675
+ server.tool("entity_merge", "Merge source entity into target. Moves all relations and memory links.", {
6676
+ source: exports_external.string(),
6677
+ target: exports_external.string()
6678
+ }, async (args) => {
6679
+ try {
6680
+ const sourceEntity = resolveEntityParam(args.source);
6681
+ const targetEntity = resolveEntityParam(args.target);
6682
+ const merged = mergeEntities(sourceEntity.id, targetEntity.id);
6683
+ return { content: [{ type: "text", text: `Merged: ${sourceEntity.name} \u2192 ${merged.name} [${merged.type}] (${merged.id.slice(0, 8)})` }] };
6684
+ } catch (e) {
6685
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
6686
+ }
6687
+ });
6688
+ server.tool("entity_link", "Link an entity to a memory with a role (subject, object, or context).", {
6689
+ entity_name_or_id: exports_external.string(),
6690
+ memory_id: exports_external.string(),
6691
+ role: exports_external.enum(["subject", "object", "context"]).optional()
6692
+ }, async (args) => {
6693
+ try {
6694
+ const entity = resolveEntityParam(args.entity_name_or_id);
6695
+ const memoryId = resolveId(args.memory_id);
6696
+ const link = linkEntityToMemory(entity.id, memoryId, args.role || "context");
6697
+ return { content: [{ type: "text", text: `Linked: ${entity.name} \u2192 memory ${memoryId.slice(0, 8)} [${link.role}]` }] };
6698
+ } catch (e) {
6699
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
6700
+ }
6701
+ });
6702
+ server.tool("relation_create", "Create a relation between two entities (uses, knows, depends_on, created_by, related_to, contradicts, part_of, implements).", {
6703
+ source_entity: exports_external.string(),
6704
+ target_entity: exports_external.string(),
6705
+ relation_type: exports_external.enum(["uses", "knows", "depends_on", "created_by", "related_to", "contradicts", "part_of", "implements"]),
6706
+ weight: exports_external.coerce.number().optional()
6707
+ }, async (args) => {
6708
+ try {
6709
+ const source = resolveEntityParam(args.source_entity);
6710
+ const target = resolveEntityParam(args.target_entity);
6711
+ const relation = createRelation({
6712
+ source_entity_id: source.id,
6713
+ target_entity_id: target.id,
6714
+ relation_type: args.relation_type,
6715
+ weight: args.weight
6716
+ });
6717
+ return { content: [{ type: "text", text: `Relation: ${source.name} \u2014[${relation.relation_type}]\u2192 ${target.name} (${relation.id.slice(0, 8)})` }] };
6718
+ } catch (e) {
6719
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
6720
+ }
6721
+ });
6722
+ server.tool("relation_list", "List relations for an entity. Filter by type and direction (outgoing, incoming, both).", {
6723
+ entity_name_or_id: exports_external.string(),
6724
+ relation_type: exports_external.enum(["uses", "knows", "depends_on", "created_by", "related_to", "contradicts", "part_of", "implements"]).optional(),
6725
+ direction: exports_external.enum(["outgoing", "incoming", "both"]).optional()
6726
+ }, async (args) => {
6727
+ try {
6728
+ const entity = resolveEntityParam(args.entity_name_or_id);
6729
+ const relations = listRelations({
6730
+ entity_id: entity.id,
6731
+ relation_type: args.relation_type,
6732
+ direction: args.direction || "both"
6733
+ });
6734
+ if (relations.length === 0) {
6735
+ return { content: [{ type: "text", text: `No relations found for: ${entity.name}` }] };
6736
+ }
6737
+ const lines = relations.map((r) => `${r.id.slice(0, 8)} | ${r.source_entity_id.slice(0, 8)} \u2014[${r.relation_type}]\u2192 ${r.target_entity_id.slice(0, 8)} (w:${r.weight})`);
6738
+ return { content: [{ type: "text", text: `${relations.length} relation(s) for ${entity.name}:
6739
+ ${lines.join(`
6740
+ `)}` }] };
6741
+ } catch (e) {
6742
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
6743
+ }
6744
+ });
6745
+ server.tool("relation_delete", "Delete a relation by ID.", {
6746
+ id: exports_external.string()
6747
+ }, async (args) => {
6748
+ try {
6749
+ const id = resolveId(args.id, "relations");
6750
+ deleteRelation(id);
6751
+ return { content: [{ type: "text", text: `Relation ${id.slice(0, 8)} deleted.` }] };
6752
+ } catch (e) {
6753
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
6754
+ }
6755
+ });
6756
+ server.tool("graph_query", "Traverse the knowledge graph from an entity up to N hops. Returns entities and relations.", {
6757
+ entity_name_or_id: exports_external.string(),
6758
+ depth: exports_external.coerce.number().optional()
6759
+ }, async (args) => {
6760
+ try {
6761
+ const entity = resolveEntityParam(args.entity_name_or_id);
6762
+ const depth = args.depth ?? 2;
6763
+ const graph = getEntityGraph(entity.id, depth);
6764
+ if (graph.entities.length === 0) {
6765
+ return { content: [{ type: "text", text: `No graph found for: ${entity.name}` }] };
6766
+ }
6767
+ const entityLines = graph.entities.map((e) => ` ${e.id.slice(0, 8)} | ${e.type} | ${e.name}`);
6768
+ const relationLines = graph.relations.map((r) => ` ${r.source_entity_id.slice(0, 8)} \u2014[${r.relation_type}]\u2192 ${r.target_entity_id.slice(0, 8)}`);
6769
+ const lines = [
6770
+ `Graph for ${entity.name} (depth ${depth}):`,
6771
+ `Entities (${graph.entities.length}):`,
6772
+ ...entityLines
6773
+ ];
6774
+ if (relationLines.length > 0) {
6775
+ lines.push(`Relations (${graph.relations.length}):`, ...relationLines);
6776
+ }
6777
+ return { content: [{ type: "text", text: lines.join(`
6778
+ `) }] };
6779
+ } catch (e) {
6780
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
6781
+ }
6782
+ });
6783
+ server.tool("graph_path", "Find shortest path between two entities in the knowledge graph.", {
6784
+ from_entity: exports_external.string(),
6785
+ to_entity: exports_external.string(),
6786
+ max_depth: exports_external.coerce.number().optional()
6787
+ }, async (args) => {
6788
+ try {
6789
+ const from = resolveEntityParam(args.from_entity);
6790
+ const to = resolveEntityParam(args.to_entity);
6791
+ const maxDepth = args.max_depth ?? 5;
6792
+ const path = findPath(from.id, to.id, maxDepth);
6793
+ if (!path || path.length === 0) {
6794
+ return { content: [{ type: "text", text: `No path found: ${from.name} \u2192 ${to.name} (max depth ${maxDepth})` }] };
6795
+ }
6796
+ const pathStr = path.map((e) => e.name).join(" \u2192 ");
6797
+ return { content: [{ type: "text", text: `Path: ${pathStr}` }] };
6798
+ } catch (e) {
6799
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
6800
+ }
6801
+ });
6802
+ server.tool("graph_stats", "Get entity and relation counts by type.", {}, async () => {
6803
+ try {
6804
+ const db = getDatabase();
6805
+ const entityTotal = db.query("SELECT COUNT(*) as c FROM entities").get().c;
6806
+ const byType = db.query("SELECT type, COUNT(*) as c FROM entities GROUP BY type").all();
6807
+ const relationTotal = db.query("SELECT COUNT(*) as c FROM relations").get().c;
6808
+ const byRelType = db.query("SELECT relation_type, COUNT(*) as c FROM relations GROUP BY relation_type").all();
6809
+ const linkTotal = db.query("SELECT COUNT(*) as c FROM entity_memories").get().c;
6810
+ const lines = [
6811
+ `Entities: ${entityTotal}`
6812
+ ];
6813
+ if (byType.length > 0) {
6814
+ lines.push(` By type: ${byType.map((r) => `${r.type}=${r.c}`).join(", ")}`);
6815
+ }
6816
+ lines.push(`Relations: ${relationTotal}`);
6817
+ if (byRelType.length > 0) {
6818
+ lines.push(` By type: ${byRelType.map((r) => `${r.relation_type}=${r.c}`).join(", ")}`);
6819
+ }
6820
+ lines.push(`Entity-memory links: ${linkTotal}`);
6821
+ return { content: [{ type: "text", text: lines.join(`
6822
+ `) }] };
6823
+ } catch (e) {
6824
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
6825
+ }
6826
+ });
6827
+ var FULL_SCHEMAS = {
6828
+ memory_save: {
6829
+ description: "Save/upsert a memory. Creates new or merges with existing key.",
6830
+ category: "memory",
6831
+ params: {
6832
+ key: { type: "string", description: "Unique key for the memory (kebab-case recommended)", required: true },
6833
+ value: { type: "string", description: "The memory content", required: true },
6834
+ scope: { type: "string", description: "Visibility: global=all agents, shared=project, private=single agent", enum: ["global", "shared", "private"] },
6835
+ category: { type: "string", description: "Memory type", enum: ["preference", "fact", "knowledge", "history"] },
6836
+ importance: { type: "number", description: "Priority 1-10 (10=critical)" },
6837
+ tags: { type: "array", description: "Searchable tags", items: { type: "string" } },
6838
+ summary: { type: "string", description: "Short summary for display" },
6839
+ agent_id: { type: "string", description: "Agent UUID to scope this memory to" },
6840
+ project_id: { type: "string", description: "Project UUID to scope this memory to" },
6841
+ session_id: { type: "string", description: "Session UUID" },
6842
+ ttl_ms: { type: "string|number", description: "Time-to-live e.g. '7d', '2h', or ms integer" },
6843
+ source: { type: "string", description: "Origin of the memory", enum: ["user", "agent", "system", "auto", "imported"] },
6844
+ metadata: { type: "object", description: "Arbitrary JSON metadata" }
6845
+ },
6846
+ example: '{"key":"preferred-language","value":"TypeScript","scope":"global","importance":8,"tags":["language","preference"]}'
6847
+ },
6848
+ memory_recall: {
6849
+ description: "Recall a memory by exact key. Falls back to fuzzy search if no exact match.",
6850
+ category: "memory",
6851
+ params: {
6852
+ key: { type: "string", description: "Key to look up", required: true },
6853
+ scope: { type: "string", description: "Scope filter", enum: ["global", "shared", "private"] },
6854
+ agent_id: { type: "string", description: "Agent UUID filter" },
6855
+ project_id: { type: "string", description: "Project UUID filter" },
6856
+ session_id: { type: "string", description: "Session UUID filter" }
6857
+ },
6858
+ example: '{"key":"preferred-language","scope":"global"}'
6859
+ },
6860
+ memory_list: {
6861
+ description: "List memories with optional filters. Returns compact lines by default.",
6862
+ category: "memory",
6863
+ params: {
6864
+ scope: { type: "string", description: "Scope filter", enum: ["global", "shared", "private"] },
6865
+ category: { type: "string", description: "Category filter", enum: ["preference", "fact", "knowledge", "history"] },
6866
+ tags: { type: "array", description: "Filter by tags (AND logic)", items: { type: "string" } },
6867
+ min_importance: { type: "number", description: "Minimum importance threshold" },
6868
+ pinned: { type: "boolean", description: "Filter to pinned memories only" },
6869
+ agent_id: { type: "string", description: "Agent UUID filter" },
6870
+ project_id: { type: "string", description: "Project UUID filter" },
6871
+ session_id: { type: "string", description: "Session UUID filter" },
6872
+ status: { type: "string", description: "Memory status filter", enum: ["active", "archived", "expired"] },
6873
+ limit: { type: "number", description: "Max results (default 50)" },
6874
+ offset: { type: "number", description: "Pagination offset" },
6875
+ full: { type: "boolean", description: "Return full JSON objects instead of compact lines" },
6876
+ fields: { type: "array", description: "Fields to include in full mode", items: { type: "string" } }
6877
+ },
6878
+ example: '{"scope":"global","min_importance":7,"limit":20}'
6879
+ },
6880
+ memory_update: {
6881
+ description: "Update a memory's fields. Requires current version for optimistic concurrency.",
6882
+ category: "memory",
6883
+ params: {
6884
+ id: { type: "string", description: "Memory ID (partial OK)", required: true },
6885
+ version: { type: "number", description: "Current version (for conflict detection)", required: true },
6886
+ value: { type: "string", description: "New value" },
6887
+ category: { type: "string", description: "New category", enum: ["preference", "fact", "knowledge", "history"] },
6888
+ scope: { type: "string", description: "New scope", enum: ["global", "shared", "private"] },
6889
+ importance: { type: "number", description: "New importance 1-10" },
6890
+ tags: { type: "array", description: "New tags (replaces all)", items: { type: "string" } },
6891
+ summary: { type: "string", description: "New summary (null to clear)" },
6892
+ pinned: { type: "boolean", description: "Pin/unpin the memory" },
6893
+ status: { type: "string", description: "New status", enum: ["active", "archived", "expired"] },
6894
+ metadata: { type: "object", description: "New metadata (replaces existing)" },
6895
+ expires_at: { type: "string", description: "New expiry ISO timestamp (null to clear)" }
6896
+ },
6897
+ example: '{"id":"abc123","version":1,"importance":9,"tags":["correction","important"]}'
6898
+ },
6899
+ memory_forget: {
6900
+ description: "Delete a memory by ID or key.",
6901
+ category: "memory",
6902
+ params: {
6903
+ id: { type: "string", description: "Memory ID (partial OK)" },
6904
+ key: { type: "string", description: "Memory key" },
6905
+ scope: { type: "string", description: "Scope for key lookup", enum: ["global", "shared", "private"] },
6906
+ agent_id: { type: "string", description: "Agent UUID for key lookup" },
6907
+ project_id: { type: "string", description: "Project UUID for key lookup" }
6908
+ },
6909
+ example: '{"key":"old-preference","scope":"global"}'
6910
+ },
6911
+ memory_search: {
6912
+ description: "Full-text search across key, value, summary, and tags.",
6913
+ category: "memory",
6914
+ params: {
6915
+ query: { type: "string", description: "Search query", required: true },
6916
+ scope: { type: "string", description: "Scope filter", enum: ["global", "shared", "private"] },
6917
+ category: { type: "string", description: "Category filter", enum: ["preference", "fact", "knowledge", "history"] },
6918
+ tags: { type: "array", description: "Tag filter", items: { type: "string" } },
6919
+ agent_id: { type: "string", description: "Agent UUID filter" },
6920
+ project_id: { type: "string", description: "Project UUID filter" },
6921
+ limit: { type: "number", description: "Max results (default 20)" }
6922
+ },
6923
+ example: '{"query":"typescript","scope":"global","limit":10}'
6924
+ },
6925
+ memory_stats: {
6926
+ description: "Aggregate statistics: total, by scope, by category, pinned, expired counts.",
6927
+ category: "memory",
6928
+ params: {},
6929
+ example: "{}"
6930
+ },
6931
+ memory_export: {
6932
+ description: "Export memories as a JSON array.",
6933
+ category: "memory",
6934
+ params: {
6935
+ scope: { type: "string", description: "Scope filter", enum: ["global", "shared", "private"] },
6936
+ category: { type: "string", description: "Category filter", enum: ["preference", "fact", "knowledge", "history"] },
6937
+ agent_id: { type: "string", description: "Agent UUID filter" },
6938
+ project_id: { type: "string", description: "Project UUID filter" }
6939
+ },
6940
+ example: '{"scope":"global"}'
6941
+ },
6942
+ memory_import: {
6943
+ description: "Import memories from a JSON array. Merges by key by default.",
6944
+ category: "memory",
6945
+ params: {
6946
+ memories: { type: "array", description: "Array of memory objects with key+value (required), plus optional fields", required: true, items: { type: "object" } },
6947
+ overwrite: { type: "boolean", description: "false=create-only (skip existing keys), default=merge" }
6948
+ },
6949
+ example: '{"memories":[{"key":"foo","value":"bar","scope":"global","importance":7}]}'
6950
+ },
6951
+ memory_inject: {
6952
+ description: "Get formatted memory context for system prompt injection. Respects token budget.",
6953
+ category: "memory",
6954
+ params: {
6955
+ agent_id: { type: "string", description: "Agent UUID to include private memories" },
6956
+ project_id: { type: "string", description: "Project UUID to include shared memories" },
6957
+ session_id: { type: "string", description: "Session UUID" },
6958
+ max_tokens: { type: "number", description: "Approximate token budget (default 500)" },
6959
+ categories: { type: "array", description: "Categories to include (default: preference, fact, knowledge)", items: { type: "string", enum: ["preference", "fact", "knowledge", "history"] } },
6960
+ min_importance: { type: "number", description: "Minimum importance (default 3)" },
6961
+ raw: { type: "boolean", description: "true=plain lines only, false=wrapped in <agent-memories> tags" }
6962
+ },
6963
+ example: '{"project_id":"proj-uuid","max_tokens":300,"min_importance":5}'
6964
+ },
6965
+ memory_context: {
6966
+ description: "Get active memories for the current context (agent/project/scope).",
6967
+ category: "memory",
6968
+ params: {
6969
+ agent_id: { type: "string", description: "Agent UUID filter" },
6970
+ project_id: { type: "string", description: "Project UUID filter" },
6971
+ scope: { type: "string", description: "Scope filter", enum: ["global", "shared", "private"] },
6972
+ limit: { type: "number", description: "Max results (default 30)" }
6973
+ },
6974
+ example: '{"project_id":"proj-uuid","scope":"shared","limit":20}'
6975
+ },
6976
+ register_agent: {
6977
+ description: "Register an agent. Idempotent \u2014 same name returns existing agent.",
6978
+ category: "agent",
6979
+ params: {
6980
+ name: { type: "string", description: "Agent name (e.g. 'maximus', 'cassius')", required: true },
6981
+ description: { type: "string", description: "Agent description" },
6982
+ role: { type: "string", description: "Agent role (default: 'agent')" }
6983
+ },
6984
+ example: '{"name":"maximus","role":"developer"}'
6985
+ },
6986
+ list_agents: {
6987
+ description: "List all registered agents with IDs, names, roles, and last-seen timestamps.",
6988
+ category: "agent",
6989
+ params: {},
6990
+ example: "{}"
6991
+ },
6992
+ get_agent: {
6993
+ description: "Get agent details by UUID or name.",
6994
+ category: "agent",
6995
+ params: {
6996
+ id: { type: "string", description: "Agent UUID or name", required: true }
6997
+ },
6998
+ example: '{"id":"maximus"}'
6999
+ },
7000
+ update_agent: {
7001
+ description: "Update agent name, description, role, or metadata.",
7002
+ category: "agent",
7003
+ params: {
7004
+ id: { type: "string", description: "Agent UUID", required: true },
7005
+ name: { type: "string", description: "New name" },
7006
+ description: { type: "string", description: "New description" },
7007
+ role: { type: "string", description: "New role" },
7008
+ metadata: { type: "object", description: "New metadata" }
7009
+ },
7010
+ example: '{"id":"agent-uuid","role":"senior-developer"}'
7011
+ },
7012
+ register_project: {
7013
+ description: "Register a project for memory scoping. Idempotent by name.",
7014
+ category: "project",
7015
+ params: {
7016
+ name: { type: "string", description: "Project name (use git repo name)", required: true },
7017
+ path: { type: "string", description: "Absolute path to project root", required: true },
7018
+ description: { type: "string", description: "Project description" },
7019
+ memory_prefix: { type: "string", description: "Key prefix for project memories" }
7020
+ },
7021
+ example: '{"name":"open-mementos","path":"/Users/hasna/Workspace/hasna/opensource/opensourcedev/open-mementos"}'
7022
+ },
7023
+ list_projects: {
7024
+ description: "List all registered projects with IDs, names, and paths.",
7025
+ category: "project",
7026
+ params: {},
7027
+ example: "{}"
7028
+ },
7029
+ bulk_forget: {
7030
+ description: "Delete multiple memories by IDs in one call.",
7031
+ category: "bulk",
7032
+ params: {
7033
+ ids: { type: "array", description: "Array of memory IDs (partials OK)", required: true, items: { type: "string" } }
7034
+ },
7035
+ example: '{"ids":["abc123","def456"]}'
7036
+ },
7037
+ bulk_update: {
7038
+ description: "Apply the same field updates to multiple memories.",
7039
+ category: "bulk",
7040
+ params: {
7041
+ ids: { type: "array", description: "Array of memory IDs (partials OK)", required: true, items: { type: "string" } },
7042
+ importance: { type: "number", description: "New importance 1-10" },
7043
+ tags: { type: "array", description: "New tags (replaces all)", items: { type: "string" } },
7044
+ pinned: { type: "boolean", description: "Pin/unpin" },
7045
+ category: { type: "string", description: "New category", enum: ["preference", "fact", "knowledge", "history"] },
7046
+ status: { type: "string", description: "New status", enum: ["active", "archived", "expired"] }
7047
+ },
7048
+ example: '{"ids":["abc123","def456"],"importance":9,"tags":["important"]}'
7049
+ },
7050
+ clean_expired: {
7051
+ description: "Remove expired memories from the database. Returns count of removed entries.",
7052
+ category: "utility",
7053
+ params: {},
7054
+ example: "{}"
7055
+ },
7056
+ entity_create: {
7057
+ description: "Create a knowledge graph entity.",
7058
+ category: "graph",
7059
+ params: {
7060
+ name: { type: "string", description: "Entity name", required: true },
7061
+ type: { type: "string", description: "Entity type", required: true, enum: ["person", "project", "tool", "concept", "file", "api", "pattern", "organization"] },
7062
+ description: { type: "string", description: "Entity description" },
7063
+ project_id: { type: "string", description: "Project UUID to scope this entity" }
7064
+ },
7065
+ example: '{"name":"TypeScript","type":"tool","description":"Typed superset of JavaScript"}'
7066
+ },
7067
+ entity_get: {
7068
+ description: "Get entity details including relations summary and linked memory count.",
7069
+ category: "graph",
7070
+ params: {
7071
+ name_or_id: { type: "string", description: "Entity name or ID (partial OK)", required: true },
7072
+ type: { type: "string", description: "Type hint for name disambiguation", enum: ["person", "project", "tool", "concept", "file", "api", "pattern", "organization"] }
7073
+ },
7074
+ example: '{"name_or_id":"TypeScript"}'
7075
+ },
7076
+ entity_list: {
7077
+ description: "List entities with optional type, project, and search filters.",
7078
+ category: "graph",
7079
+ params: {
7080
+ type: { type: "string", description: "Type filter", enum: ["person", "project", "tool", "concept", "file", "api", "pattern", "organization"] },
7081
+ project_id: { type: "string", description: "Project UUID filter" },
7082
+ search: { type: "string", description: "Name search string" },
7083
+ limit: { type: "number", description: "Max results (default 50)" }
7084
+ },
7085
+ example: '{"type":"tool","limit":20}'
7086
+ },
7087
+ entity_delete: {
7088
+ description: "Delete an entity and all its relations.",
7089
+ category: "graph",
7090
+ params: {
7091
+ name_or_id: { type: "string", description: "Entity name or ID (partial OK)", required: true }
7092
+ },
7093
+ example: '{"name_or_id":"OldEntity"}'
7094
+ },
7095
+ entity_merge: {
7096
+ description: "Merge source entity into target \u2014 moves all relations and memory links.",
7097
+ category: "graph",
7098
+ params: {
7099
+ source: { type: "string", description: "Source entity name or ID (will be deleted)", required: true },
7100
+ target: { type: "string", description: "Target entity name or ID (will be kept)", required: true }
7101
+ },
7102
+ example: '{"source":"OldName","target":"NewName"}'
7103
+ },
7104
+ entity_link: {
7105
+ description: "Link an entity to a memory with a semantic role.",
7106
+ category: "graph",
7107
+ params: {
7108
+ entity_name_or_id: { type: "string", description: "Entity name or ID", required: true },
7109
+ memory_id: { type: "string", description: "Memory ID (partial OK)", required: true },
7110
+ role: { type: "string", description: "Semantic role (default: context)", enum: ["subject", "object", "context"] }
7111
+ },
7112
+ example: '{"entity_name_or_id":"TypeScript","memory_id":"abc123","role":"subject"}'
7113
+ },
7114
+ relation_create: {
7115
+ description: "Create a typed relation between two entities.",
7116
+ category: "graph",
7117
+ params: {
7118
+ source_entity: { type: "string", description: "Source entity name or ID", required: true },
7119
+ target_entity: { type: "string", description: "Target entity name or ID", required: true },
7120
+ relation_type: { type: "string", description: "Relation type", required: true, enum: ["uses", "knows", "depends_on", "created_by", "related_to", "contradicts", "part_of", "implements"] },
7121
+ weight: { type: "number", description: "Relation weight 0-1 (default 1.0)" }
7122
+ },
7123
+ example: '{"source_entity":"MyApp","target_entity":"TypeScript","relation_type":"uses"}'
7124
+ },
7125
+ relation_list: {
7126
+ description: "List relations for an entity, with optional type and direction filters.",
7127
+ category: "graph",
7128
+ params: {
7129
+ entity_name_or_id: { type: "string", description: "Entity name or ID", required: true },
7130
+ relation_type: { type: "string", description: "Type filter", enum: ["uses", "knows", "depends_on", "created_by", "related_to", "contradicts", "part_of", "implements"] },
7131
+ direction: { type: "string", description: "Direction filter (default: both)", enum: ["outgoing", "incoming", "both"] }
7132
+ },
7133
+ example: '{"entity_name_or_id":"MyApp","direction":"outgoing"}'
7134
+ },
7135
+ relation_delete: {
7136
+ description: "Delete a relation by ID.",
7137
+ category: "graph",
7138
+ params: {
7139
+ id: { type: "string", description: "Relation ID (partial OK)", required: true }
7140
+ },
7141
+ example: '{"id":"rel-abc123"}'
7142
+ },
7143
+ graph_query: {
7144
+ description: "Traverse the knowledge graph from an entity up to N hops. Returns entities and relations.",
7145
+ category: "graph",
7146
+ params: {
7147
+ entity_name_or_id: { type: "string", description: "Starting entity name or ID", required: true },
7148
+ depth: { type: "number", description: "Max traversal depth (default 2)" }
7149
+ },
7150
+ example: '{"entity_name_or_id":"MyApp","depth":3}'
7151
+ },
7152
+ graph_path: {
7153
+ description: "Find the shortest path between two entities in the knowledge graph.",
7154
+ category: "graph",
7155
+ params: {
7156
+ from_entity: { type: "string", description: "Starting entity name or ID", required: true },
7157
+ to_entity: { type: "string", description: "Target entity name or ID", required: true },
7158
+ max_depth: { type: "number", description: "Max search depth (default 5)" }
7159
+ },
7160
+ example: '{"from_entity":"Agent","to_entity":"Database","max_depth":4}'
7161
+ },
7162
+ graph_stats: {
7163
+ description: "Get entity and relation counts broken down by type.",
7164
+ category: "graph",
7165
+ params: {},
7166
+ example: "{}"
7167
+ },
7168
+ search_tools: {
7169
+ description: "Search available tools by name or keyword. Returns matching tool names and categories.",
7170
+ category: "meta",
7171
+ params: {
7172
+ query: { type: "string", description: "Search keyword (matches tool name or description)", required: true },
7173
+ category: { type: "string", description: "Category filter", enum: ["memory", "agent", "project", "bulk", "utility", "graph", "meta"] }
7174
+ },
7175
+ example: '{"query":"memory","category":"memory"}'
7176
+ },
7177
+ describe_tools: {
7178
+ description: "Get full parameter schemas and examples for specific tools. Omit names to list all tools.",
7179
+ category: "meta",
7180
+ params: {
7181
+ names: { type: "array", description: "Tool names to describe (omit for all tools)", items: { type: "string" } }
7182
+ },
7183
+ example: '{"names":["memory_save","memory_recall"]}'
7184
+ }
7185
+ };
7186
+ var TOOL_REGISTRY = Object.entries(FULL_SCHEMAS).map(([name, schema]) => ({
7187
+ name,
7188
+ description: schema.description,
7189
+ category: schema.category
7190
+ }));
5511
7191
  server.tool("search_tools", "Search available tools by name or keyword. Returns names only.", {
5512
7192
  query: exports_external.string(),
5513
- category: exports_external.enum(["memory", "agent", "project", "bulk", "utility", "meta"]).optional()
7193
+ category: exports_external.enum(["memory", "agent", "project", "bulk", "utility", "graph", "meta"]).optional()
5514
7194
  }, async (args) => {
5515
7195
  const q = args.query.toLowerCase();
5516
7196
  const results = TOOL_REGISTRY.filter((t) => (!args.category || t.category === args.category) && (t.name.includes(q) || t.description.toLowerCase().includes(q)));
5517
7197
  if (results.length === 0)
5518
7198
  return { content: [{ type: "text", text: "No tools found." }] };
5519
- return { content: [{ type: "text", text: results.map((t) => `${t.name} [${t.category}]`).join(`
7199
+ return { content: [{ type: "text", text: results.map((t) => `${t.name} [${t.category}]: ${t.description}`).join(`
5520
7200
  `) }] };
5521
7201
  });
5522
- server.tool("describe_tools", "Get full schemas for specific tools by name.", {
5523
- names: exports_external.array(exports_external.string())
7202
+ server.tool("describe_tools", "Get full parameter schemas and examples for tools. Omit names to list all tools.", {
7203
+ names: exports_external.array(exports_external.string()).optional()
5524
7204
  }, async (args) => {
5525
- const found = TOOL_REGISTRY.filter((t) => args.names.includes(t.name));
5526
- if (found.length === 0)
7205
+ const targets = args.names && args.names.length > 0 ? args.names : Object.keys(FULL_SCHEMAS);
7206
+ const results = targets.filter((name) => (name in FULL_SCHEMAS)).map((name) => {
7207
+ const schema = FULL_SCHEMAS[name];
7208
+ const paramLines = Object.entries(schema.params).map(([pname, p]) => {
7209
+ const req = p.required ? " [required]" : "";
7210
+ const enumStr = p.enum ? ` (${p.enum.join("|")})` : "";
7211
+ return ` ${pname}${req}: ${p.type}${enumStr} \u2014 ${p.description}`;
7212
+ });
7213
+ const lines = [
7214
+ `### ${name} [${schema.category}]`,
7215
+ schema.description
7216
+ ];
7217
+ if (paramLines.length > 0) {
7218
+ lines.push("Params:", ...paramLines);
7219
+ } else {
7220
+ lines.push("Params: none");
7221
+ }
7222
+ if (schema.example)
7223
+ lines.push(`Example: ${schema.example}`);
7224
+ return lines.join(`
7225
+ `);
7226
+ });
7227
+ if (results.length === 0) {
5527
7228
  return { content: [{ type: "text", text: "No matching tools." }] };
5528
- const lines = found.map((t) => `**${t.name}** [${t.category}]: ${t.description}`);
5529
- return { content: [{ type: "text", text: lines.join(`
7229
+ }
7230
+ return { content: [{ type: "text", text: results.join(`
7231
+
5530
7232
  `) }] };
5531
7233
  });
5532
7234
  server.resource("memories", "mementos://memories", { description: "All active memories", mimeType: "application/json" }, async () => {