@hasna/mementos 0.3.9 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,146 +5331,26 @@ 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;
4540
- }
4541
- function touchMemory(id, db) {
4542
- const d = db || getDatabase();
4543
- d.run("UPDATE memories SET access_count = access_count + 1, accessed_at = ? WHERE id = ?", [now(), id]);
4544
- }
4545
- function cleanExpiredMemories(db) {
4546
- const d = db || getDatabase();
4547
- 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]);
4622
- }
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;
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;
4673
5340
  }
4674
- function listProjects(db) {
5341
+ function touchMemory(id, db) {
4675
5342
  const d = db || getDatabase();
4676
- const rows = d.query("SELECT * FROM projects ORDER BY updated_at DESC").all();
4677
- return rows.map(parseProjectRow);
5343
+ d.run("UPDATE memories SET access_count = access_count + 1, accessed_at = ? WHERE id = ?", [now(), id]);
5344
+ }
5345
+ function cleanExpiredMemories(db) {
5346
+ const d = db || getDatabase();
5347
+ const timestamp = now();
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]);
5352
+ }
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,6 +6589,241 @@ 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
  });
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
+ });
5487
6827
  var TOOL_REGISTRY = [
5488
6828
  { name: "memory_save", description: "Save/upsert a memory. scope: global=all agents, shared=project, private=single agent.", category: "memory" },
5489
6829
  { name: "memory_recall", description: "Recall a memory by key. Returns the best matching active memory.", category: "memory" },
@@ -5505,12 +6845,24 @@ var TOOL_REGISTRY = [
5505
6845
  { name: "bulk_forget", description: "Delete multiple memories by IDs", category: "bulk" },
5506
6846
  { name: "bulk_update", description: "Update multiple memories with the same changes", category: "bulk" },
5507
6847
  { name: "clean_expired", description: "Remove expired memories from the database", category: "utility" },
6848
+ { name: "entity_create", description: "Create a knowledge graph entity (person, project, tool, concept, file, api, pattern, organization).", category: "graph" },
6849
+ { name: "entity_get", description: "Get entity details by name or ID, including relations summary and memory count.", category: "graph" },
6850
+ { name: "entity_list", description: "List entities. Optional filters: type, project_id, search, limit.", category: "graph" },
6851
+ { name: "entity_delete", description: "Delete an entity by name or ID.", category: "graph" },
6852
+ { name: "entity_merge", description: "Merge source entity into target. Moves all relations and memory links.", category: "graph" },
6853
+ { name: "entity_link", description: "Link an entity to a memory with a role (subject, object, or context).", category: "graph" },
6854
+ { name: "relation_create", description: "Create a relation between two entities.", category: "graph" },
6855
+ { name: "relation_list", description: "List relations for an entity. Filter by type and direction.", category: "graph" },
6856
+ { name: "relation_delete", description: "Delete a relation by ID.", category: "graph" },
6857
+ { name: "graph_query", description: "Traverse the knowledge graph from an entity up to N hops.", category: "graph" },
6858
+ { name: "graph_path", description: "Find shortest path between two entities.", category: "graph" },
6859
+ { name: "graph_stats", description: "Get entity and relation counts by type.", category: "graph" },
5508
6860
  { name: "search_tools", description: "Search available tools by name or keyword. Returns names only.", category: "meta" },
5509
6861
  { name: "describe_tools", description: "Get full schemas for specific tools by name.", category: "meta" }
5510
6862
  ];
5511
6863
  server.tool("search_tools", "Search available tools by name or keyword. Returns names only.", {
5512
6864
  query: exports_external.string(),
5513
- category: exports_external.enum(["memory", "agent", "project", "bulk", "utility", "meta"]).optional()
6865
+ category: exports_external.enum(["memory", "agent", "project", "bulk", "utility", "graph", "meta"]).optional()
5514
6866
  }, async (args) => {
5515
6867
  const q = args.query.toLowerCase();
5516
6868
  const results = TOOL_REGISTRY.filter((t) => (!args.category || t.category === args.category) && (t.name.includes(q) || t.description.toLowerCase().includes(q)));