@hasna/mementos 0.4.41 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/cli/index.js +2797 -1611
  2. package/dist/db/database.d.ts.map +1 -1
  3. package/dist/db/entities.d.ts.map +1 -1
  4. package/dist/db/memories.d.ts +1 -0
  5. package/dist/db/memories.d.ts.map +1 -1
  6. package/dist/db/relations.d.ts.map +1 -1
  7. package/dist/db/webhook_hooks.d.ts +25 -0
  8. package/dist/db/webhook_hooks.d.ts.map +1 -0
  9. package/dist/index.d.ts +6 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +2187 -1331
  12. package/dist/lib/auto-memory-queue.d.ts +46 -0
  13. package/dist/lib/auto-memory-queue.d.ts.map +1 -0
  14. package/dist/lib/auto-memory.d.ts +18 -0
  15. package/dist/lib/auto-memory.d.ts.map +1 -0
  16. package/dist/lib/built-in-hooks.d.ts +12 -0
  17. package/dist/lib/built-in-hooks.d.ts.map +1 -0
  18. package/dist/lib/dedup.d.ts +33 -0
  19. package/dist/lib/dedup.d.ts.map +1 -0
  20. package/dist/lib/focus.d.ts +58 -0
  21. package/dist/lib/focus.d.ts.map +1 -0
  22. package/dist/lib/hooks.d.ts +50 -0
  23. package/dist/lib/hooks.d.ts.map +1 -0
  24. package/dist/lib/providers/anthropic.d.ts +21 -0
  25. package/dist/lib/providers/anthropic.d.ts.map +1 -0
  26. package/dist/lib/providers/base.d.ts +96 -0
  27. package/dist/lib/providers/base.d.ts.map +1 -0
  28. package/dist/lib/providers/cerebras.d.ts +20 -0
  29. package/dist/lib/providers/cerebras.d.ts.map +1 -0
  30. package/dist/lib/providers/grok.d.ts +19 -0
  31. package/dist/lib/providers/grok.d.ts.map +1 -0
  32. package/dist/lib/providers/index.d.ts +7 -0
  33. package/dist/lib/providers/index.d.ts.map +1 -0
  34. package/dist/lib/providers/openai-compat.d.ts +18 -0
  35. package/dist/lib/providers/openai-compat.d.ts.map +1 -0
  36. package/dist/lib/providers/openai.d.ts +20 -0
  37. package/dist/lib/providers/openai.d.ts.map +1 -0
  38. package/dist/lib/providers/registry.d.ts +38 -0
  39. package/dist/lib/providers/registry.d.ts.map +1 -0
  40. package/dist/lib/search.d.ts.map +1 -1
  41. package/dist/mcp/index.js +6851 -5544
  42. package/dist/server/index.d.ts.map +1 -1
  43. package/dist/server/index.js +2716 -1596
  44. package/dist/types/hooks.d.ts +136 -0
  45. package/dist/types/hooks.d.ts.map +1 -0
  46. package/dist/types/index.d.ts +7 -0
  47. package/dist/types/index.d.ts.map +1 -1
  48. package/package.json +2 -2
@@ -16,62 +16,68 @@ var __toESM = (mod, isNodeMode, target) => {
16
16
  });
17
17
  return to;
18
18
  };
19
+ var __export = (target, all) => {
20
+ for (var name in all)
21
+ __defProp(target, name, {
22
+ get: all[name],
23
+ enumerable: true,
24
+ configurable: true,
25
+ set: (newValue) => all[name] = () => newValue
26
+ });
27
+ };
28
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
19
29
  var __require = import.meta.require;
20
30
 
21
- // src/server/index.ts
22
- import { existsSync as existsSync3 } from "fs";
23
- import { dirname as dirname3, extname, join as join3, resolve as resolve3, sep } from "path";
24
- import { fileURLToPath } from "url";
25
-
26
31
  // src/types/index.ts
27
- class AgentConflictError extends Error {
28
- conflict = true;
29
- existing_id;
30
- existing_name;
31
- last_seen_at;
32
- session_hint;
33
- working_dir;
34
- constructor(opts) {
35
- const msg = `Agent "${opts.existing_name}" is already active (session hint: ${opts.session_hint ?? "unknown"}, last seen ${opts.last_seen_at}). Wait 30 minutes or use a different name.`;
36
- super(msg);
37
- this.name = "AgentConflictError";
38
- this.existing_id = opts.existing_id;
39
- this.existing_name = opts.existing_name;
40
- this.last_seen_at = opts.last_seen_at;
41
- this.session_hint = opts.session_hint;
42
- this.working_dir = opts.working_dir ?? null;
43
- }
44
- }
45
- class EntityNotFoundError extends Error {
46
- constructor(id) {
47
- super(`Entity not found: ${id}`);
48
- this.name = "EntityNotFoundError";
49
- }
50
- }
51
-
52
- class MemoryNotFoundError extends Error {
53
- constructor(id) {
54
- super(`Memory not found: ${id}`);
55
- this.name = "MemoryNotFoundError";
56
- }
57
- }
58
-
59
- class DuplicateMemoryError extends Error {
60
- constructor(key, scope) {
61
- super(`Memory already exists with key "${key}" in scope "${scope}"`);
62
- this.name = "DuplicateMemoryError";
63
- }
64
- }
65
- class VersionConflictError extends Error {
66
- expected;
67
- actual;
68
- constructor(id, expected, actual) {
69
- super(`Version conflict for memory ${id}: expected ${expected}, got ${actual}`);
70
- this.name = "VersionConflictError";
71
- this.expected = expected;
72
- this.actual = actual;
73
- }
74
- }
32
+ var AgentConflictError, EntityNotFoundError, MemoryNotFoundError, DuplicateMemoryError, VersionConflictError;
33
+ var init_types = __esm(() => {
34
+ AgentConflictError = class AgentConflictError extends Error {
35
+ conflict = true;
36
+ existing_id;
37
+ existing_name;
38
+ last_seen_at;
39
+ session_hint;
40
+ working_dir;
41
+ constructor(opts) {
42
+ const msg = `Agent "${opts.existing_name}" is already active (session hint: ${opts.session_hint ?? "unknown"}, last seen ${opts.last_seen_at}). Wait 30 minutes or use a different name.`;
43
+ super(msg);
44
+ this.name = "AgentConflictError";
45
+ this.existing_id = opts.existing_id;
46
+ this.existing_name = opts.existing_name;
47
+ this.last_seen_at = opts.last_seen_at;
48
+ this.session_hint = opts.session_hint;
49
+ this.working_dir = opts.working_dir ?? null;
50
+ }
51
+ };
52
+ EntityNotFoundError = class EntityNotFoundError extends Error {
53
+ constructor(id) {
54
+ super(`Entity not found: ${id}`);
55
+ this.name = "EntityNotFoundError";
56
+ }
57
+ };
58
+ MemoryNotFoundError = class MemoryNotFoundError extends Error {
59
+ constructor(id) {
60
+ super(`Memory not found: ${id}`);
61
+ this.name = "MemoryNotFoundError";
62
+ }
63
+ };
64
+ DuplicateMemoryError = class DuplicateMemoryError extends Error {
65
+ constructor(key, scope) {
66
+ super(`Memory already exists with key "${key}" in scope "${scope}"`);
67
+ this.name = "DuplicateMemoryError";
68
+ }
69
+ };
70
+ VersionConflictError = class VersionConflictError extends Error {
71
+ expected;
72
+ actual;
73
+ constructor(id, expected, actual) {
74
+ super(`Version conflict for memory ${id}: expected ${expected}, got ${actual}`);
75
+ this.name = "VersionConflictError";
76
+ this.expected = expected;
77
+ this.actual = actual;
78
+ }
79
+ };
80
+ });
75
81
 
76
82
  // src/db/database.ts
77
83
  import { Database } from "bun:sqlite";
@@ -130,8 +136,48 @@ function ensureDir(filePath) {
130
136
  mkdirSync(dir, { recursive: true });
131
137
  }
132
138
  }
133
- var MIGRATIONS = [
134
- `
139
+ function getDatabase(dbPath) {
140
+ if (_db)
141
+ return _db;
142
+ const path = dbPath || getDbPath();
143
+ ensureDir(path);
144
+ _db = new Database(path, { create: true });
145
+ _db.run("PRAGMA journal_mode = WAL");
146
+ _db.run("PRAGMA busy_timeout = 5000");
147
+ _db.run("PRAGMA foreign_keys = ON");
148
+ runMigrations(_db);
149
+ return _db;
150
+ }
151
+ function runMigrations(db) {
152
+ try {
153
+ const result = db.query("SELECT MAX(id) as max_id FROM _migrations").get();
154
+ const currentLevel = result?.max_id ?? 0;
155
+ for (let i = currentLevel;i < MIGRATIONS.length; i++) {
156
+ try {
157
+ db.exec(MIGRATIONS[i]);
158
+ } catch {}
159
+ }
160
+ } catch {
161
+ for (const migration of MIGRATIONS) {
162
+ try {
163
+ db.exec(migration);
164
+ } catch {}
165
+ }
166
+ }
167
+ }
168
+ function now() {
169
+ return new Date().toISOString();
170
+ }
171
+ function uuid() {
172
+ return crypto.randomUUID();
173
+ }
174
+ function shortUuid() {
175
+ return crypto.randomUUID().slice(0, 8);
176
+ }
177
+ var MIGRATIONS, _db = null;
178
+ var init_database = __esm(() => {
179
+ MIGRATIONS = [
180
+ `
135
181
  CREATE TABLE IF NOT EXISTS projects (
136
182
  id TEXT PRIMARY KEY,
137
183
  name TEXT NOT NULL,
@@ -218,7 +264,7 @@ var MIGRATIONS = [
218
264
 
219
265
  INSERT OR IGNORE INTO _migrations (id) VALUES (1);
220
266
  `,
221
- `
267
+ `
222
268
  CREATE TABLE IF NOT EXISTS memory_versions (
223
269
  id TEXT PRIMARY KEY,
224
270
  memory_id TEXT NOT NULL,
@@ -240,7 +286,7 @@ var MIGRATIONS = [
240
286
 
241
287
  INSERT OR IGNORE INTO _migrations (id) VALUES (2);
242
288
  `,
243
- `
289
+ `
244
290
  CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
245
291
  key, value, summary,
246
292
  content='memories',
@@ -264,7 +310,7 @@ var MIGRATIONS = [
264
310
 
265
311
  INSERT OR IGNORE INTO _migrations (id) VALUES (3);
266
312
  `,
267
- `
313
+ `
268
314
  CREATE TABLE IF NOT EXISTS search_history (
269
315
  id TEXT PRIMARY KEY,
270
316
  query TEXT NOT NULL,
@@ -278,7 +324,7 @@ var MIGRATIONS = [
278
324
 
279
325
  INSERT OR IGNORE INTO _migrations (id) VALUES (4);
280
326
  `,
281
- `
327
+ `
282
328
  CREATE TABLE IF NOT EXISTS entities (
283
329
  id TEXT PRIMARY KEY,
284
330
  name TEXT NOT NULL,
@@ -325,17 +371,17 @@ var MIGRATIONS = [
325
371
 
326
372
  INSERT OR IGNORE INTO _migrations (id) VALUES (5);
327
373
  `,
328
- `
374
+ `
329
375
  ALTER TABLE agents ADD COLUMN active_project_id TEXT REFERENCES projects(id) ON DELETE SET NULL;
330
376
  CREATE INDEX IF NOT EXISTS idx_agents_active_project ON agents(active_project_id);
331
377
  INSERT OR IGNORE INTO _migrations (id) VALUES (6);
332
378
  `,
333
- `
379
+ `
334
380
  ALTER TABLE agents ADD COLUMN session_id TEXT;
335
381
  CREATE INDEX IF NOT EXISTS idx_agents_session ON agents(session_id);
336
382
  INSERT OR IGNORE INTO _migrations (id) VALUES (7);
337
383
  `,
338
- `
384
+ `
339
385
  CREATE TABLE IF NOT EXISTS resource_locks (
340
386
  id TEXT PRIMARY KEY,
341
387
  resource_type TEXT NOT NULL CHECK(resource_type IN ('project', 'memory', 'entity', 'agent', 'connector')),
@@ -351,67 +397,35 @@ var MIGRATIONS = [
351
397
  CREATE INDEX IF NOT EXISTS idx_resource_locks_agent ON resource_locks(agent_id);
352
398
  CREATE INDEX IF NOT EXISTS idx_resource_locks_expires ON resource_locks(expires_at);
353
399
  INSERT OR IGNORE INTO _migrations (id) VALUES (8);
400
+ `,
401
+ `
402
+ ALTER TABLE memories ADD COLUMN recall_count INTEGER NOT NULL DEFAULT 0;
403
+ CREATE INDEX IF NOT EXISTS idx_memories_recall_count ON memories(recall_count DESC);
404
+ INSERT OR IGNORE INTO _migrations (id) VALUES (9);
405
+ `,
406
+ `
407
+ CREATE TABLE IF NOT EXISTS webhook_hooks (
408
+ id TEXT PRIMARY KEY,
409
+ type TEXT NOT NULL,
410
+ handler_url TEXT NOT NULL,
411
+ priority INTEGER NOT NULL DEFAULT 50,
412
+ blocking INTEGER NOT NULL DEFAULT 0,
413
+ agent_id TEXT,
414
+ project_id TEXT,
415
+ description TEXT,
416
+ enabled INTEGER NOT NULL DEFAULT 1,
417
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
418
+ invocation_count INTEGER NOT NULL DEFAULT 0,
419
+ failure_count INTEGER NOT NULL DEFAULT 0
420
+ );
421
+ CREATE INDEX IF NOT EXISTS idx_webhook_hooks_type ON webhook_hooks(type);
422
+ CREATE INDEX IF NOT EXISTS idx_webhook_hooks_enabled ON webhook_hooks(enabled);
423
+ INSERT OR IGNORE INTO _migrations (id) VALUES (10);
354
424
  `
355
- ];
356
- var _db = null;
357
- function getDatabase(dbPath) {
358
- if (_db)
359
- return _db;
360
- const path = dbPath || getDbPath();
361
- ensureDir(path);
362
- _db = new Database(path, { create: true });
363
- _db.run("PRAGMA journal_mode = WAL");
364
- _db.run("PRAGMA busy_timeout = 5000");
365
- _db.run("PRAGMA foreign_keys = ON");
366
- runMigrations(_db);
367
- return _db;
368
- }
369
- function runMigrations(db) {
370
- try {
371
- const result = db.query("SELECT MAX(id) as max_id FROM _migrations").get();
372
- const currentLevel = result?.max_id ?? 0;
373
- for (let i = currentLevel;i < MIGRATIONS.length; i++) {
374
- try {
375
- db.exec(MIGRATIONS[i]);
376
- } catch {}
377
- }
378
- } catch {
379
- for (const migration of MIGRATIONS) {
380
- try {
381
- db.exec(migration);
382
- } catch {}
383
- }
384
- }
385
- }
386
- function now() {
387
- return new Date().toISOString();
388
- }
389
- function uuid() {
390
- return crypto.randomUUID();
391
- }
392
- function shortUuid() {
393
- return crypto.randomUUID().slice(0, 8);
394
- }
425
+ ];
426
+ });
395
427
 
396
428
  // src/lib/redact.ts
397
- var REDACTED = "[REDACTED]";
398
- var SECRET_PATTERNS = [
399
- { name: "openai_key", pattern: /sk-[a-zA-Z0-9_-]{20,}/g },
400
- { name: "anthropic_key", pattern: /sk-ant-[a-zA-Z0-9_-]{20,}/g },
401
- { name: "generic_key", pattern: /(?:pk|tok|key|token|api[_-]?key)[_-][a-zA-Z0-9_-]{16,}/gi },
402
- { name: "aws_key", pattern: /AKIA[A-Z0-9]{16}/g },
403
- { name: "aws_secret", pattern: /(?<=AWS_SECRET_ACCESS_KEY\s*=\s*)[A-Za-z0-9/+=]{40}/g },
404
- { name: "github_token", pattern: /gh[ps]_[a-zA-Z0-9]{36,}/g },
405
- { name: "github_oauth", pattern: /gho_[a-zA-Z0-9]{36,}/g },
406
- { name: "npm_token", pattern: /npm_[a-zA-Z0-9]{36,}/g },
407
- { name: "bearer", pattern: /Bearer\s+[a-zA-Z0-9_\-.]{20,}/g },
408
- { name: "conn_string", pattern: /(?:postgres|postgresql|mysql|mongodb|redis|amqp|mqtt):\/\/[^\s"'`]+@[^\s"'`]+/gi },
409
- { name: "env_secret", pattern: /(?:SECRET|TOKEN|PASSWORD|PASSPHRASE|API_KEY|PRIVATE_KEY|AUTH|CREDENTIAL)[_A-Z]*\s*=\s*["']?[^\s"'\n]{8,}["']?/gi },
410
- { name: "stripe_key", pattern: /(?:sk|pk|rk)_(?:test|live)_[a-zA-Z0-9]{20,}/g },
411
- { name: "slack_token", pattern: /xox[bpras]-[a-zA-Z0-9-]{20,}/g },
412
- { name: "jwt", pattern: /eyJ[a-zA-Z0-9_-]{10,}\.eyJ[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}/g },
413
- { name: "hex_secret", pattern: /(?<=(?:key|token|secret|password|hash)\s*[:=]\s*["']?)[0-9a-f]{32,}(?=["']?)/gi }
414
- ];
415
429
  function redactSecrets(text) {
416
430
  let result = text;
417
431
  for (const { pattern } of SECRET_PATTERNS) {
@@ -420,492 +434,617 @@ function redactSecrets(text) {
420
434
  }
421
435
  return result;
422
436
  }
437
+ var REDACTED = "[REDACTED]", SECRET_PATTERNS;
438
+ var init_redact = __esm(() => {
439
+ SECRET_PATTERNS = [
440
+ { name: "openai_key", pattern: /sk-[a-zA-Z0-9_-]{20,}/g },
441
+ { name: "anthropic_key", pattern: /sk-ant-[a-zA-Z0-9_-]{20,}/g },
442
+ { name: "generic_key", pattern: /(?:pk|tok|key|token|api[_-]?key)[_-][a-zA-Z0-9_-]{16,}/gi },
443
+ { name: "aws_key", pattern: /AKIA[A-Z0-9]{16}/g },
444
+ { name: "aws_secret", pattern: /(?<=AWS_SECRET_ACCESS_KEY\s*=\s*)[A-Za-z0-9/+=]{40}/g },
445
+ { name: "github_token", pattern: /gh[ps]_[a-zA-Z0-9]{36,}/g },
446
+ { name: "github_oauth", pattern: /gho_[a-zA-Z0-9]{36,}/g },
447
+ { name: "npm_token", pattern: /npm_[a-zA-Z0-9]{36,}/g },
448
+ { name: "bearer", pattern: /Bearer\s+[a-zA-Z0-9_\-.]{20,}/g },
449
+ { name: "conn_string", pattern: /(?:postgres|postgresql|mysql|mongodb|redis|amqp|mqtt):\/\/[^\s"'`]+@[^\s"'`]+/gi },
450
+ { name: "env_secret", pattern: /(?:SECRET|TOKEN|PASSWORD|PASSPHRASE|API_KEY|PRIVATE_KEY|AUTH|CREDENTIAL)[_A-Z]*\s*=\s*["']?[^\s"'\n]{8,}["']?/gi },
451
+ { name: "stripe_key", pattern: /(?:sk|pk|rk)_(?:test|live)_[a-zA-Z0-9]{20,}/g },
452
+ { name: "slack_token", pattern: /xox[bpras]-[a-zA-Z0-9-]{20,}/g },
453
+ { name: "jwt", pattern: /eyJ[a-zA-Z0-9_-]{10,}\.eyJ[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}/g },
454
+ { name: "hex_secret", pattern: /(?<=(?:key|token|secret|password|hash)\s*[:=]\s*["']?)[0-9a-f]{32,}(?=["']?)/gi }
455
+ ];
456
+ });
423
457
 
424
- // src/db/agents.ts
425
- var CONFLICT_WINDOW_MS = 30 * 60 * 1000;
426
- function parseAgentRow(row) {
427
- return {
428
- id: row["id"],
429
- name: row["name"],
430
- session_id: row["session_id"] || null,
431
- description: row["description"] || null,
432
- role: row["role"] || null,
433
- metadata: JSON.parse(row["metadata"] || "{}"),
434
- active_project_id: row["active_project_id"] || null,
435
- created_at: row["created_at"],
436
- last_seen_at: row["last_seen_at"]
437
- };
458
+ // src/lib/hooks.ts
459
+ function generateHookId() {
460
+ return `hook_${++_idCounter}_${Date.now().toString(36)}`;
438
461
  }
439
- function registerAgent(name, sessionId, description, role, projectId, db) {
440
- const d = db || getDatabase();
441
- const timestamp = now();
442
- const normalizedName = name.trim().toLowerCase();
443
- const existing = d.query("SELECT * FROM agents WHERE LOWER(name) = ?").get(normalizedName);
444
- if (existing) {
445
- const existingId = existing["id"];
446
- const existingSessionId = existing["session_id"] || null;
447
- const existingLastSeen = existing["last_seen_at"];
448
- if (sessionId && existingSessionId && existingSessionId !== sessionId) {
449
- const lastSeenMs = new Date(existingLastSeen).getTime();
450
- const nowMs = Date.now();
451
- if (nowMs - lastSeenMs < CONFLICT_WINDOW_MS) {
452
- throw new AgentConflictError({
453
- existing_id: existingId,
454
- existing_name: normalizedName,
455
- last_seen_at: existingLastSeen,
456
- session_hint: existingSessionId.slice(0, 8),
457
- working_dir: null
458
- });
462
+
463
+ class HookRegistry {
464
+ hooks = new Map;
465
+ register(reg) {
466
+ const id = generateHookId();
467
+ const hook = {
468
+ ...reg,
469
+ id,
470
+ priority: reg.priority ?? 50
471
+ };
472
+ this.hooks.set(id, hook);
473
+ return id;
474
+ }
475
+ unregister(hookId) {
476
+ const hook = this.hooks.get(hookId);
477
+ if (!hook)
478
+ return false;
479
+ if (hook.builtin)
480
+ return false;
481
+ this.hooks.delete(hookId);
482
+ return true;
483
+ }
484
+ list(type) {
485
+ const all = [...this.hooks.values()];
486
+ if (!type)
487
+ return all;
488
+ return all.filter((h) => h.type === type);
489
+ }
490
+ async runHooks(type, context) {
491
+ const matching = this.getMatchingHooks(type, context);
492
+ if (matching.length === 0)
493
+ return true;
494
+ matching.sort((a, b) => a.priority - b.priority);
495
+ for (const hook of matching) {
496
+ if (hook.blocking) {
497
+ try {
498
+ const result = await hook.handler(context);
499
+ if (result === false)
500
+ return false;
501
+ } catch (err) {
502
+ console.error(`[hooks] blocking hook ${hook.id} (${type}) threw:`, err);
503
+ }
504
+ } else {
505
+ Promise.resolve().then(() => hook.handler(context)).catch((err) => console.error(`[hooks] non-blocking hook ${hook.id} (${type}) threw:`, err));
459
506
  }
460
507
  }
461
- d.run("UPDATE agents SET last_seen_at = ?, session_id = ? WHERE id = ?", [
462
- timestamp,
463
- sessionId ?? existingSessionId,
464
- existingId
465
- ]);
466
- if (description) {
467
- d.run("UPDATE agents SET description = ? WHERE id = ?", [description, existingId]);
468
- }
469
- if (role) {
470
- d.run("UPDATE agents SET role = ? WHERE id = ?", [role, existingId]);
471
- }
472
- if (projectId !== undefined) {
473
- d.run("UPDATE agents SET active_project_id = ? WHERE id = ?", [projectId, existingId]);
508
+ return true;
509
+ }
510
+ getMatchingHooks(type, context) {
511
+ const ctx = context;
512
+ return [...this.hooks.values()].filter((hook) => {
513
+ if (hook.type !== type)
514
+ return false;
515
+ if (hook.agentId && hook.agentId !== ctx.agentId)
516
+ return false;
517
+ if (hook.projectId && hook.projectId !== ctx.projectId)
518
+ return false;
519
+ return true;
520
+ });
521
+ }
522
+ stats() {
523
+ const all = [...this.hooks.values()];
524
+ const byType = {};
525
+ for (const hook of all) {
526
+ byType[hook.type] = (byType[hook.type] ?? 0) + 1;
474
527
  }
475
- return getAgent(existingId, d);
528
+ return {
529
+ total: all.length,
530
+ byType,
531
+ blocking: all.filter((h) => h.blocking).length,
532
+ nonBlocking: all.filter((h) => !h.blocking).length
533
+ };
476
534
  }
477
- const id = shortUuid();
478
- d.run("INSERT INTO agents (id, name, session_id, description, role, active_project_id, created_at, last_seen_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [id, normalizedName, sessionId ?? null, description || null, role || "agent", projectId ?? null, timestamp, timestamp]);
479
- return getAgent(id, d);
480
535
  }
481
- function getAgent(idOrName, db) {
536
+ var _idCounter = 0, hookRegistry;
537
+ var init_hooks = __esm(() => {
538
+ hookRegistry = new HookRegistry;
539
+ });
540
+
541
+ // src/db/entity-memories.ts
542
+ function parseEntityMemoryRow(row) {
543
+ return {
544
+ entity_id: row["entity_id"],
545
+ memory_id: row["memory_id"],
546
+ role: row["role"],
547
+ created_at: row["created_at"]
548
+ };
549
+ }
550
+ function linkEntityToMemory(entityId, memoryId, role = "context", db) {
482
551
  const d = db || getDatabase();
483
- let row = d.query("SELECT * FROM agents WHERE id = ?").get(idOrName);
484
- if (row)
485
- return parseAgentRow(row);
486
- row = d.query("SELECT * FROM agents WHERE LOWER(name) = ?").get(idOrName.trim().toLowerCase());
487
- if (row)
488
- return parseAgentRow(row);
489
- const rows = d.query("SELECT * FROM agents WHERE id LIKE ?").all(`${idOrName}%`);
490
- if (rows.length === 1)
491
- return parseAgentRow(rows[0]);
492
- return null;
552
+ const timestamp = now();
553
+ d.run(`INSERT OR IGNORE INTO entity_memories (entity_id, memory_id, role, created_at)
554
+ VALUES (?, ?, ?, ?)`, [entityId, memoryId, role, timestamp]);
555
+ const row = d.query("SELECT * FROM entity_memories WHERE entity_id = ? AND memory_id = ?").get(entityId, memoryId);
556
+ return parseEntityMemoryRow(row);
493
557
  }
494
- function listAgents(db) {
558
+ function unlinkEntityFromMemory(entityId, memoryId, db) {
495
559
  const d = db || getDatabase();
496
- const rows = d.query("SELECT * FROM agents ORDER BY last_seen_at DESC").all();
497
- return rows.map(parseAgentRow);
560
+ d.run("DELETE FROM entity_memories WHERE entity_id = ? AND memory_id = ?", [entityId, memoryId]);
498
561
  }
499
- function listAgentsByProject(projectId, db) {
562
+ function getMemoriesForEntity(entityId, db) {
500
563
  const d = db || getDatabase();
501
- const rows = d.query("SELECT * FROM agents WHERE active_project_id = ? ORDER BY last_seen_at DESC").all(projectId);
502
- return rows.map(parseAgentRow);
564
+ const rows = d.query(`SELECT m.* FROM memories m
565
+ INNER JOIN entity_memories em ON em.memory_id = m.id
566
+ WHERE em.entity_id = ?
567
+ ORDER BY m.importance DESC, m.created_at DESC`).all(entityId);
568
+ return rows.map(parseMemoryRow);
503
569
  }
504
- function updateAgent(id, updates, db) {
570
+ function getEntityMemoryLinks(entityId, memoryId, db) {
505
571
  const d = db || getDatabase();
506
- const agent = getAgent(id, d);
507
- if (!agent)
508
- return null;
509
- const timestamp = now();
510
- if (updates.name) {
511
- const normalizedNewName = updates.name.trim().toLowerCase();
512
- if (normalizedNewName !== agent.name) {
513
- const existing = d.query("SELECT id FROM agents WHERE LOWER(name) = ? AND id != ?").get(normalizedNewName, agent.id);
514
- if (existing) {
515
- throw new Error(`Agent name already taken: ${normalizedNewName}`);
516
- }
517
- d.run("UPDATE agents SET name = ? WHERE id = ?", [normalizedNewName, agent.id]);
518
- }
572
+ const conditions = [];
573
+ const params = [];
574
+ if (entityId) {
575
+ conditions.push("entity_id = ?");
576
+ params.push(entityId);
519
577
  }
520
- if (updates.description !== undefined) {
521
- d.run("UPDATE agents SET description = ? WHERE id = ?", [updates.description, agent.id]);
578
+ if (memoryId) {
579
+ conditions.push("memory_id = ?");
580
+ params.push(memoryId);
522
581
  }
523
- if (updates.role !== undefined) {
524
- d.run("UPDATE agents SET role = ? WHERE id = ?", [updates.role, agent.id]);
525
- }
526
- if (updates.metadata !== undefined) {
527
- d.run("UPDATE agents SET metadata = ? WHERE id = ?", [JSON.stringify(updates.metadata), agent.id]);
528
- }
529
- if ("active_project_id" in updates) {
530
- d.run("UPDATE agents SET active_project_id = ? WHERE id = ?", [updates.active_project_id ?? null, agent.id]);
582
+ let sql = "SELECT * FROM entity_memories";
583
+ if (conditions.length > 0) {
584
+ sql += ` WHERE ${conditions.join(" AND ")}`;
531
585
  }
532
- d.run("UPDATE agents SET last_seen_at = ? WHERE id = ?", [timestamp, agent.id]);
533
- return getAgent(agent.id, d);
586
+ sql += " ORDER BY created_at DESC";
587
+ const rows = d.query(sql).all(...params);
588
+ return rows.map(parseEntityMemoryRow);
534
589
  }
590
+ var init_entity_memories = __esm(() => {
591
+ init_database();
592
+ init_memories();
593
+ });
535
594
 
536
- // src/db/projects.ts
537
- function parseProjectRow(row) {
595
+ // src/db/memories.ts
596
+ var exports_memories = {};
597
+ __export(exports_memories, {
598
+ updateMemory: () => updateMemory,
599
+ touchMemory: () => touchMemory,
600
+ parseMemoryRow: () => parseMemoryRow,
601
+ listMemories: () => listMemories,
602
+ incrementRecallCount: () => incrementRecallCount,
603
+ getMemoryVersions: () => getMemoryVersions,
604
+ getMemoryByKey: () => getMemoryByKey,
605
+ getMemory: () => getMemory,
606
+ getMemoriesByKey: () => getMemoriesByKey,
607
+ deleteMemory: () => deleteMemory,
608
+ createMemory: () => createMemory,
609
+ cleanExpiredMemories: () => cleanExpiredMemories,
610
+ bulkDeleteMemories: () => bulkDeleteMemories
611
+ });
612
+ function runEntityExtraction(_memory, _projectId, _d) {}
613
+ function parseMemoryRow(row) {
538
614
  return {
539
615
  id: row["id"],
540
- name: row["name"],
541
- path: row["path"],
542
- description: row["description"] || null,
543
- memory_prefix: row["memory_prefix"] || null,
616
+ key: row["key"],
617
+ value: row["value"],
618
+ category: row["category"],
619
+ scope: row["scope"],
620
+ summary: row["summary"] || null,
621
+ tags: JSON.parse(row["tags"] || "[]"),
622
+ importance: row["importance"],
623
+ source: row["source"],
624
+ status: row["status"],
625
+ pinned: !!row["pinned"],
626
+ agent_id: row["agent_id"] || null,
627
+ project_id: row["project_id"] || null,
628
+ session_id: row["session_id"] || null,
629
+ metadata: JSON.parse(row["metadata"] || "{}"),
630
+ access_count: row["access_count"],
631
+ version: row["version"],
632
+ expires_at: row["expires_at"] || null,
544
633
  created_at: row["created_at"],
545
- updated_at: row["updated_at"]
634
+ updated_at: row["updated_at"],
635
+ accessed_at: row["accessed_at"] || null
546
636
  };
547
637
  }
548
- function registerProject(name, path, description, memoryPrefix, db) {
638
+ function createMemory(input, dedupeMode = "merge", db) {
549
639
  const d = db || getDatabase();
550
640
  const timestamp = now();
551
- const existing = d.query("SELECT * FROM projects WHERE path = ?").get(path);
552
- if (existing) {
553
- const existingId = existing["id"];
554
- d.run("UPDATE projects SET updated_at = ? WHERE id = ?", [
555
- timestamp,
556
- existingId
557
- ]);
558
- return parseProjectRow(existing);
641
+ let expiresAt = input.expires_at || null;
642
+ if (input.ttl_ms && !expiresAt) {
643
+ expiresAt = new Date(Date.now() + input.ttl_ms).toISOString();
559
644
  }
560
645
  const id = uuid();
561
- d.run("INSERT INTO projects (id, name, path, description, memory_prefix, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)", [id, name, path, description || null, memoryPrefix || null, timestamp, timestamp]);
562
- return getProject(id, d);
563
- }
564
- function getProject(idOrPath, db) {
565
- const d = db || getDatabase();
566
- let row = d.query("SELECT * FROM projects WHERE id = ?").get(idOrPath);
567
- if (row)
568
- return parseProjectRow(row);
569
- row = d.query("SELECT * FROM projects WHERE path = ?").get(idOrPath);
570
- if (row)
571
- return parseProjectRow(row);
572
- row = d.query("SELECT * FROM projects WHERE LOWER(name) = ?").get(idOrPath.toLowerCase());
573
- if (row)
574
- return parseProjectRow(row);
575
- return null;
576
- }
577
- function listProjects(db) {
578
- const d = db || getDatabase();
579
- const rows = d.query("SELECT * FROM projects ORDER BY updated_at DESC").all();
580
- return rows.map(parseProjectRow);
581
- }
582
-
583
- // src/lib/extractor.ts
584
- var TECH_KEYWORDS = new Set([
585
- "typescript",
586
- "javascript",
587
- "python",
588
- "rust",
589
- "go",
590
- "java",
591
- "ruby",
592
- "swift",
593
- "kotlin",
594
- "react",
595
- "vue",
596
- "angular",
597
- "svelte",
598
- "nextjs",
599
- "bun",
600
- "node",
601
- "deno",
602
- "sqlite",
603
- "postgres",
604
- "mysql",
605
- "redis",
606
- "docker",
607
- "kubernetes",
608
- "git",
609
- "npm",
610
- "yarn",
611
- "pnpm",
612
- "webpack",
613
- "vite",
614
- "tailwind",
615
- "prisma",
616
- "drizzle",
617
- "zod",
618
- "commander",
619
- "express",
620
- "fastify",
621
- "hono"
622
- ]);
623
- var FILE_PATH_RE = /(?:^|\s)((?:\/|\.\/|~\/)?(?:[\w.-]+\/)+[\w.-]+\.\w+)/g;
624
- var URL_RE = /https?:\/\/[^\s)]+/g;
625
- var NPM_PACKAGE_RE = /@[\w-]+\/[\w.-]+/g;
626
- var PASCAL_CASE_RE = /\b([A-Z][a-z]+(?:[A-Z][a-z]+)+)\b/g;
627
- function getSearchText(memory) {
628
- const parts = [memory.key, memory.value];
629
- if (memory.summary)
630
- parts.push(memory.summary);
631
- return parts.join(" ");
632
- }
633
- function extractEntities(memory, db) {
634
- const text = getSearchText(memory);
635
- const entityMap = new Map;
636
- function add(name, type, confidence) {
637
- const normalized = name.toLowerCase();
638
- if (normalized.length < 3)
639
- return;
640
- const existing = entityMap.get(normalized);
641
- if (!existing || existing.confidence < confidence) {
642
- entityMap.set(normalized, { name: normalized, type, confidence });
643
- }
644
- }
645
- for (const match of text.matchAll(FILE_PATH_RE)) {
646
- add(match[1].trim(), "file", 0.9);
647
- }
648
- for (const match of text.matchAll(URL_RE)) {
649
- add(match[0], "api", 0.8);
650
- }
651
- for (const match of text.matchAll(NPM_PACKAGE_RE)) {
652
- add(match[0], "tool", 0.85);
653
- }
654
- try {
655
- const d = db || getDatabase();
656
- const agents = listAgents(d);
657
- const textLower2 = text.toLowerCase();
658
- for (const agent of agents) {
659
- const nameLower = agent.name.toLowerCase();
660
- if (nameLower.length >= 3 && textLower2.includes(nameLower)) {
661
- add(agent.name, "person", 0.95);
662
- }
663
- }
664
- } catch {}
665
- try {
666
- const d = db || getDatabase();
667
- const projects = listProjects(d);
668
- const textLower2 = text.toLowerCase();
669
- for (const project of projects) {
670
- const nameLower = project.name.toLowerCase();
671
- if (nameLower.length >= 3 && textLower2.includes(nameLower)) {
672
- add(project.name, "project", 0.95);
673
- }
674
- }
675
- } catch {}
676
- const textLower = text.toLowerCase();
677
- for (const keyword of TECH_KEYWORDS) {
678
- const re = new RegExp(`\\b${keyword}\\b`, "i");
679
- if (re.test(textLower)) {
680
- add(keyword, "tool", 0.7);
681
- }
682
- }
683
- for (const match of text.matchAll(PASCAL_CASE_RE)) {
684
- add(match[1], "concept", 0.5);
685
- }
686
- return Array.from(entityMap.values()).sort((a, b) => b.confidence - a.confidence);
687
- }
688
-
689
- // src/db/entities.ts
690
- function parseEntityRow(row) {
691
- return {
692
- id: row["id"],
693
- name: row["name"],
694
- type: row["type"],
695
- description: row["description"] || null,
696
- metadata: JSON.parse(row["metadata"] || "{}"),
697
- project_id: row["project_id"] || null,
698
- created_at: row["created_at"],
699
- updated_at: row["updated_at"]
700
- };
701
- }
702
- function createEntity(input, db) {
703
- const d = db || getDatabase();
704
- const timestamp = now();
646
+ const tags = input.tags || [];
647
+ const tagsJson = JSON.stringify(tags);
705
648
  const metadataJson = JSON.stringify(input.metadata || {});
706
- const existing = d.query(`SELECT * FROM entities
707
- WHERE name = ? AND type = ? AND COALESCE(project_id, '') = ?`).get(input.name, input.type, input.project_id || "");
708
- if (existing) {
709
- const sets = ["updated_at = ?"];
710
- const params = [timestamp];
711
- if (input.description !== undefined) {
712
- sets.push("description = ?");
713
- params.push(input.description);
714
- }
715
- if (input.metadata !== undefined) {
716
- sets.push("metadata = ?");
717
- params.push(metadataJson);
649
+ const safeValue = redactSecrets(input.value);
650
+ const safeSummary = input.summary ? redactSecrets(input.summary) : null;
651
+ if (dedupeMode === "merge") {
652
+ const existing = d.query(`SELECT id, version FROM memories
653
+ WHERE key = ? AND scope = ?
654
+ AND COALESCE(agent_id, '') = ?
655
+ AND COALESCE(project_id, '') = ?
656
+ AND COALESCE(session_id, '') = ?`).get(input.key, input.scope || "private", input.agent_id || "", input.project_id || "", input.session_id || "");
657
+ if (existing) {
658
+ d.run(`UPDATE memories SET
659
+ value = ?, category = ?, summary = ?, tags = ?,
660
+ importance = ?, metadata = ?, expires_at = ?,
661
+ pinned = COALESCE(pinned, 0),
662
+ version = version + 1, updated_at = ?
663
+ WHERE id = ?`, [
664
+ safeValue,
665
+ input.category || "knowledge",
666
+ safeSummary,
667
+ tagsJson,
668
+ input.importance ?? 5,
669
+ metadataJson,
670
+ expiresAt,
671
+ timestamp,
672
+ existing.id
673
+ ]);
674
+ d.run("DELETE FROM memory_tags WHERE memory_id = ?", [existing.id]);
675
+ const insertTag2 = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
676
+ for (const tag of tags) {
677
+ insertTag2.run(existing.id, tag);
678
+ }
679
+ const merged = getMemory(existing.id, d);
680
+ try {
681
+ const oldLinks = getEntityMemoryLinks(undefined, merged.id, d);
682
+ for (const link of oldLinks) {
683
+ unlinkEntityFromMemory(link.entity_id, merged.id, d);
684
+ }
685
+ runEntityExtraction(merged, input.project_id, d);
686
+ } catch {}
687
+ return merged;
718
688
  }
719
- const existingId = existing["id"];
720
- params.push(existingId);
721
- d.run(`UPDATE entities SET ${sets.join(", ")} WHERE id = ?`, params);
722
- return getEntity(existingId, d);
723
689
  }
724
- const id = shortUuid();
725
- d.run(`INSERT INTO entities (id, name, type, description, metadata, project_id, created_at, updated_at)
726
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
690
+ 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)
691
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', 0, ?, ?, ?, ?, 0, 1, ?, ?, ?)`, [
727
692
  id,
728
- input.name,
729
- input.type,
730
- input.description || null,
731
- metadataJson,
693
+ input.key,
694
+ input.value,
695
+ input.category || "knowledge",
696
+ input.scope || "private",
697
+ input.summary || null,
698
+ tagsJson,
699
+ input.importance ?? 5,
700
+ input.source || "agent",
701
+ input.agent_id || null,
732
702
  input.project_id || null,
703
+ input.session_id || null,
704
+ metadataJson,
705
+ expiresAt,
733
706
  timestamp,
734
707
  timestamp
735
708
  ]);
736
- return getEntity(id, d);
709
+ const insertTag = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
710
+ for (const tag of tags) {
711
+ insertTag.run(id, tag);
712
+ }
713
+ const memory = getMemory(id, d);
714
+ runEntityExtraction(memory, input.project_id, d);
715
+ hookRegistry.runHooks("PostMemorySave", {
716
+ memory,
717
+ wasUpdated: false,
718
+ agentId: input.agent_id,
719
+ projectId: input.project_id,
720
+ sessionId: input.session_id,
721
+ timestamp: Date.now()
722
+ });
723
+ return memory;
737
724
  }
738
- function getEntity(id, db) {
725
+ function getMemory(id, db) {
739
726
  const d = db || getDatabase();
740
- const row = d.query("SELECT * FROM entities WHERE id = ?").get(id);
727
+ const row = d.query("SELECT * FROM memories WHERE id = ?").get(id);
741
728
  if (!row)
742
- throw new EntityNotFoundError(id);
743
- return parseEntityRow(row);
729
+ return null;
730
+ return parseMemoryRow(row);
744
731
  }
745
- function getEntityByName(name, type, projectId, db) {
732
+ function getMemoryByKey(key, scope, agentId, projectId, sessionId, db) {
746
733
  const d = db || getDatabase();
747
- let sql = "SELECT * FROM entities WHERE name = ?";
748
- const params = [name];
749
- if (type) {
750
- sql += " AND type = ?";
751
- params.push(type);
734
+ let sql = "SELECT * FROM memories WHERE key = ?";
735
+ const params = [key];
736
+ if (scope) {
737
+ sql += " AND scope = ?";
738
+ params.push(scope);
752
739
  }
753
- if (projectId !== undefined) {
740
+ if (agentId) {
741
+ sql += " AND agent_id = ?";
742
+ params.push(agentId);
743
+ }
744
+ if (projectId) {
754
745
  sql += " AND project_id = ?";
755
746
  params.push(projectId);
756
747
  }
757
- sql += " LIMIT 1";
748
+ if (sessionId) {
749
+ sql += " AND session_id = ?";
750
+ params.push(sessionId);
751
+ }
752
+ sql += " AND status = 'active' ORDER BY importance DESC LIMIT 1";
758
753
  const row = d.query(sql).get(...params);
759
754
  if (!row)
760
755
  return null;
761
- return parseEntityRow(row);
756
+ return parseMemoryRow(row);
762
757
  }
763
- function listEntities(filter = {}, db) {
758
+ function getMemoriesByKey(key, scope, agentId, projectId, db) {
764
759
  const d = db || getDatabase();
765
- const conditions = [];
766
- const params = [];
767
- if (filter.type) {
768
- conditions.push("type = ?");
769
- params.push(filter.type);
770
- }
771
- if (filter.project_id) {
772
- conditions.push("project_id = ?");
773
- params.push(filter.project_id);
760
+ let sql = "SELECT * FROM memories WHERE key = ?";
761
+ const params = [key];
762
+ if (scope) {
763
+ sql += " AND scope = ?";
764
+ params.push(scope);
774
765
  }
775
- if (filter.search) {
776
- conditions.push("(name LIKE ? OR description LIKE ?)");
777
- const term = `%${filter.search}%`;
778
- params.push(term, term);
766
+ if (agentId) {
767
+ sql += " AND agent_id = ?";
768
+ params.push(agentId);
779
769
  }
780
- let sql = "SELECT * FROM entities";
781
- if (conditions.length > 0) {
782
- sql += ` WHERE ${conditions.join(" AND ")}`;
770
+ if (projectId) {
771
+ sql += " AND project_id = ?";
772
+ params.push(projectId);
783
773
  }
784
- sql += " ORDER BY updated_at DESC";
785
- if (filter.limit) {
774
+ sql += " AND status = 'active' ORDER BY importance DESC";
775
+ const rows = d.query(sql).all(...params);
776
+ return rows.map(parseMemoryRow);
777
+ }
778
+ function listMemories(filter, db) {
779
+ const d = db || getDatabase();
780
+ const conditions = [];
781
+ const params = [];
782
+ if (filter) {
783
+ if (filter.scope) {
784
+ if (Array.isArray(filter.scope)) {
785
+ conditions.push(`scope IN (${filter.scope.map(() => "?").join(",")})`);
786
+ params.push(...filter.scope);
787
+ } else {
788
+ conditions.push("scope = ?");
789
+ params.push(filter.scope);
790
+ }
791
+ }
792
+ if (filter.category) {
793
+ if (Array.isArray(filter.category)) {
794
+ conditions.push(`category IN (${filter.category.map(() => "?").join(",")})`);
795
+ params.push(...filter.category);
796
+ } else {
797
+ conditions.push("category = ?");
798
+ params.push(filter.category);
799
+ }
800
+ }
801
+ if (filter.source) {
802
+ if (Array.isArray(filter.source)) {
803
+ conditions.push(`source IN (${filter.source.map(() => "?").join(",")})`);
804
+ params.push(...filter.source);
805
+ } else {
806
+ conditions.push("source = ?");
807
+ params.push(filter.source);
808
+ }
809
+ }
810
+ if (filter.status) {
811
+ if (Array.isArray(filter.status)) {
812
+ conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
813
+ params.push(...filter.status);
814
+ } else {
815
+ conditions.push("status = ?");
816
+ params.push(filter.status);
817
+ }
818
+ } else {
819
+ conditions.push("status = 'active'");
820
+ }
821
+ if (filter.project_id) {
822
+ conditions.push("project_id = ?");
823
+ params.push(filter.project_id);
824
+ }
825
+ if (filter.agent_id) {
826
+ conditions.push("agent_id = ?");
827
+ params.push(filter.agent_id);
828
+ }
829
+ if (filter.session_id) {
830
+ conditions.push("session_id = ?");
831
+ params.push(filter.session_id);
832
+ }
833
+ if (filter.min_importance) {
834
+ conditions.push("importance >= ?");
835
+ params.push(filter.min_importance);
836
+ }
837
+ if (filter.pinned !== undefined) {
838
+ conditions.push("pinned = ?");
839
+ params.push(filter.pinned ? 1 : 0);
840
+ }
841
+ if (filter.tags && filter.tags.length > 0) {
842
+ for (const tag of filter.tags) {
843
+ conditions.push("id IN (SELECT memory_id FROM memory_tags WHERE tag = ?)");
844
+ params.push(tag);
845
+ }
846
+ }
847
+ if (filter.search) {
848
+ conditions.push("(key LIKE ? OR value LIKE ? OR summary LIKE ?)");
849
+ const term = `%${filter.search}%`;
850
+ params.push(term, term, term);
851
+ }
852
+ } else {
853
+ conditions.push("status = 'active'");
854
+ }
855
+ let sql = "SELECT * FROM memories";
856
+ if (conditions.length > 0) {
857
+ sql += ` WHERE ${conditions.join(" AND ")}`;
858
+ }
859
+ sql += " ORDER BY importance DESC, created_at DESC";
860
+ if (filter?.limit) {
786
861
  sql += " LIMIT ?";
787
862
  params.push(filter.limit);
788
863
  }
789
- if (filter.offset) {
864
+ if (filter?.offset) {
790
865
  sql += " OFFSET ?";
791
866
  params.push(filter.offset);
792
867
  }
793
868
  const rows = d.query(sql).all(...params);
794
- return rows.map(parseEntityRow);
869
+ return rows.map(parseMemoryRow);
795
870
  }
796
- function updateEntity(id, input, db) {
871
+ function updateMemory(id, input, db) {
797
872
  const d = db || getDatabase();
798
- const existing = d.query("SELECT id FROM entities WHERE id = ?").get(id);
873
+ const existing = getMemory(id, d);
799
874
  if (!existing)
800
- throw new EntityNotFoundError(id);
801
- const sets = ["updated_at = ?"];
875
+ throw new MemoryNotFoundError(id);
876
+ if (existing.version !== input.version) {
877
+ throw new VersionConflictError(id, input.version, existing.version);
878
+ }
879
+ try {
880
+ d.run(`INSERT OR IGNORE INTO memory_versions (id, memory_id, version, value, importance, scope, category, tags, summary, pinned, status, created_at)
881
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
882
+ uuid(),
883
+ existing.id,
884
+ existing.version,
885
+ existing.value,
886
+ existing.importance,
887
+ existing.scope,
888
+ existing.category,
889
+ JSON.stringify(existing.tags),
890
+ existing.summary,
891
+ existing.pinned ? 1 : 0,
892
+ existing.status,
893
+ existing.updated_at
894
+ ]);
895
+ } catch {}
896
+ const sets = ["version = version + 1", "updated_at = ?"];
802
897
  const params = [now()];
803
- if (input.name !== undefined) {
804
- sets.push("name = ?");
805
- params.push(input.name);
898
+ if (input.value !== undefined) {
899
+ sets.push("value = ?");
900
+ params.push(redactSecrets(input.value));
806
901
  }
807
- if (input.type !== undefined) {
808
- sets.push("type = ?");
809
- params.push(input.type);
902
+ if (input.category !== undefined) {
903
+ sets.push("category = ?");
904
+ params.push(input.category);
810
905
  }
811
- if (input.description !== undefined) {
812
- sets.push("description = ?");
813
- params.push(input.description);
906
+ if (input.scope !== undefined) {
907
+ sets.push("scope = ?");
908
+ params.push(input.scope);
909
+ }
910
+ if (input.summary !== undefined) {
911
+ sets.push("summary = ?");
912
+ params.push(input.summary);
913
+ }
914
+ if (input.importance !== undefined) {
915
+ sets.push("importance = ?");
916
+ params.push(input.importance);
917
+ }
918
+ if (input.pinned !== undefined) {
919
+ sets.push("pinned = ?");
920
+ params.push(input.pinned ? 1 : 0);
921
+ }
922
+ if (input.status !== undefined) {
923
+ sets.push("status = ?");
924
+ params.push(input.status);
814
925
  }
815
926
  if (input.metadata !== undefined) {
816
927
  sets.push("metadata = ?");
817
928
  params.push(JSON.stringify(input.metadata));
818
929
  }
930
+ if (input.expires_at !== undefined) {
931
+ sets.push("expires_at = ?");
932
+ params.push(input.expires_at);
933
+ }
934
+ if (input.tags !== undefined) {
935
+ sets.push("tags = ?");
936
+ params.push(JSON.stringify(input.tags));
937
+ d.run("DELETE FROM memory_tags WHERE memory_id = ?", [id]);
938
+ const insertTag = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
939
+ for (const tag of input.tags) {
940
+ insertTag.run(id, tag);
941
+ }
942
+ }
819
943
  params.push(id);
820
- d.run(`UPDATE entities SET ${sets.join(", ")} WHERE id = ?`, params);
821
- return getEntity(id, d);
944
+ d.run(`UPDATE memories SET ${sets.join(", ")} WHERE id = ?`, params);
945
+ const updated = getMemory(id, d);
946
+ if (input.value !== undefined) {
947
+ try {
948
+ const oldLinks = getEntityMemoryLinks(undefined, updated.id, d);
949
+ for (const link of oldLinks) {
950
+ unlinkEntityFromMemory(link.entity_id, updated.id, d);
951
+ }
952
+ } catch {}
953
+ }
954
+ hookRegistry.runHooks("PostMemoryUpdate", {
955
+ memory: updated,
956
+ previousValue: existing.value,
957
+ agentId: existing.agent_id ?? undefined,
958
+ projectId: existing.project_id ?? undefined,
959
+ sessionId: existing.session_id ?? undefined,
960
+ timestamp: Date.now()
961
+ });
962
+ return updated;
822
963
  }
823
- function deleteEntity(id, db) {
964
+ function deleteMemory(id, db) {
824
965
  const d = db || getDatabase();
825
- const result = d.run("DELETE FROM entities WHERE id = ?", [id]);
826
- if (result.changes === 0)
827
- throw new EntityNotFoundError(id);
966
+ const result = d.run("DELETE FROM memories WHERE id = ?", [id]);
967
+ if (result.changes > 0) {
968
+ hookRegistry.runHooks("PostMemoryDelete", {
969
+ memoryId: id,
970
+ timestamp: Date.now()
971
+ });
972
+ }
973
+ return result.changes > 0;
828
974
  }
829
- function mergeEntities(sourceId, targetId, db) {
975
+ function bulkDeleteMemories(ids, db) {
830
976
  const d = db || getDatabase();
831
- getEntity(sourceId, d);
832
- getEntity(targetId, d);
833
- d.run(`UPDATE OR IGNORE relations SET source_entity_id = ? WHERE source_entity_id = ?`, [targetId, sourceId]);
834
- d.run(`UPDATE OR IGNORE relations SET target_entity_id = ? WHERE target_entity_id = ?`, [targetId, sourceId]);
835
- d.run("DELETE FROM relations WHERE source_entity_id = ? OR target_entity_id = ?", [
836
- sourceId,
837
- sourceId
838
- ]);
839
- d.run(`UPDATE OR IGNORE entity_memories SET entity_id = ? WHERE entity_id = ?`, [targetId, sourceId]);
840
- d.run("DELETE FROM entity_memories WHERE entity_id = ?", [sourceId]);
841
- d.run("DELETE FROM entities WHERE id = ?", [sourceId]);
842
- d.run("UPDATE entities SET updated_at = ? WHERE id = ?", [now(), targetId]);
843
- return getEntity(targetId, d);
844
- }
845
-
846
- // src/db/entity-memories.ts
847
- function parseEntityMemoryRow(row) {
848
- return {
849
- entity_id: row["entity_id"],
850
- memory_id: row["memory_id"],
851
- role: row["role"],
852
- created_at: row["created_at"]
853
- };
977
+ if (ids.length === 0)
978
+ return 0;
979
+ const placeholders = ids.map(() => "?").join(",");
980
+ const countRow = d.query(`SELECT COUNT(*) as c FROM memories WHERE id IN (${placeholders})`).get(...ids);
981
+ const count = countRow.c;
982
+ if (count > 0) {
983
+ d.run(`DELETE FROM memories WHERE id IN (${placeholders})`, ids);
984
+ }
985
+ return count;
854
986
  }
855
- function linkEntityToMemory(entityId, memoryId, role = "context", db) {
987
+ function touchMemory(id, db) {
856
988
  const d = db || getDatabase();
857
- const timestamp = now();
858
- d.run(`INSERT OR IGNORE INTO entity_memories (entity_id, memory_id, role, created_at)
859
- VALUES (?, ?, ?, ?)`, [entityId, memoryId, role, timestamp]);
860
- const row = d.query("SELECT * FROM entity_memories WHERE entity_id = ? AND memory_id = ?").get(entityId, memoryId);
861
- return parseEntityMemoryRow(row);
989
+ d.run("UPDATE memories SET access_count = access_count + 1, accessed_at = ? WHERE id = ?", [now(), id]);
862
990
  }
863
- function unlinkEntityFromMemory(entityId, memoryId, db) {
991
+ function incrementRecallCount(id, db) {
864
992
  const d = db || getDatabase();
865
- d.run("DELETE FROM entity_memories WHERE entity_id = ? AND memory_id = ?", [entityId, memoryId]);
993
+ try {
994
+ d.run("UPDATE memories SET recall_count = recall_count + 1, access_count = access_count + 1, accessed_at = ? WHERE id = ?", [now(), id]);
995
+ const row = d.query("SELECT recall_count, importance FROM memories WHERE id = ?").get(id);
996
+ if (!row)
997
+ return;
998
+ const promotions = Math.floor(row.recall_count / RECALL_PROMOTE_THRESHOLD);
999
+ if (promotions > 0 && row.importance < 10) {
1000
+ const newImportance = Math.min(10, row.importance + 1);
1001
+ d.run("UPDATE memories SET importance = ? WHERE id = ? AND importance < 10", [newImportance, id]);
1002
+ }
1003
+ } catch {}
866
1004
  }
867
- function getMemoriesForEntity(entityId, db) {
1005
+ function cleanExpiredMemories(db) {
868
1006
  const d = db || getDatabase();
869
- const rows = d.query(`SELECT m.* FROM memories m
870
- INNER JOIN entity_memories em ON em.memory_id = m.id
871
- WHERE em.entity_id = ?
872
- ORDER BY m.importance DESC, m.created_at DESC`).all(entityId);
873
- return rows.map(parseMemoryRow);
1007
+ const timestamp = now();
1008
+ const countRow = d.query("SELECT COUNT(*) as c FROM memories WHERE expires_at IS NOT NULL AND expires_at < ?").get(timestamp);
1009
+ const count = countRow.c;
1010
+ if (count > 0) {
1011
+ d.run("DELETE FROM memories WHERE expires_at IS NOT NULL AND expires_at < ?", [timestamp]);
1012
+ }
1013
+ return count;
874
1014
  }
875
- function getEntityMemoryLinks(entityId, memoryId, db) {
1015
+ function getMemoryVersions(memoryId, db) {
876
1016
  const d = db || getDatabase();
877
- const conditions = [];
878
- const params = [];
879
- if (entityId) {
880
- conditions.push("entity_id = ?");
881
- params.push(entityId);
882
- }
883
- if (memoryId) {
884
- conditions.push("memory_id = ?");
885
- params.push(memoryId);
886
- }
887
- let sql = "SELECT * FROM entity_memories";
888
- if (conditions.length > 0) {
889
- sql += ` WHERE ${conditions.join(" AND ")}`;
1017
+ try {
1018
+ const rows = d.query("SELECT * FROM memory_versions WHERE memory_id = ? ORDER BY version ASC").all(memoryId);
1019
+ return rows.map((row) => ({
1020
+ id: row["id"],
1021
+ memory_id: row["memory_id"],
1022
+ version: row["version"],
1023
+ value: row["value"],
1024
+ importance: row["importance"],
1025
+ scope: row["scope"],
1026
+ category: row["category"],
1027
+ tags: JSON.parse(row["tags"] || "[]"),
1028
+ summary: row["summary"] || null,
1029
+ pinned: !!row["pinned"],
1030
+ status: row["status"],
1031
+ created_at: row["created_at"]
1032
+ }));
1033
+ } catch {
1034
+ return [];
890
1035
  }
891
- sql += " ORDER BY created_at DESC";
892
- const rows = d.query(sql).all(...params);
893
- return rows.map(parseEntityMemoryRow);
894
1036
  }
1037
+ var RECALL_PROMOTE_THRESHOLD = 3;
1038
+ var init_memories = __esm(() => {
1039
+ init_types();
1040
+ init_database();
1041
+ init_redact();
1042
+ init_hooks();
1043
+ init_entity_memories();
1044
+ });
895
1045
 
896
- // src/db/relations.ts
897
- function parseRelationRow(row) {
898
- return {
899
- id: row["id"],
900
- source_entity_id: row["source_entity_id"],
901
- target_entity_id: row["target_entity_id"],
902
- relation_type: row["relation_type"],
903
- weight: row["weight"],
904
- metadata: JSON.parse(row["metadata"] || "{}"),
905
- created_at: row["created_at"]
906
- };
907
- }
908
- function parseEntityRow2(row) {
1046
+ // src/db/entities.ts
1047
+ function parseEntityRow(row) {
909
1048
  return {
910
1049
  id: row["id"],
911
1050
  name: row["name"],
@@ -917,307 +1056,164 @@ function parseEntityRow2(row) {
917
1056
  updated_at: row["updated_at"]
918
1057
  };
919
1058
  }
920
- function createRelation(input, db) {
1059
+ function createEntity(input, db) {
921
1060
  const d = db || getDatabase();
922
- const id = shortUuid();
923
1061
  const timestamp = now();
924
- const weight = input.weight ?? 1;
925
- const metadata = JSON.stringify(input.metadata ?? {});
926
- d.run(`INSERT INTO relations (id, source_entity_id, target_entity_id, relation_type, weight, metadata, created_at)
927
- VALUES (?, ?, ?, ?, ?, ?, ?)
928
- ON CONFLICT(source_entity_id, target_entity_id, relation_type)
929
- DO UPDATE SET weight = excluded.weight, metadata = excluded.metadata`, [id, input.source_entity_id, input.target_entity_id, input.relation_type, weight, metadata, timestamp]);
930
- const row = d.query(`SELECT * FROM relations
931
- WHERE source_entity_id = ? AND target_entity_id = ? AND relation_type = ?`).get(input.source_entity_id, input.target_entity_id, input.relation_type);
932
- return parseRelationRow(row);
1062
+ const metadataJson = JSON.stringify(input.metadata || {});
1063
+ const existing = d.query(`SELECT * FROM entities
1064
+ WHERE name = ? AND type = ? AND COALESCE(project_id, '') = ?`).get(input.name, input.type, input.project_id || "");
1065
+ if (existing) {
1066
+ const sets = ["updated_at = ?"];
1067
+ const params = [timestamp];
1068
+ if (input.description !== undefined) {
1069
+ sets.push("description = ?");
1070
+ params.push(input.description);
1071
+ }
1072
+ if (input.metadata !== undefined) {
1073
+ sets.push("metadata = ?");
1074
+ params.push(metadataJson);
1075
+ }
1076
+ const existingId = existing["id"];
1077
+ params.push(existingId);
1078
+ d.run(`UPDATE entities SET ${sets.join(", ")} WHERE id = ?`, params);
1079
+ return getEntity(existingId, d);
1080
+ }
1081
+ const id = shortUuid();
1082
+ d.run(`INSERT INTO entities (id, name, type, description, metadata, project_id, created_at, updated_at)
1083
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
1084
+ id,
1085
+ input.name,
1086
+ input.type,
1087
+ input.description || null,
1088
+ metadataJson,
1089
+ input.project_id || null,
1090
+ timestamp,
1091
+ timestamp
1092
+ ]);
1093
+ hookRegistry.runHooks("PostEntityCreate", {
1094
+ entityId: id,
1095
+ name: input.name,
1096
+ entityType: input.type,
1097
+ projectId: input.project_id,
1098
+ timestamp: Date.now()
1099
+ });
1100
+ return getEntity(id, d);
933
1101
  }
934
- function getRelation(id, db) {
1102
+ function getEntity(id, db) {
935
1103
  const d = db || getDatabase();
936
- const row = d.query("SELECT * FROM relations WHERE id = ?").get(id);
1104
+ const row = d.query("SELECT * FROM entities WHERE id = ?").get(id);
937
1105
  if (!row)
938
- throw new Error(`Relation not found: ${id}`);
939
- return parseRelationRow(row);
1106
+ throw new EntityNotFoundError(id);
1107
+ return parseEntityRow(row);
940
1108
  }
941
- function listRelations(filter, db) {
1109
+ function getEntityByName(name, type, projectId, db) {
942
1110
  const d = db || getDatabase();
943
- const conditions = [];
944
- const params = [];
945
- if (filter.entity_id) {
946
- const dir = filter.direction || "both";
947
- if (dir === "outgoing") {
948
- conditions.push("source_entity_id = ?");
949
- params.push(filter.entity_id);
950
- } else if (dir === "incoming") {
951
- conditions.push("target_entity_id = ?");
952
- params.push(filter.entity_id);
953
- } else {
954
- conditions.push("(source_entity_id = ? OR target_entity_id = ?)");
955
- params.push(filter.entity_id, filter.entity_id);
956
- }
1111
+ let sql = "SELECT * FROM entities WHERE name = ?";
1112
+ const params = [name];
1113
+ if (type) {
1114
+ sql += " AND type = ?";
1115
+ params.push(type);
957
1116
  }
958
- if (filter.relation_type) {
959
- conditions.push("relation_type = ?");
960
- params.push(filter.relation_type);
1117
+ if (projectId !== undefined) {
1118
+ sql += " AND project_id = ?";
1119
+ params.push(projectId);
961
1120
  }
962
- const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
963
- const rows = d.query(`SELECT * FROM relations ${where} ORDER BY created_at DESC`).all(...params);
964
- return rows.map(parseRelationRow);
965
- }
966
- function deleteRelation(id, db) {
967
- const d = db || getDatabase();
968
- const result = d.run("DELETE FROM relations WHERE id = ?", [id]);
969
- if (result.changes === 0)
970
- throw new Error(`Relation not found: ${id}`);
1121
+ sql += " LIMIT 1";
1122
+ const row = d.query(sql).get(...params);
1123
+ if (!row)
1124
+ return null;
1125
+ return parseEntityRow(row);
971
1126
  }
972
- function getEntityGraph(entityId, depth = 2, db) {
1127
+ function listEntities(filter = {}, db) {
973
1128
  const d = db || getDatabase();
974
- const entityRows = d.query(`WITH RECURSIVE graph(id, depth) AS (
975
- VALUES(?, 0)
976
- UNION
977
- SELECT CASE WHEN r.source_entity_id = g.id THEN r.target_entity_id ELSE r.source_entity_id END, g.depth + 1
978
- FROM relations r JOIN graph g ON (r.source_entity_id = g.id OR r.target_entity_id = g.id)
979
- WHERE g.depth < ?
980
- )
981
- SELECT DISTINCT e.* FROM entities e JOIN graph g ON e.id = g.id`).all(entityId, depth);
982
- const entities = entityRows.map(parseEntityRow2);
983
- const entityIds = new Set(entities.map((e) => e.id));
984
- if (entityIds.size === 0) {
985
- return { entities: [], relations: [] };
1129
+ const conditions = [];
1130
+ const params = [];
1131
+ if (filter.type) {
1132
+ conditions.push("type = ?");
1133
+ params.push(filter.type);
986
1134
  }
987
- const placeholders = Array.from(entityIds).map(() => "?").join(",");
988
- const relationRows = d.query(`SELECT * FROM relations
989
- WHERE source_entity_id IN (${placeholders})
990
- AND target_entity_id IN (${placeholders})`).all(...Array.from(entityIds), ...Array.from(entityIds));
991
- const relations = relationRows.map(parseRelationRow);
992
- return { entities, relations };
993
- }
994
- function findPath(fromEntityId, toEntityId, maxDepth = 5, db) {
995
- const d = db || getDatabase();
996
- const rows = d.query(`WITH RECURSIVE path(id, trail, depth) AS (
997
- SELECT ?, ?, 0
998
- UNION
999
- SELECT
1000
- CASE WHEN r.source_entity_id = p.id THEN r.target_entity_id ELSE r.source_entity_id END,
1001
- p.trail || ',' || CASE WHEN r.source_entity_id = p.id THEN r.target_entity_id ELSE r.source_entity_id END,
1002
- p.depth + 1
1003
- FROM relations r JOIN path p ON (r.source_entity_id = p.id OR r.target_entity_id = p.id)
1004
- WHERE p.depth < ?
1005
- AND INSTR(p.trail, CASE WHEN r.source_entity_id = p.id THEN r.target_entity_id ELSE r.source_entity_id END) = 0
1006
- )
1007
- SELECT trail FROM path WHERE id = ? ORDER BY depth ASC LIMIT 1`).get(fromEntityId, fromEntityId, maxDepth, toEntityId);
1008
- if (!rows)
1009
- return null;
1010
- const ids = rows.trail.split(",");
1011
- const entities = [];
1012
- for (const id of ids) {
1013
- const row = d.query("SELECT * FROM entities WHERE id = ?").get(id);
1014
- if (row)
1015
- entities.push(parseEntityRow2(row));
1135
+ if (filter.project_id) {
1136
+ conditions.push("project_id = ?");
1137
+ params.push(filter.project_id);
1016
1138
  }
1017
- return entities.length > 0 ? entities : null;
1018
- }
1019
-
1020
- // src/lib/config.ts
1021
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, readdirSync, writeFileSync, unlinkSync } from "fs";
1022
- import { homedir } from "os";
1023
- import { basename, dirname as dirname2, join as join2, resolve as resolve2 } from "path";
1024
- var DEFAULT_CONFIG = {
1025
- default_scope: "private",
1026
- default_category: "knowledge",
1027
- default_importance: 5,
1028
- max_entries: 1000,
1029
- max_entries_per_scope: {
1030
- global: 500,
1031
- shared: 300,
1032
- private: 200
1033
- },
1034
- injection: {
1035
- max_tokens: 500,
1036
- min_importance: 5,
1037
- categories: ["preference", "fact"],
1038
- refresh_interval: 5
1039
- },
1040
- extraction: {
1041
- enabled: true,
1042
- min_confidence: 0.5
1043
- },
1044
- sync_agents: ["claude", "codex", "gemini"],
1045
- auto_cleanup: {
1046
- enabled: true,
1047
- expired_check_interval: 3600,
1048
- unused_archive_days: 7,
1049
- stale_deprioritize_days: 14
1139
+ if (filter.search) {
1140
+ conditions.push("(name LIKE ? OR description LIKE ?)");
1141
+ const term = `%${filter.search}%`;
1142
+ params.push(term, term);
1050
1143
  }
1051
- };
1052
- function deepMerge(target, source) {
1053
- const result = { ...target };
1054
- for (const key of Object.keys(source)) {
1055
- const sourceVal = source[key];
1056
- const targetVal = result[key];
1057
- if (sourceVal !== null && typeof sourceVal === "object" && !Array.isArray(sourceVal) && targetVal !== null && typeof targetVal === "object" && !Array.isArray(targetVal)) {
1058
- result[key] = deepMerge(targetVal, sourceVal);
1059
- } else {
1060
- result[key] = sourceVal;
1061
- }
1144
+ let sql = "SELECT * FROM entities";
1145
+ if (conditions.length > 0) {
1146
+ sql += ` WHERE ${conditions.join(" AND ")}`;
1062
1147
  }
1063
- return result;
1064
- }
1065
- var VALID_SCOPES = ["global", "shared", "private"];
1066
- var VALID_CATEGORIES = [
1067
- "preference",
1068
- "fact",
1069
- "knowledge",
1070
- "history"
1071
- ];
1072
- function isValidScope(value) {
1073
- return VALID_SCOPES.includes(value);
1074
- }
1075
- function isValidCategory(value) {
1076
- return VALID_CATEGORIES.includes(value);
1148
+ sql += " ORDER BY updated_at DESC";
1149
+ if (filter.limit) {
1150
+ sql += " LIMIT ?";
1151
+ params.push(filter.limit);
1152
+ }
1153
+ if (filter.offset) {
1154
+ sql += " OFFSET ?";
1155
+ params.push(filter.offset);
1156
+ }
1157
+ const rows = d.query(sql).all(...params);
1158
+ return rows.map(parseEntityRow);
1077
1159
  }
1078
- function loadConfig() {
1079
- const configPath = join2(homedir(), ".mementos", "config.json");
1080
- let fileConfig = {};
1081
- if (existsSync2(configPath)) {
1082
- try {
1083
- const raw = readFileSync(configPath, "utf-8");
1084
- fileConfig = JSON.parse(raw);
1085
- } catch {}
1160
+ function updateEntity(id, input, db) {
1161
+ const d = db || getDatabase();
1162
+ const existing = d.query("SELECT id FROM entities WHERE id = ?").get(id);
1163
+ if (!existing)
1164
+ throw new EntityNotFoundError(id);
1165
+ const sets = ["updated_at = ?"];
1166
+ const params = [now()];
1167
+ if (input.name !== undefined) {
1168
+ sets.push("name = ?");
1169
+ params.push(input.name);
1086
1170
  }
1087
- const merged = deepMerge(DEFAULT_CONFIG, fileConfig);
1088
- const envScope = process.env["MEMENTOS_DEFAULT_SCOPE"];
1089
- if (envScope && isValidScope(envScope)) {
1090
- merged.default_scope = envScope;
1171
+ if (input.type !== undefined) {
1172
+ sets.push("type = ?");
1173
+ params.push(input.type);
1091
1174
  }
1092
- const envCategory = process.env["MEMENTOS_DEFAULT_CATEGORY"];
1093
- if (envCategory && isValidCategory(envCategory)) {
1094
- merged.default_category = envCategory;
1175
+ if (input.description !== undefined) {
1176
+ sets.push("description = ?");
1177
+ params.push(input.description);
1095
1178
  }
1096
- const envImportance = process.env["MEMENTOS_DEFAULT_IMPORTANCE"];
1097
- if (envImportance) {
1098
- const parsed = parseInt(envImportance, 10);
1099
- if (!Number.isNaN(parsed) && parsed >= 1 && parsed <= 10) {
1100
- merged.default_importance = parsed;
1101
- }
1179
+ if (input.metadata !== undefined) {
1180
+ sets.push("metadata = ?");
1181
+ params.push(JSON.stringify(input.metadata));
1102
1182
  }
1103
- return merged;
1183
+ params.push(id);
1184
+ d.run(`UPDATE entities SET ${sets.join(", ")} WHERE id = ?`, params);
1185
+ return getEntity(id, d);
1104
1186
  }
1105
- function findFileWalkingUp(filename) {
1106
- let dir = process.cwd();
1107
- while (true) {
1108
- const candidate = join2(dir, filename);
1109
- if (existsSync2(candidate)) {
1110
- return candidate;
1111
- }
1112
- const parent = dirname2(dir);
1113
- if (parent === dir) {
1114
- return null;
1115
- }
1116
- dir = parent;
1117
- }
1118
- }
1119
- function findGitRoot2() {
1120
- let dir = process.cwd();
1121
- while (true) {
1122
- if (existsSync2(join2(dir, ".git"))) {
1123
- return dir;
1124
- }
1125
- const parent = dirname2(dir);
1126
- if (parent === dir) {
1127
- return null;
1128
- }
1129
- dir = parent;
1130
- }
1131
- }
1132
- function profilesDir() {
1133
- return join2(homedir(), ".mementos", "profiles");
1134
- }
1135
- function globalConfigPath() {
1136
- return join2(homedir(), ".mementos", "config.json");
1137
- }
1138
- function readGlobalConfig() {
1139
- const p = globalConfigPath();
1140
- if (!existsSync2(p))
1141
- return {};
1142
- try {
1143
- return JSON.parse(readFileSync(p, "utf-8"));
1144
- } catch {
1145
- return {};
1146
- }
1147
- }
1148
- function getActiveProfile() {
1149
- const envProfile = process.env["MEMENTOS_PROFILE"];
1150
- if (envProfile)
1151
- return envProfile.trim();
1152
- const cfg = readGlobalConfig();
1153
- return cfg["active_profile"] || null;
1154
- }
1155
- function listProfiles() {
1156
- const dir = profilesDir();
1157
- if (!existsSync2(dir))
1158
- return [];
1159
- return readdirSync(dir).filter((f) => f.endsWith(".db")).map((f) => basename(f, ".db")).sort();
1160
- }
1161
- function getDbPath2() {
1162
- const envDbPath = process.env["MEMENTOS_DB_PATH"];
1163
- if (envDbPath) {
1164
- const resolved = resolve2(envDbPath);
1165
- ensureDir2(dirname2(resolved));
1166
- return resolved;
1167
- }
1168
- const profile = getActiveProfile();
1169
- if (profile) {
1170
- const profilePath = join2(profilesDir(), `${profile}.db`);
1171
- ensureDir2(dirname2(profilePath));
1172
- return profilePath;
1173
- }
1174
- const dbScope = process.env["MEMENTOS_DB_SCOPE"];
1175
- if (dbScope === "project") {
1176
- const gitRoot = findGitRoot2();
1177
- if (gitRoot) {
1178
- const dbPath = join2(gitRoot, ".mementos", "mementos.db");
1179
- ensureDir2(dirname2(dbPath));
1180
- return dbPath;
1181
- }
1182
- }
1183
- const found = findFileWalkingUp(join2(".mementos", "mementos.db"));
1184
- if (found) {
1185
- return found;
1186
- }
1187
- const fallback = join2(homedir(), ".mementos", "mementos.db");
1188
- ensureDir2(dirname2(fallback));
1189
- return fallback;
1187
+ function deleteEntity(id, db) {
1188
+ const d = db || getDatabase();
1189
+ const result = d.run("DELETE FROM entities WHERE id = ?", [id]);
1190
+ if (result.changes === 0)
1191
+ throw new EntityNotFoundError(id);
1190
1192
  }
1191
- function ensureDir2(dir) {
1192
- if (!existsSync2(dir)) {
1193
- mkdirSync2(dir, { recursive: true });
1194
- }
1193
+ function mergeEntities(sourceId, targetId, db) {
1194
+ const d = db || getDatabase();
1195
+ getEntity(sourceId, d);
1196
+ getEntity(targetId, d);
1197
+ d.run(`UPDATE OR IGNORE relations SET source_entity_id = ? WHERE source_entity_id = ?`, [targetId, sourceId]);
1198
+ d.run(`UPDATE OR IGNORE relations SET target_entity_id = ? WHERE target_entity_id = ?`, [targetId, sourceId]);
1199
+ d.run("DELETE FROM relations WHERE source_entity_id = ? OR target_entity_id = ?", [
1200
+ sourceId,
1201
+ sourceId
1202
+ ]);
1203
+ d.run(`UPDATE OR IGNORE entity_memories SET entity_id = ? WHERE entity_id = ?`, [targetId, sourceId]);
1204
+ d.run("DELETE FROM entity_memories WHERE entity_id = ?", [sourceId]);
1205
+ d.run("DELETE FROM entities WHERE id = ?", [sourceId]);
1206
+ d.run("UPDATE entities SET updated_at = ? WHERE id = ?", [now(), targetId]);
1207
+ return getEntity(targetId, d);
1195
1208
  }
1209
+ var init_entities = __esm(() => {
1210
+ init_database();
1211
+ init_types();
1212
+ init_hooks();
1213
+ });
1196
1214
 
1197
- // src/db/memories.ts
1198
- function runEntityExtraction(memory, projectId, d) {
1199
- const config = loadConfig();
1200
- if (config.extraction?.enabled === false)
1201
- return;
1202
- const extracted = extractEntities(memory, d);
1203
- const minConfidence = config.extraction?.min_confidence ?? 0.5;
1204
- const entityIds = [];
1205
- for (const ext of extracted) {
1206
- if (ext.confidence >= minConfidence) {
1207
- const entity = createEntity({ name: ext.name, type: ext.type, project_id: projectId }, d);
1208
- linkEntityToMemory(entity.id, memory.id, "context", d);
1209
- entityIds.push(entity.id);
1210
- }
1211
- }
1212
- for (let i = 0;i < entityIds.length; i++) {
1213
- for (let j = i + 1;j < entityIds.length; j++) {
1214
- try {
1215
- createRelation({ source_entity_id: entityIds[i], target_entity_id: entityIds[j], relation_type: "related_to" }, d);
1216
- } catch {}
1217
- }
1218
- }
1219
- }
1220
- function parseMemoryRow(row) {
1215
+ // src/lib/search.ts
1216
+ function parseMemoryRow2(row) {
1221
1217
  return {
1222
1218
  id: row["id"],
1223
1219
  key: row["key"],
@@ -1242,923 +1238,1730 @@ function parseMemoryRow(row) {
1242
1238
  accessed_at: row["accessed_at"] || null
1243
1239
  };
1244
1240
  }
1245
- function createMemory(input, dedupeMode = "merge", db) {
1246
- const d = db || getDatabase();
1247
- const timestamp = now();
1248
- let expiresAt = input.expires_at || null;
1249
- if (input.ttl_ms && !expiresAt) {
1250
- expiresAt = new Date(Date.now() + input.ttl_ms).toISOString();
1251
- }
1252
- const id = uuid();
1253
- const tags = input.tags || [];
1254
- const tagsJson = JSON.stringify(tags);
1255
- const metadataJson = JSON.stringify(input.metadata || {});
1256
- const safeValue = redactSecrets(input.value);
1257
- const safeSummary = input.summary ? redactSecrets(input.summary) : null;
1258
- if (dedupeMode === "merge") {
1259
- const existing = d.query(`SELECT id, version FROM memories
1260
- WHERE key = ? AND scope = ?
1261
- AND COALESCE(agent_id, '') = ?
1262
- AND COALESCE(project_id, '') = ?
1263
- AND COALESCE(session_id, '') = ?`).get(input.key, input.scope || "private", input.agent_id || "", input.project_id || "", input.session_id || "");
1264
- if (existing) {
1265
- d.run(`UPDATE memories SET
1266
- value = ?, category = ?, summary = ?, tags = ?,
1267
- importance = ?, metadata = ?, expires_at = ?,
1268
- pinned = COALESCE(pinned, 0),
1269
- version = version + 1, updated_at = ?
1270
- WHERE id = ?`, [
1271
- safeValue,
1272
- input.category || "knowledge",
1273
- safeSummary,
1274
- tagsJson,
1275
- input.importance ?? 5,
1276
- metadataJson,
1277
- expiresAt,
1278
- timestamp,
1279
- existing.id
1280
- ]);
1281
- d.run("DELETE FROM memory_tags WHERE memory_id = ?", [existing.id]);
1282
- const insertTag2 = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
1283
- for (const tag of tags) {
1284
- insertTag2.run(existing.id, tag);
1241
+ function preprocessQuery(query) {
1242
+ let q = query.trim();
1243
+ q = q.replace(/\s+/g, " ");
1244
+ q = q.normalize("NFC");
1245
+ return q;
1246
+ }
1247
+ function escapeLikePattern(s) {
1248
+ return s.replace(/%/g, "\\%").replace(/_/g, "\\_");
1249
+ }
1250
+ function removeStopWords(tokens) {
1251
+ if (tokens.length <= 1)
1252
+ return tokens;
1253
+ const filtered = tokens.filter((t) => !STOP_WORDS.has(t.toLowerCase()));
1254
+ return filtered.length > 0 ? filtered : tokens;
1255
+ }
1256
+ function extractHighlights(memory, queryLower) {
1257
+ const highlights = [];
1258
+ const tokens = queryLower.split(/\s+/).filter(Boolean);
1259
+ for (const field of ["key", "value", "summary"]) {
1260
+ const text = field === "summary" ? memory.summary : memory[field];
1261
+ if (!text)
1262
+ continue;
1263
+ const textLower = text.toLowerCase();
1264
+ const searchTerms = [queryLower, ...tokens].filter(Boolean);
1265
+ for (const term of searchTerms) {
1266
+ const idx = textLower.indexOf(term);
1267
+ if (idx !== -1) {
1268
+ const start = Math.max(0, idx - 30);
1269
+ const end = Math.min(text.length, idx + term.length + 30);
1270
+ const prefix = start > 0 ? "..." : "";
1271
+ const suffix = end < text.length ? "..." : "";
1272
+ highlights.push({
1273
+ field,
1274
+ snippet: prefix + text.slice(start, end) + suffix
1275
+ });
1276
+ break;
1285
1277
  }
1286
- const merged = getMemory(existing.id, d);
1287
- try {
1288
- const oldLinks = getEntityMemoryLinks(undefined, merged.id, d);
1289
- for (const link of oldLinks) {
1290
- unlinkEntityFromMemory(link.entity_id, merged.id, d);
1291
- }
1292
- runEntityExtraction(merged, input.project_id, d);
1293
- } catch {}
1294
- return merged;
1295
1278
  }
1296
1279
  }
1297
- 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)
1298
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', 0, ?, ?, ?, ?, 0, 1, ?, ?, ?)`, [
1299
- id,
1300
- input.key,
1301
- input.value,
1302
- input.category || "knowledge",
1303
- input.scope || "private",
1304
- input.summary || null,
1305
- tagsJson,
1306
- input.importance ?? 5,
1307
- input.source || "agent",
1308
- input.agent_id || null,
1309
- input.project_id || null,
1310
- input.session_id || null,
1311
- metadataJson,
1312
- expiresAt,
1313
- timestamp,
1314
- timestamp
1315
- ]);
1316
- const insertTag = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
1317
- for (const tag of tags) {
1318
- insertTag.run(id, tag);
1280
+ for (const tag of memory.tags) {
1281
+ if (tag.toLowerCase().includes(queryLower) || tokens.some((t) => tag.toLowerCase().includes(t))) {
1282
+ highlights.push({ field: "tag", snippet: tag });
1283
+ }
1319
1284
  }
1320
- const memory = getMemory(id, d);
1321
- try {
1322
- runEntityExtraction(memory, input.project_id, d);
1323
- } catch {}
1324
- return memory;
1285
+ return highlights;
1325
1286
  }
1326
- function getMemory(id, db) {
1327
- const d = db || getDatabase();
1328
- const row = d.query("SELECT * FROM memories WHERE id = ?").get(id);
1329
- if (!row)
1330
- return null;
1331
- return parseMemoryRow(row);
1287
+ function determineMatchType(memory, queryLower) {
1288
+ if (memory.key.toLowerCase() === queryLower)
1289
+ return "exact";
1290
+ if (memory.tags.some((t) => t.toLowerCase() === queryLower))
1291
+ return "tag";
1292
+ if (memory.tags.some((t) => t.toLowerCase().includes(queryLower)))
1293
+ return "tag";
1294
+ return "fuzzy";
1332
1295
  }
1333
- function listMemories(filter, db) {
1334
- const d = db || getDatabase();
1335
- const conditions = [];
1336
- const params = [];
1337
- if (filter) {
1338
- if (filter.scope) {
1339
- if (Array.isArray(filter.scope)) {
1340
- conditions.push(`scope IN (${filter.scope.map(() => "?").join(",")})`);
1341
- params.push(...filter.scope);
1342
- } else {
1343
- conditions.push("scope = ?");
1344
- params.push(filter.scope);
1345
- }
1346
- }
1347
- if (filter.category) {
1348
- if (Array.isArray(filter.category)) {
1349
- conditions.push(`category IN (${filter.category.map(() => "?").join(",")})`);
1350
- params.push(...filter.category);
1351
- } else {
1352
- conditions.push("category = ?");
1353
- params.push(filter.category);
1296
+ function computeScore(memory, queryLower) {
1297
+ const fieldScores = [];
1298
+ const keyLower = memory.key.toLowerCase();
1299
+ if (keyLower === queryLower) {
1300
+ fieldScores.push(10);
1301
+ } else if (keyLower.includes(queryLower)) {
1302
+ fieldScores.push(7);
1303
+ }
1304
+ if (memory.tags.some((t) => t.toLowerCase() === queryLower)) {
1305
+ fieldScores.push(6);
1306
+ } else if (memory.tags.some((t) => t.toLowerCase().includes(queryLower))) {
1307
+ fieldScores.push(3);
1308
+ }
1309
+ if (memory.summary && memory.summary.toLowerCase().includes(queryLower)) {
1310
+ fieldScores.push(4);
1311
+ }
1312
+ if (memory.value.toLowerCase().includes(queryLower)) {
1313
+ fieldScores.push(3);
1314
+ }
1315
+ const metadataStr = JSON.stringify(memory.metadata).toLowerCase();
1316
+ if (metadataStr !== "{}" && metadataStr.includes(queryLower)) {
1317
+ fieldScores.push(2);
1318
+ }
1319
+ fieldScores.sort((a, b) => b - a);
1320
+ const diminishingMultipliers = [1, 0.5, 0.25, 0.15, 0.15];
1321
+ let score = 0;
1322
+ for (let i = 0;i < fieldScores.length; i++) {
1323
+ score += fieldScores[i] * (diminishingMultipliers[i] ?? 0.15);
1324
+ }
1325
+ const { phrases } = extractQuotedPhrases(queryLower);
1326
+ for (const phrase of phrases) {
1327
+ if (keyLower.includes(phrase))
1328
+ score += 8;
1329
+ if (memory.value.toLowerCase().includes(phrase))
1330
+ score += 5;
1331
+ if (memory.summary && memory.summary.toLowerCase().includes(phrase))
1332
+ score += 4;
1333
+ }
1334
+ const { remainder } = extractQuotedPhrases(queryLower);
1335
+ const tokens = removeStopWords(remainder.split(/\s+/).filter(Boolean));
1336
+ if (tokens.length > 1) {
1337
+ let tokenScore = 0;
1338
+ for (const token of tokens) {
1339
+ if (keyLower === token) {
1340
+ tokenScore += 10 / tokens.length;
1341
+ } else if (keyLower.includes(token)) {
1342
+ tokenScore += 7 / tokens.length;
1354
1343
  }
1355
- }
1356
- if (filter.source) {
1357
- if (Array.isArray(filter.source)) {
1358
- conditions.push(`source IN (${filter.source.map(() => "?").join(",")})`);
1359
- params.push(...filter.source);
1360
- } else {
1361
- conditions.push("source = ?");
1362
- params.push(filter.source);
1344
+ if (memory.tags.some((t) => t.toLowerCase() === token)) {
1345
+ tokenScore += 6 / tokens.length;
1346
+ } else if (memory.tags.some((t) => t.toLowerCase().includes(token))) {
1347
+ tokenScore += 3 / tokens.length;
1363
1348
  }
1364
- }
1365
- if (filter.status) {
1366
- if (Array.isArray(filter.status)) {
1367
- conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
1368
- params.push(...filter.status);
1369
- } else {
1370
- conditions.push("status = ?");
1371
- params.push(filter.status);
1349
+ if (memory.summary && memory.summary.toLowerCase().includes(token)) {
1350
+ tokenScore += 4 / tokens.length;
1372
1351
  }
1373
- } else {
1374
- conditions.push("status = 'active'");
1375
- }
1376
- if (filter.project_id) {
1377
- conditions.push("project_id = ?");
1378
- params.push(filter.project_id);
1379
- }
1380
- if (filter.agent_id) {
1381
- conditions.push("agent_id = ?");
1382
- params.push(filter.agent_id);
1383
- }
1384
- if (filter.session_id) {
1385
- conditions.push("session_id = ?");
1386
- params.push(filter.session_id);
1387
- }
1388
- if (filter.min_importance) {
1389
- conditions.push("importance >= ?");
1390
- params.push(filter.min_importance);
1391
- }
1392
- if (filter.pinned !== undefined) {
1393
- conditions.push("pinned = ?");
1394
- params.push(filter.pinned ? 1 : 0);
1395
- }
1396
- if (filter.tags && filter.tags.length > 0) {
1397
- for (const tag of filter.tags) {
1398
- conditions.push("id IN (SELECT memory_id FROM memory_tags WHERE tag = ?)");
1399
- params.push(tag);
1352
+ if (memory.value.toLowerCase().includes(token)) {
1353
+ tokenScore += 3 / tokens.length;
1354
+ }
1355
+ if (metadataStr !== "{}" && metadataStr.includes(token)) {
1356
+ tokenScore += 2 / tokens.length;
1400
1357
  }
1401
1358
  }
1402
- if (filter.search) {
1403
- conditions.push("(key LIKE ? OR value LIKE ? OR summary LIKE ?)");
1404
- const term = `%${filter.search}%`;
1405
- params.push(term, term, term);
1359
+ if (score > 0) {
1360
+ score += tokenScore * 0.3;
1361
+ } else {
1362
+ score += tokenScore;
1406
1363
  }
1407
- } else {
1408
- conditions.push("status = 'active'");
1409
- }
1410
- let sql = "SELECT * FROM memories";
1411
- if (conditions.length > 0) {
1412
- sql += ` WHERE ${conditions.join(" AND ")}`;
1413
1364
  }
1414
- sql += " ORDER BY importance DESC, created_at DESC";
1415
- if (filter?.limit) {
1416
- sql += " LIMIT ?";
1417
- params.push(filter.limit);
1365
+ return score;
1366
+ }
1367
+ function extractQuotedPhrases(query) {
1368
+ const phrases = [];
1369
+ const remainder = query.replace(/"([^"]+)"/g, (_match, phrase) => {
1370
+ phrases.push(phrase);
1371
+ return "";
1372
+ });
1373
+ return { phrases, remainder: remainder.trim() };
1374
+ }
1375
+ function escapeFts5Query(query) {
1376
+ const { phrases, remainder } = extractQuotedPhrases(query);
1377
+ const parts = [];
1378
+ for (const phrase of phrases) {
1379
+ parts.push(`"${phrase.replace(/"/g, '""')}"`);
1418
1380
  }
1419
- if (filter?.offset) {
1420
- sql += " OFFSET ?";
1421
- params.push(filter.offset);
1381
+ const tokens = removeStopWords(remainder.split(/\s+/).filter(Boolean));
1382
+ for (const t of tokens) {
1383
+ parts.push(`"${t.replace(/"/g, '""')}"`);
1422
1384
  }
1423
- const rows = d.query(sql).all(...params);
1424
- return rows.map(parseMemoryRow);
1385
+ return parts.join(" ");
1425
1386
  }
1426
- function updateMemory(id, input, db) {
1427
- const d = db || getDatabase();
1428
- const existing = getMemory(id, d);
1429
- if (!existing)
1430
- throw new MemoryNotFoundError(id);
1431
- if (existing.version !== input.version) {
1432
- throw new VersionConflictError(id, input.version, existing.version);
1433
- }
1387
+ function hasFts5Table(d) {
1434
1388
  try {
1435
- d.run(`INSERT OR IGNORE INTO memory_versions (id, memory_id, version, value, importance, scope, category, tags, summary, pinned, status, created_at)
1436
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
1437
- uuid(),
1438
- existing.id,
1439
- existing.version,
1440
- existing.value,
1441
- existing.importance,
1442
- existing.scope,
1443
- existing.category,
1444
- JSON.stringify(existing.tags),
1445
- existing.summary,
1446
- existing.pinned ? 1 : 0,
1447
- existing.status,
1448
- existing.updated_at
1449
- ]);
1450
- } catch {}
1451
- const sets = ["version = version + 1", "updated_at = ?"];
1452
- const params = [now()];
1453
- if (input.value !== undefined) {
1454
- sets.push("value = ?");
1455
- params.push(redactSecrets(input.value));
1456
- }
1457
- if (input.category !== undefined) {
1458
- sets.push("category = ?");
1459
- params.push(input.category);
1389
+ const row = d.query("SELECT name FROM sqlite_master WHERE type='table' AND name='memories_fts'").get();
1390
+ return !!row;
1391
+ } catch {
1392
+ return false;
1460
1393
  }
1461
- if (input.scope !== undefined) {
1462
- sets.push("scope = ?");
1463
- params.push(input.scope);
1394
+ }
1395
+ function buildFilterConditions(filter) {
1396
+ const conditions = [];
1397
+ const params = [];
1398
+ conditions.push("m.status = 'active'");
1399
+ conditions.push("(m.expires_at IS NULL OR m.expires_at >= datetime('now'))");
1400
+ if (!filter)
1401
+ return { conditions, params };
1402
+ if (filter.scope) {
1403
+ if (Array.isArray(filter.scope)) {
1404
+ conditions.push(`m.scope IN (${filter.scope.map(() => "?").join(",")})`);
1405
+ params.push(...filter.scope);
1406
+ } else {
1407
+ conditions.push("m.scope = ?");
1408
+ params.push(filter.scope);
1409
+ }
1464
1410
  }
1465
- if (input.summary !== undefined) {
1466
- sets.push("summary = ?");
1467
- params.push(input.summary);
1411
+ if (filter.category) {
1412
+ if (Array.isArray(filter.category)) {
1413
+ conditions.push(`m.category IN (${filter.category.map(() => "?").join(",")})`);
1414
+ params.push(...filter.category);
1415
+ } else {
1416
+ conditions.push("m.category = ?");
1417
+ params.push(filter.category);
1418
+ }
1468
1419
  }
1469
- if (input.importance !== undefined) {
1470
- sets.push("importance = ?");
1471
- params.push(input.importance);
1420
+ if (filter.source) {
1421
+ if (Array.isArray(filter.source)) {
1422
+ conditions.push(`m.source IN (${filter.source.map(() => "?").join(",")})`);
1423
+ params.push(...filter.source);
1424
+ } else {
1425
+ conditions.push("m.source = ?");
1426
+ params.push(filter.source);
1427
+ }
1472
1428
  }
1473
- if (input.pinned !== undefined) {
1474
- sets.push("pinned = ?");
1475
- params.push(input.pinned ? 1 : 0);
1429
+ if (filter.status) {
1430
+ conditions.shift();
1431
+ if (Array.isArray(filter.status)) {
1432
+ conditions.push(`m.status IN (${filter.status.map(() => "?").join(",")})`);
1433
+ params.push(...filter.status);
1434
+ } else {
1435
+ conditions.push("m.status = ?");
1436
+ params.push(filter.status);
1437
+ }
1476
1438
  }
1477
- if (input.status !== undefined) {
1478
- sets.push("status = ?");
1479
- params.push(input.status);
1439
+ if (filter.project_id) {
1440
+ conditions.push("m.project_id = ?");
1441
+ params.push(filter.project_id);
1480
1442
  }
1481
- if (input.metadata !== undefined) {
1482
- sets.push("metadata = ?");
1483
- params.push(JSON.stringify(input.metadata));
1443
+ if (filter.agent_id) {
1444
+ conditions.push("m.agent_id = ?");
1445
+ params.push(filter.agent_id);
1484
1446
  }
1485
- if (input.expires_at !== undefined) {
1486
- sets.push("expires_at = ?");
1487
- params.push(input.expires_at);
1447
+ if (filter.session_id) {
1448
+ conditions.push("m.session_id = ?");
1449
+ params.push(filter.session_id);
1488
1450
  }
1489
- if (input.tags !== undefined) {
1490
- sets.push("tags = ?");
1491
- params.push(JSON.stringify(input.tags));
1492
- d.run("DELETE FROM memory_tags WHERE memory_id = ?", [id]);
1493
- const insertTag = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
1494
- for (const tag of input.tags) {
1495
- insertTag.run(id, tag);
1496
- }
1451
+ if (filter.min_importance) {
1452
+ conditions.push("m.importance >= ?");
1453
+ params.push(filter.min_importance);
1497
1454
  }
1498
- params.push(id);
1499
- d.run(`UPDATE memories SET ${sets.join(", ")} WHERE id = ?`, params);
1500
- const updated = getMemory(id, d);
1501
- try {
1502
- if (input.value !== undefined) {
1503
- const oldLinks = getEntityMemoryLinks(undefined, updated.id, d);
1504
- for (const link of oldLinks) {
1505
- unlinkEntityFromMemory(link.entity_id, updated.id, d);
1506
- }
1507
- runEntityExtraction(updated, existing.project_id || undefined, d);
1455
+ if (filter.pinned !== undefined) {
1456
+ conditions.push("m.pinned = ?");
1457
+ params.push(filter.pinned ? 1 : 0);
1458
+ }
1459
+ if (filter.tags && filter.tags.length > 0) {
1460
+ for (const tag of filter.tags) {
1461
+ conditions.push("m.id IN (SELECT memory_id FROM memory_tags WHERE tag = ?)");
1462
+ params.push(tag);
1508
1463
  }
1509
- } catch {}
1510
- return updated;
1511
- }
1512
- function deleteMemory(id, db) {
1513
- const d = db || getDatabase();
1514
- const result = d.run("DELETE FROM memories WHERE id = ?", [id]);
1515
- return result.changes > 0;
1516
- }
1517
- function touchMemory(id, db) {
1518
- const d = db || getDatabase();
1519
- d.run("UPDATE memories SET access_count = access_count + 1, accessed_at = ? WHERE id = ?", [now(), id]);
1520
- }
1521
- function cleanExpiredMemories(db) {
1522
- const d = db || getDatabase();
1523
- const timestamp = now();
1524
- const countRow = d.query("SELECT COUNT(*) as c FROM memories WHERE expires_at IS NOT NULL AND expires_at < ?").get(timestamp);
1525
- const count = countRow.c;
1526
- if (count > 0) {
1527
- d.run("DELETE FROM memories WHERE expires_at IS NOT NULL AND expires_at < ?", [timestamp]);
1528
1464
  }
1529
- return count;
1465
+ return { conditions, params };
1530
1466
  }
1531
- function getMemoryVersions(memoryId, db) {
1532
- const d = db || getDatabase();
1467
+ function searchWithFts5(d, query, queryLower, filter, graphBoostedIds) {
1468
+ const ftsQuery = escapeFts5Query(query);
1469
+ if (!ftsQuery)
1470
+ return null;
1533
1471
  try {
1534
- const rows = d.query("SELECT * FROM memory_versions WHERE memory_id = ? ORDER BY version ASC").all(memoryId);
1535
- return rows.map((row) => ({
1536
- id: row["id"],
1537
- memory_id: row["memory_id"],
1538
- version: row["version"],
1539
- value: row["value"],
1540
- importance: row["importance"],
1541
- scope: row["scope"],
1542
- category: row["category"],
1543
- tags: JSON.parse(row["tags"] || "[]"),
1544
- summary: row["summary"] || null,
1545
- pinned: !!row["pinned"],
1546
- status: row["status"],
1547
- created_at: row["created_at"]
1548
- }));
1472
+ const { conditions, params } = buildFilterConditions(filter);
1473
+ const queryParam = `%${query}%`;
1474
+ 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 ?)`;
1475
+ const allConditions = [ftsCondition, ...conditions];
1476
+ const allParams = [ftsQuery, queryParam, queryParam, ...params];
1477
+ const candidateSql = `SELECT m.* FROM memories m WHERE ${allConditions.join(" AND ")}`;
1478
+ const rows = d.query(candidateSql).all(...allParams);
1479
+ return scoreResults(rows, queryLower, graphBoostedIds);
1549
1480
  } catch {
1550
- return [];
1481
+ return null;
1551
1482
  }
1552
1483
  }
1553
-
1554
- // src/db/locks.ts
1555
- function parseLockRow(row) {
1556
- return {
1557
- id: row["id"],
1558
- resource_type: row["resource_type"],
1559
- resource_id: row["resource_id"],
1560
- agent_id: row["agent_id"],
1561
- lock_type: row["lock_type"],
1562
- locked_at: row["locked_at"],
1563
- expires_at: row["expires_at"]
1564
- };
1484
+ function searchWithLike(d, query, queryLower, filter, graphBoostedIds) {
1485
+ const { conditions, params } = buildFilterConditions(filter);
1486
+ const rawTokens = query.trim().split(/\s+/).filter(Boolean);
1487
+ const tokens = removeStopWords(rawTokens);
1488
+ const escapedQuery = escapeLikePattern(query);
1489
+ const likePatterns = [`%${escapedQuery}%`];
1490
+ if (tokens.length > 1) {
1491
+ for (const t of tokens)
1492
+ likePatterns.push(`%${escapeLikePattern(t)}%`);
1493
+ }
1494
+ const fieldClauses = [];
1495
+ for (const pattern of likePatterns) {
1496
+ fieldClauses.push("m.key LIKE ? ESCAPE '\\'");
1497
+ params.push(pattern);
1498
+ fieldClauses.push("m.value LIKE ? ESCAPE '\\'");
1499
+ params.push(pattern);
1500
+ fieldClauses.push("m.summary LIKE ? ESCAPE '\\'");
1501
+ params.push(pattern);
1502
+ fieldClauses.push("m.metadata LIKE ? ESCAPE '\\'");
1503
+ params.push(pattern);
1504
+ fieldClauses.push("m.id IN (SELECT memory_id FROM memory_tags WHERE tag LIKE ? ESCAPE '\\')");
1505
+ params.push(pattern);
1506
+ }
1507
+ conditions.push(`(${fieldClauses.join(" OR ")})`);
1508
+ const sql = `SELECT DISTINCT m.* FROM memories m WHERE ${conditions.join(" AND ")}`;
1509
+ const rows = d.query(sql).all(...params);
1510
+ return scoreResults(rows, queryLower, graphBoostedIds);
1565
1511
  }
1566
- function acquireLock(agentId, resourceType, resourceId, lockType = "exclusive", ttlSeconds = 300, db) {
1567
- const d = db || getDatabase();
1568
- cleanExpiredLocks(d);
1569
- const ownLock = d.query("SELECT * FROM resource_locks WHERE resource_type = ? AND resource_id = ? AND agent_id = ? AND lock_type = ? AND expires_at > datetime('now')").get(resourceType, resourceId, agentId, lockType);
1570
- if (ownLock) {
1571
- const newExpiry = new Date(Date.now() + ttlSeconds * 1000).toISOString();
1572
- d.run("UPDATE resource_locks SET expires_at = ? WHERE id = ?", [
1573
- newExpiry,
1574
- ownLock["id"]
1575
- ]);
1576
- return parseLockRow({ ...ownLock, expires_at: newExpiry });
1512
+ function generateTrigrams(s) {
1513
+ const lower = s.toLowerCase();
1514
+ const trigrams = new Set;
1515
+ for (let i = 0;i <= lower.length - 3; i++) {
1516
+ trigrams.add(lower.slice(i, i + 3));
1577
1517
  }
1578
- if (lockType === "exclusive") {
1579
- const existing = d.query("SELECT * FROM resource_locks WHERE resource_type = ? AND resource_id = ? AND lock_type = 'exclusive' AND agent_id != ? AND expires_at > datetime('now')").get(resourceType, resourceId, agentId);
1580
- if (existing) {
1581
- return null;
1518
+ return trigrams;
1519
+ }
1520
+ function trigramSimilarity(a, b) {
1521
+ const triA = generateTrigrams(a);
1522
+ const triB = generateTrigrams(b);
1523
+ if (triA.size === 0 || triB.size === 0)
1524
+ return 0;
1525
+ let intersection = 0;
1526
+ for (const t of triA) {
1527
+ if (triB.has(t))
1528
+ intersection++;
1529
+ }
1530
+ const union = triA.size + triB.size - intersection;
1531
+ return union === 0 ? 0 : intersection / union;
1532
+ }
1533
+ function searchWithFuzzy(d, query, filter, graphBoostedIds) {
1534
+ const { conditions, params } = buildFilterConditions(filter);
1535
+ const sql = `SELECT m.* FROM memories m WHERE ${conditions.join(" AND ")}`;
1536
+ const rows = d.query(sql).all(...params);
1537
+ const MIN_SIMILARITY = 0.3;
1538
+ const results = [];
1539
+ for (const row of rows) {
1540
+ const memory = parseMemoryRow2(row);
1541
+ let bestSimilarity = 0;
1542
+ bestSimilarity = Math.max(bestSimilarity, trigramSimilarity(query, memory.key));
1543
+ bestSimilarity = Math.max(bestSimilarity, trigramSimilarity(query, memory.value.slice(0, 200)));
1544
+ if (memory.summary) {
1545
+ bestSimilarity = Math.max(bestSimilarity, trigramSimilarity(query, memory.summary));
1546
+ }
1547
+ for (const tag of memory.tags) {
1548
+ bestSimilarity = Math.max(bestSimilarity, trigramSimilarity(query, tag));
1549
+ }
1550
+ if (bestSimilarity >= MIN_SIMILARITY) {
1551
+ const graphBoost = graphBoostedIds?.has(memory.id) ? 2 : 0;
1552
+ const score = bestSimilarity * 5 * memory.importance / 10 + graphBoost;
1553
+ results.push({ memory, score, match_type: "fuzzy" });
1582
1554
  }
1583
1555
  }
1584
- const id = shortUuid();
1585
- const lockedAt = now();
1586
- const expiresAt = new Date(Date.now() + ttlSeconds * 1000).toISOString();
1587
- d.run("INSERT INTO resource_locks (id, resource_type, resource_id, agent_id, lock_type, locked_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?)", [id, resourceType, resourceId, agentId, lockType, lockedAt, expiresAt]);
1588
- return {
1589
- id,
1590
- resource_type: resourceType,
1591
- resource_id: resourceId,
1592
- agent_id: agentId,
1593
- lock_type: lockType,
1594
- locked_at: lockedAt,
1595
- expires_at: expiresAt
1596
- };
1556
+ results.sort((a, b) => b.score - a.score);
1557
+ return results;
1597
1558
  }
1598
- function releaseLock(lockId, agentId, db) {
1599
- const d = db || getDatabase();
1600
- const result = d.run("DELETE FROM resource_locks WHERE id = ? AND agent_id = ?", [lockId, agentId]);
1601
- return result.changes > 0;
1559
+ function getGraphBoostedMemoryIds(query, d) {
1560
+ const boostedIds = new Set;
1561
+ try {
1562
+ const matchingEntities = listEntities({ search: query, limit: 10 }, d);
1563
+ const exactMatch = getEntityByName(query, undefined, undefined, d);
1564
+ if (exactMatch && !matchingEntities.find((e) => e.id === exactMatch.id)) {
1565
+ matchingEntities.push(exactMatch);
1566
+ }
1567
+ for (const entity of matchingEntities) {
1568
+ const memories = getMemoriesForEntity(entity.id, d);
1569
+ for (const mem of memories) {
1570
+ boostedIds.add(mem.id);
1571
+ }
1572
+ }
1573
+ } catch {}
1574
+ return boostedIds;
1602
1575
  }
1603
- function releaseAllAgentLocks(agentId, db) {
1604
- const d = db || getDatabase();
1605
- const result = d.run("DELETE FROM resource_locks WHERE agent_id = ?", [agentId]);
1606
- return result.changes;
1576
+ function computeRecencyBoost(memory) {
1577
+ if (memory.pinned)
1578
+ return 1;
1579
+ const mostRecent = memory.accessed_at || memory.updated_at;
1580
+ if (!mostRecent)
1581
+ return 0;
1582
+ const daysSinceAccess = (Date.now() - Date.parse(mostRecent)) / (1000 * 60 * 60 * 24);
1583
+ return Math.max(0, 1 - daysSinceAccess / 30);
1607
1584
  }
1608
- function checkLock(resourceType, resourceId, lockType, db) {
1609
- const d = db || getDatabase();
1610
- cleanExpiredLocks(d);
1611
- const query = lockType ? "SELECT * FROM resource_locks WHERE resource_type = ? AND resource_id = ? AND lock_type = ? AND expires_at > datetime('now')" : "SELECT * FROM resource_locks WHERE resource_type = ? AND resource_id = ? AND expires_at > datetime('now')";
1612
- const rows = lockType ? d.query(query).all(resourceType, resourceId, lockType) : d.query(query).all(resourceType, resourceId);
1613
- return rows.map(parseLockRow);
1585
+ function scoreResults(rows, queryLower, graphBoostedIds) {
1586
+ const scored = [];
1587
+ for (const row of rows) {
1588
+ const memory = parseMemoryRow2(row);
1589
+ const rawScore = computeScore(memory, queryLower);
1590
+ if (rawScore === 0)
1591
+ continue;
1592
+ const weightedScore = rawScore * memory.importance / 10;
1593
+ const recencyBoost = computeRecencyBoost(memory);
1594
+ const accessBoost = Math.min(memory.access_count / 20, 0.2);
1595
+ const graphBoost = graphBoostedIds?.has(memory.id) ? 2 : 0;
1596
+ const finalScore = (weightedScore + graphBoost) * (1 + recencyBoost * 0.3) * (1 + accessBoost);
1597
+ const matchType = determineMatchType(memory, queryLower);
1598
+ scored.push({
1599
+ memory,
1600
+ score: finalScore,
1601
+ match_type: matchType,
1602
+ highlights: extractHighlights(memory, queryLower)
1603
+ });
1604
+ }
1605
+ scored.sort((a, b) => {
1606
+ if (b.score !== a.score)
1607
+ return b.score - a.score;
1608
+ return b.memory.importance - a.memory.importance;
1609
+ });
1610
+ return scored;
1614
1611
  }
1615
- function listAgentLocks(agentId, db) {
1612
+ function searchMemories(query, filter, db) {
1616
1613
  const d = db || getDatabase();
1617
- cleanExpiredLocks(d);
1618
- const rows = d.query("SELECT * FROM resource_locks WHERE agent_id = ? AND expires_at > datetime('now') ORDER BY locked_at DESC").all(agentId);
1619
- return rows.map(parseLockRow);
1614
+ query = preprocessQuery(query);
1615
+ if (!query)
1616
+ return [];
1617
+ const queryLower = query.toLowerCase();
1618
+ const graphBoostedIds = getGraphBoostedMemoryIds(query, d);
1619
+ let scored;
1620
+ if (hasFts5Table(d)) {
1621
+ const ftsResult = searchWithFts5(d, query, queryLower, filter, graphBoostedIds);
1622
+ if (ftsResult !== null) {
1623
+ scored = ftsResult;
1624
+ } else {
1625
+ scored = searchWithLike(d, query, queryLower, filter, graphBoostedIds);
1626
+ }
1627
+ } else {
1628
+ scored = searchWithLike(d, query, queryLower, filter, graphBoostedIds);
1629
+ }
1630
+ if (scored.length < 3) {
1631
+ const fuzzyResults = searchWithFuzzy(d, query, filter, graphBoostedIds);
1632
+ const seenIds = new Set(scored.map((r) => r.memory.id));
1633
+ for (const fr of fuzzyResults) {
1634
+ if (!seenIds.has(fr.memory.id)) {
1635
+ scored.push(fr);
1636
+ seenIds.add(fr.memory.id);
1637
+ }
1638
+ }
1639
+ scored.sort((a, b) => {
1640
+ if (b.score !== a.score)
1641
+ return b.score - a.score;
1642
+ return b.memory.importance - a.memory.importance;
1643
+ });
1644
+ }
1645
+ const offset = filter?.offset ?? 0;
1646
+ const limit = filter?.limit ?? scored.length;
1647
+ const finalResults = scored.slice(offset, offset + limit);
1648
+ if (finalResults.length > 0 && scored.length > 0) {
1649
+ const topScore = scored[0]?.score ?? 0;
1650
+ const secondScore = scored[1]?.score ?? 0;
1651
+ const confidence = topScore > 0 ? Math.max(0, Math.min(1, (topScore - secondScore) / topScore)) : 0;
1652
+ finalResults[0] = { ...finalResults[0], confidence };
1653
+ }
1654
+ logSearchQuery(query, scored.length, filter?.agent_id, filter?.project_id, d);
1655
+ return finalResults;
1620
1656
  }
1621
- function cleanExpiredLocks(db) {
1622
- const d = db || getDatabase();
1623
- const result = d.run("DELETE FROM resource_locks WHERE expires_at <= datetime('now')");
1624
- return result.changes;
1657
+ function logSearchQuery(query, resultCount, agentId, projectId, db) {
1658
+ try {
1659
+ const d = db || getDatabase();
1660
+ const id = crypto.randomUUID().slice(0, 8);
1661
+ d.run("INSERT INTO search_history (id, query, result_count, agent_id, project_id) VALUES (?, ?, ?, ?, ?)", [id, query, resultCount, agentId || null, projectId || null]);
1662
+ } catch {}
1625
1663
  }
1664
+ var STOP_WORDS;
1665
+ var init_search = __esm(() => {
1666
+ init_database();
1667
+ init_entities();
1668
+ init_entity_memories();
1669
+ STOP_WORDS = new Set([
1670
+ "a",
1671
+ "an",
1672
+ "the",
1673
+ "is",
1674
+ "are",
1675
+ "was",
1676
+ "were",
1677
+ "be",
1678
+ "been",
1679
+ "being",
1680
+ "have",
1681
+ "has",
1682
+ "had",
1683
+ "do",
1684
+ "does",
1685
+ "did",
1686
+ "will",
1687
+ "would",
1688
+ "could",
1689
+ "should",
1690
+ "may",
1691
+ "might",
1692
+ "shall",
1693
+ "can",
1694
+ "need",
1695
+ "dare",
1696
+ "ought",
1697
+ "used",
1698
+ "to",
1699
+ "of",
1700
+ "in",
1701
+ "for",
1702
+ "on",
1703
+ "with",
1704
+ "at",
1705
+ "by",
1706
+ "from",
1707
+ "as",
1708
+ "into",
1709
+ "through",
1710
+ "during",
1711
+ "before",
1712
+ "after",
1713
+ "above",
1714
+ "below",
1715
+ "between",
1716
+ "out",
1717
+ "off",
1718
+ "over",
1719
+ "under",
1720
+ "again",
1721
+ "further",
1722
+ "then",
1723
+ "once",
1724
+ "here",
1725
+ "there",
1726
+ "when",
1727
+ "where",
1728
+ "why",
1729
+ "how",
1730
+ "all",
1731
+ "each",
1732
+ "every",
1733
+ "both",
1734
+ "few",
1735
+ "more",
1736
+ "most",
1737
+ "other",
1738
+ "some",
1739
+ "such",
1740
+ "no",
1741
+ "not",
1742
+ "only",
1743
+ "own",
1744
+ "same",
1745
+ "so",
1746
+ "than",
1747
+ "too",
1748
+ "very",
1749
+ "just",
1750
+ "because",
1751
+ "but",
1752
+ "and",
1753
+ "or",
1754
+ "if",
1755
+ "while",
1756
+ "that",
1757
+ "this",
1758
+ "it"
1759
+ ]);
1760
+ });
1626
1761
 
1627
- // src/lib/search.ts
1628
- function parseMemoryRow2(row) {
1762
+ // src/db/relations.ts
1763
+ function parseRelationRow(row) {
1629
1764
  return {
1630
1765
  id: row["id"],
1631
- key: row["key"],
1632
- value: row["value"],
1633
- category: row["category"],
1634
- scope: row["scope"],
1635
- summary: row["summary"] || null,
1636
- tags: JSON.parse(row["tags"] || "[]"),
1637
- importance: row["importance"],
1638
- source: row["source"],
1639
- status: row["status"],
1640
- pinned: !!row["pinned"],
1641
- agent_id: row["agent_id"] || null,
1642
- project_id: row["project_id"] || null,
1643
- session_id: row["session_id"] || null,
1766
+ source_entity_id: row["source_entity_id"],
1767
+ target_entity_id: row["target_entity_id"],
1768
+ relation_type: row["relation_type"],
1769
+ weight: row["weight"],
1644
1770
  metadata: JSON.parse(row["metadata"] || "{}"),
1645
- access_count: row["access_count"],
1646
- version: row["version"],
1647
- expires_at: row["expires_at"] || null,
1648
- created_at: row["created_at"],
1649
- updated_at: row["updated_at"],
1650
- accessed_at: row["accessed_at"] || null
1771
+ created_at: row["created_at"]
1651
1772
  };
1652
1773
  }
1653
- function preprocessQuery(query) {
1654
- let q = query.trim();
1655
- q = q.replace(/\s+/g, " ");
1656
- q = q.normalize("NFC");
1657
- return q;
1774
+ function parseEntityRow2(row) {
1775
+ return {
1776
+ id: row["id"],
1777
+ name: row["name"],
1778
+ type: row["type"],
1779
+ description: row["description"] || null,
1780
+ metadata: JSON.parse(row["metadata"] || "{}"),
1781
+ project_id: row["project_id"] || null,
1782
+ created_at: row["created_at"],
1783
+ updated_at: row["updated_at"]
1784
+ };
1658
1785
  }
1659
- function escapeLikePattern(s) {
1660
- return s.replace(/%/g, "\\%").replace(/_/g, "\\_");
1786
+ function createRelation(input, db) {
1787
+ const d = db || getDatabase();
1788
+ const id = shortUuid();
1789
+ const timestamp = now();
1790
+ const weight = input.weight ?? 1;
1791
+ const metadata = JSON.stringify(input.metadata ?? {});
1792
+ d.run(`INSERT INTO relations (id, source_entity_id, target_entity_id, relation_type, weight, metadata, created_at)
1793
+ VALUES (?, ?, ?, ?, ?, ?, ?)
1794
+ ON CONFLICT(source_entity_id, target_entity_id, relation_type)
1795
+ DO UPDATE SET weight = excluded.weight, metadata = excluded.metadata`, [id, input.source_entity_id, input.target_entity_id, input.relation_type, weight, metadata, timestamp]);
1796
+ const row = d.query(`SELECT * FROM relations
1797
+ WHERE source_entity_id = ? AND target_entity_id = ? AND relation_type = ?`).get(input.source_entity_id, input.target_entity_id, input.relation_type);
1798
+ const relation = parseRelationRow(row);
1799
+ hookRegistry.runHooks("PostRelationCreate", {
1800
+ relationId: relation.id,
1801
+ sourceEntityId: relation.source_entity_id,
1802
+ targetEntityId: relation.target_entity_id,
1803
+ relationType: relation.relation_type,
1804
+ timestamp: Date.now()
1805
+ });
1806
+ return relation;
1661
1807
  }
1662
- var STOP_WORDS = new Set([
1663
- "a",
1664
- "an",
1665
- "the",
1666
- "is",
1667
- "are",
1668
- "was",
1669
- "were",
1670
- "be",
1671
- "been",
1672
- "being",
1673
- "have",
1674
- "has",
1675
- "had",
1676
- "do",
1677
- "does",
1678
- "did",
1679
- "will",
1680
- "would",
1681
- "could",
1682
- "should",
1683
- "may",
1684
- "might",
1685
- "shall",
1686
- "can",
1687
- "need",
1688
- "dare",
1689
- "ought",
1690
- "used",
1691
- "to",
1692
- "of",
1693
- "in",
1694
- "for",
1695
- "on",
1696
- "with",
1697
- "at",
1698
- "by",
1699
- "from",
1700
- "as",
1701
- "into",
1702
- "through",
1703
- "during",
1704
- "before",
1705
- "after",
1706
- "above",
1707
- "below",
1708
- "between",
1709
- "out",
1710
- "off",
1711
- "over",
1712
- "under",
1713
- "again",
1714
- "further",
1715
- "then",
1716
- "once",
1717
- "here",
1718
- "there",
1719
- "when",
1720
- "where",
1721
- "why",
1722
- "how",
1723
- "all",
1724
- "each",
1725
- "every",
1726
- "both",
1727
- "few",
1728
- "more",
1729
- "most",
1730
- "other",
1731
- "some",
1732
- "such",
1733
- "no",
1734
- "not",
1735
- "only",
1736
- "own",
1737
- "same",
1738
- "so",
1739
- "than",
1740
- "too",
1741
- "very",
1742
- "just",
1743
- "because",
1744
- "but",
1745
- "and",
1746
- "or",
1747
- "if",
1748
- "while",
1749
- "that",
1750
- "this",
1751
- "it"
1752
- ]);
1753
- function removeStopWords(tokens) {
1754
- if (tokens.length <= 1)
1755
- return tokens;
1756
- const filtered = tokens.filter((t) => !STOP_WORDS.has(t.toLowerCase()));
1757
- return filtered.length > 0 ? filtered : tokens;
1808
+ function getRelation(id, db) {
1809
+ const d = db || getDatabase();
1810
+ const row = d.query("SELECT * FROM relations WHERE id = ?").get(id);
1811
+ if (!row)
1812
+ throw new Error(`Relation not found: ${id}`);
1813
+ return parseRelationRow(row);
1758
1814
  }
1759
- function extractHighlights(memory, queryLower) {
1760
- const highlights = [];
1761
- const tokens = queryLower.split(/\s+/).filter(Boolean);
1762
- for (const field of ["key", "value", "summary"]) {
1763
- const text = field === "summary" ? memory.summary : memory[field];
1764
- if (!text)
1765
- continue;
1766
- const textLower = text.toLowerCase();
1767
- const searchTerms = [queryLower, ...tokens].filter(Boolean);
1768
- for (const term of searchTerms) {
1769
- const idx = textLower.indexOf(term);
1770
- if (idx !== -1) {
1771
- const start = Math.max(0, idx - 30);
1772
- const end = Math.min(text.length, idx + term.length + 30);
1773
- const prefix = start > 0 ? "..." : "";
1774
- const suffix = end < text.length ? "..." : "";
1775
- highlights.push({
1776
- field,
1777
- snippet: prefix + text.slice(start, end) + suffix
1778
- });
1779
- break;
1780
- }
1815
+ function listRelations(filter, db) {
1816
+ const d = db || getDatabase();
1817
+ const conditions = [];
1818
+ const params = [];
1819
+ if (filter.entity_id) {
1820
+ const dir = filter.direction || "both";
1821
+ if (dir === "outgoing") {
1822
+ conditions.push("source_entity_id = ?");
1823
+ params.push(filter.entity_id);
1824
+ } else if (dir === "incoming") {
1825
+ conditions.push("target_entity_id = ?");
1826
+ params.push(filter.entity_id);
1827
+ } else {
1828
+ conditions.push("(source_entity_id = ? OR target_entity_id = ?)");
1829
+ params.push(filter.entity_id, filter.entity_id);
1781
1830
  }
1782
1831
  }
1783
- for (const tag of memory.tags) {
1784
- if (tag.toLowerCase().includes(queryLower) || tokens.some((t) => tag.toLowerCase().includes(t))) {
1785
- highlights.push({ field: "tag", snippet: tag });
1786
- }
1832
+ if (filter.relation_type) {
1833
+ conditions.push("relation_type = ?");
1834
+ params.push(filter.relation_type);
1787
1835
  }
1788
- return highlights;
1836
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1837
+ const rows = d.query(`SELECT * FROM relations ${where} ORDER BY created_at DESC`).all(...params);
1838
+ return rows.map(parseRelationRow);
1789
1839
  }
1790
- function determineMatchType(memory, queryLower) {
1791
- if (memory.key.toLowerCase() === queryLower)
1792
- return "exact";
1793
- if (memory.tags.some((t) => t.toLowerCase() === queryLower))
1794
- return "tag";
1795
- if (memory.tags.some((t) => t.toLowerCase().includes(queryLower)))
1796
- return "tag";
1797
- return "fuzzy";
1840
+ function deleteRelation(id, db) {
1841
+ const d = db || getDatabase();
1842
+ const result = d.run("DELETE FROM relations WHERE id = ?", [id]);
1843
+ if (result.changes === 0)
1844
+ throw new Error(`Relation not found: ${id}`);
1798
1845
  }
1799
- function computeScore(memory, queryLower) {
1800
- const fieldScores = [];
1801
- const keyLower = memory.key.toLowerCase();
1802
- if (keyLower === queryLower) {
1803
- fieldScores.push(10);
1804
- } else if (keyLower.includes(queryLower)) {
1805
- fieldScores.push(7);
1806
- }
1807
- if (memory.tags.some((t) => t.toLowerCase() === queryLower)) {
1808
- fieldScores.push(6);
1809
- } else if (memory.tags.some((t) => t.toLowerCase().includes(queryLower))) {
1810
- fieldScores.push(3);
1846
+ function getEntityGraph(entityId, depth = 2, db) {
1847
+ const d = db || getDatabase();
1848
+ const entityRows = d.query(`WITH RECURSIVE graph(id, depth) AS (
1849
+ VALUES(?, 0)
1850
+ UNION
1851
+ SELECT CASE WHEN r.source_entity_id = g.id THEN r.target_entity_id ELSE r.source_entity_id END, g.depth + 1
1852
+ FROM relations r JOIN graph g ON (r.source_entity_id = g.id OR r.target_entity_id = g.id)
1853
+ WHERE g.depth < ?
1854
+ )
1855
+ SELECT DISTINCT e.* FROM entities e JOIN graph g ON e.id = g.id`).all(entityId, depth);
1856
+ const entities = entityRows.map(parseEntityRow2);
1857
+ const entityIds = new Set(entities.map((e) => e.id));
1858
+ if (entityIds.size === 0) {
1859
+ return { entities: [], relations: [] };
1811
1860
  }
1812
- if (memory.summary && memory.summary.toLowerCase().includes(queryLower)) {
1813
- fieldScores.push(4);
1861
+ const placeholders = Array.from(entityIds).map(() => "?").join(",");
1862
+ const relationRows = d.query(`SELECT * FROM relations
1863
+ WHERE source_entity_id IN (${placeholders})
1864
+ AND target_entity_id IN (${placeholders})`).all(...Array.from(entityIds), ...Array.from(entityIds));
1865
+ const relations = relationRows.map(parseRelationRow);
1866
+ return { entities, relations };
1867
+ }
1868
+ function findPath(fromEntityId, toEntityId, maxDepth = 5, db) {
1869
+ const d = db || getDatabase();
1870
+ const rows = d.query(`WITH RECURSIVE path(id, trail, depth) AS (
1871
+ SELECT ?, ?, 0
1872
+ UNION
1873
+ SELECT
1874
+ CASE WHEN r.source_entity_id = p.id THEN r.target_entity_id ELSE r.source_entity_id END,
1875
+ p.trail || ',' || CASE WHEN r.source_entity_id = p.id THEN r.target_entity_id ELSE r.source_entity_id END,
1876
+ p.depth + 1
1877
+ FROM relations r JOIN path p ON (r.source_entity_id = p.id OR r.target_entity_id = p.id)
1878
+ WHERE p.depth < ?
1879
+ AND INSTR(p.trail, CASE WHEN r.source_entity_id = p.id THEN r.target_entity_id ELSE r.source_entity_id END) = 0
1880
+ )
1881
+ SELECT trail FROM path WHERE id = ? ORDER BY depth ASC LIMIT 1`).get(fromEntityId, fromEntityId, maxDepth, toEntityId);
1882
+ if (!rows)
1883
+ return null;
1884
+ const ids = rows.trail.split(",");
1885
+ const entities = [];
1886
+ for (const id of ids) {
1887
+ const row = d.query("SELECT * FROM entities WHERE id = ?").get(id);
1888
+ if (row)
1889
+ entities.push(parseEntityRow2(row));
1814
1890
  }
1815
- if (memory.value.toLowerCase().includes(queryLower)) {
1816
- fieldScores.push(3);
1891
+ return entities.length > 0 ? entities : null;
1892
+ }
1893
+ var init_relations = __esm(() => {
1894
+ init_database();
1895
+ init_hooks();
1896
+ });
1897
+
1898
+ // src/lib/providers/base.ts
1899
+ class BaseProvider {
1900
+ config;
1901
+ constructor(config) {
1902
+ this.config = config;
1817
1903
  }
1818
- const metadataStr = JSON.stringify(memory.metadata).toLowerCase();
1819
- if (metadataStr !== "{}" && metadataStr.includes(queryLower)) {
1820
- fieldScores.push(2);
1904
+ parseJSON(raw) {
1905
+ try {
1906
+ const cleaned = raw.replace(/^```(?:json)?\s*/m, "").replace(/\s*```$/m, "").trim();
1907
+ return JSON.parse(cleaned);
1908
+ } catch {
1909
+ return null;
1910
+ }
1821
1911
  }
1822
- fieldScores.sort((a, b) => b - a);
1823
- const diminishingMultipliers = [1, 0.5, 0.25, 0.15, 0.15];
1824
- let score = 0;
1825
- for (let i = 0;i < fieldScores.length; i++) {
1826
- score += fieldScores[i] * (diminishingMultipliers[i] ?? 0.15);
1912
+ clampImportance(value) {
1913
+ const n = Number(value);
1914
+ if (isNaN(n))
1915
+ return 5;
1916
+ return Math.max(0, Math.min(10, Math.round(n)));
1827
1917
  }
1828
- const { phrases } = extractQuotedPhrases(queryLower);
1829
- for (const phrase of phrases) {
1830
- if (keyLower.includes(phrase))
1831
- score += 8;
1832
- if (memory.value.toLowerCase().includes(phrase))
1833
- score += 5;
1834
- if (memory.summary && memory.summary.toLowerCase().includes(phrase))
1835
- score += 4;
1918
+ normaliseMemory(raw) {
1919
+ if (!raw || typeof raw !== "object")
1920
+ return null;
1921
+ const m = raw;
1922
+ if (typeof m.content !== "string" || !m.content.trim())
1923
+ return null;
1924
+ const validScopes = ["private", "shared", "global"];
1925
+ const validCategories = [
1926
+ "preference",
1927
+ "fact",
1928
+ "knowledge",
1929
+ "history"
1930
+ ];
1931
+ return {
1932
+ content: m.content.trim(),
1933
+ category: validCategories.includes(m.category) ? m.category : "knowledge",
1934
+ importance: this.clampImportance(m.importance),
1935
+ tags: Array.isArray(m.tags) ? m.tags.filter((t) => typeof t === "string").map((t) => t.toLowerCase()) : [],
1936
+ suggestedScope: validScopes.includes(m.suggestedScope) ? m.suggestedScope : "shared",
1937
+ reasoning: typeof m.reasoning === "string" ? m.reasoning : undefined
1938
+ };
1836
1939
  }
1837
- const { remainder } = extractQuotedPhrases(queryLower);
1838
- const tokens = removeStopWords(remainder.split(/\s+/).filter(Boolean));
1839
- if (tokens.length > 1) {
1840
- let tokenScore = 0;
1841
- for (const token of tokens) {
1842
- if (keyLower === token) {
1843
- tokenScore += 10 / tokens.length;
1844
- } else if (keyLower.includes(token)) {
1845
- tokenScore += 7 / tokens.length;
1940
+ }
1941
+ var DEFAULT_AUTO_MEMORY_CONFIG, MEMORY_EXTRACTION_SYSTEM_PROMPT = `You are a precise memory extraction engine for an AI agent.
1942
+ Given text, extract facts worth remembering as structured JSON.
1943
+ Focus on: decisions made, preferences revealed, corrections, architectural choices, established facts, user preferences.
1944
+ Ignore: greetings, filler, questions without answers, temporary states.
1945
+ Output ONLY a JSON array \u2014 no markdown, no explanation.`, MEMORY_EXTRACTION_USER_TEMPLATE = (text, context) => `Extract memories from this text.
1946
+ ${context.projectName ? `Project: ${context.projectName}` : ""}
1947
+ ${context.existingMemoriesSummary ? `Existing memories (avoid duplicates):
1948
+ ${context.existingMemoriesSummary}` : ""}
1949
+
1950
+ Text:
1951
+ ${text}
1952
+
1953
+ Return a JSON array of objects with these exact fields:
1954
+ - content: string (the memory, concise and specific)
1955
+ - category: "preference" | "fact" | "knowledge" | "history"
1956
+ - importance: number 0-10 (10 = critical, 0 = trivial)
1957
+ - tags: string[] (lowercase keywords)
1958
+ - suggestedScope: "private" | "shared" | "global"
1959
+ - reasoning: string (one sentence why this is worth remembering)
1960
+
1961
+ Return [] if nothing is worth remembering.`, ENTITY_EXTRACTION_SYSTEM_PROMPT = `You are a knowledge graph entity extractor.
1962
+ Given text, identify named entities and their relationships.
1963
+ Output ONLY valid JSON \u2014 no markdown, no explanation.`, ENTITY_EXTRACTION_USER_TEMPLATE = (text) => `Extract entities and relations from this text.
1964
+
1965
+ Text: ${text}
1966
+
1967
+ Return JSON with this exact shape:
1968
+ {
1969
+ "entities": [
1970
+ { "name": string, "type": "person"|"project"|"tool"|"concept"|"file"|"api"|"pattern"|"organization", "confidence": 0-1 }
1971
+ ],
1972
+ "relations": [
1973
+ { "from": string, "to": string, "type": "uses"|"knows"|"depends_on"|"created_by"|"related_to"|"contradicts"|"part_of"|"implements" }
1974
+ ]
1975
+ }`;
1976
+ var init_base = __esm(() => {
1977
+ DEFAULT_AUTO_MEMORY_CONFIG = {
1978
+ provider: "anthropic",
1979
+ model: "claude-haiku-4-5",
1980
+ enabled: true,
1981
+ minImportance: 4,
1982
+ autoEntityLink: true,
1983
+ fallback: ["cerebras", "openai"]
1984
+ };
1985
+ });
1986
+
1987
+ // src/lib/providers/anthropic.ts
1988
+ var ANTHROPIC_MODELS, AnthropicProvider;
1989
+ var init_anthropic = __esm(() => {
1990
+ init_base();
1991
+ ANTHROPIC_MODELS = {
1992
+ default: "claude-haiku-4-5",
1993
+ premium: "claude-sonnet-4-5"
1994
+ };
1995
+ AnthropicProvider = class AnthropicProvider extends BaseProvider {
1996
+ name = "anthropic";
1997
+ baseUrl = "https://api.anthropic.com/v1";
1998
+ constructor(config) {
1999
+ const apiKey = config?.apiKey ?? process.env.ANTHROPIC_API_KEY ?? "";
2000
+ super({
2001
+ apiKey,
2002
+ model: config?.model ?? ANTHROPIC_MODELS.default,
2003
+ maxTokens: config?.maxTokens ?? 1024,
2004
+ temperature: config?.temperature ?? 0,
2005
+ timeoutMs: config?.timeoutMs ?? 15000
2006
+ });
2007
+ }
2008
+ async extractMemories(text, context) {
2009
+ if (!this.config.apiKey)
2010
+ return [];
2011
+ try {
2012
+ const response = await this.callAPI(MEMORY_EXTRACTION_SYSTEM_PROMPT, MEMORY_EXTRACTION_USER_TEMPLATE(text, context));
2013
+ const parsed = this.parseJSON(response);
2014
+ if (!Array.isArray(parsed))
2015
+ return [];
2016
+ return parsed.map((item) => this.normaliseMemory(item)).filter((m) => m !== null);
2017
+ } catch (err) {
2018
+ console.error("[anthropic] extractMemories failed:", err);
2019
+ return [];
1846
2020
  }
1847
- if (memory.tags.some((t) => t.toLowerCase() === token)) {
1848
- tokenScore += 6 / tokens.length;
1849
- } else if (memory.tags.some((t) => t.toLowerCase().includes(token))) {
1850
- tokenScore += 3 / tokens.length;
2021
+ }
2022
+ async extractEntities(text) {
2023
+ const empty = { entities: [], relations: [] };
2024
+ if (!this.config.apiKey)
2025
+ return empty;
2026
+ try {
2027
+ const response = await this.callAPI(ENTITY_EXTRACTION_SYSTEM_PROMPT, ENTITY_EXTRACTION_USER_TEMPLATE(text));
2028
+ const parsed = this.parseJSON(response);
2029
+ if (!parsed || typeof parsed !== "object")
2030
+ return empty;
2031
+ return {
2032
+ entities: Array.isArray(parsed.entities) ? parsed.entities : [],
2033
+ relations: Array.isArray(parsed.relations) ? parsed.relations : []
2034
+ };
2035
+ } catch (err) {
2036
+ console.error("[anthropic] extractEntities failed:", err);
2037
+ return empty;
1851
2038
  }
1852
- if (memory.summary && memory.summary.toLowerCase().includes(token)) {
1853
- tokenScore += 4 / tokens.length;
2039
+ }
2040
+ async scoreImportance(content, _context) {
2041
+ if (!this.config.apiKey)
2042
+ return 5;
2043
+ try {
2044
+ const response = await this.callAPI("You are an importance scorer. Return only a single integer 0-10. No explanation.", `How important is this memory for an AI agent to retain long-term?
2045
+
2046
+ "${content}"
2047
+
2048
+ Return only a number 0-10.`);
2049
+ return this.clampImportance(response.trim());
2050
+ } catch {
2051
+ return 5;
1854
2052
  }
1855
- if (memory.value.toLowerCase().includes(token)) {
1856
- tokenScore += 3 / tokens.length;
2053
+ }
2054
+ async callAPI(systemPrompt, userMessage) {
2055
+ const controller = new AbortController;
2056
+ const timeout = setTimeout(() => controller.abort(), this.config.timeoutMs ?? 15000);
2057
+ try {
2058
+ const res = await fetch(`${this.baseUrl}/messages`, {
2059
+ method: "POST",
2060
+ headers: {
2061
+ "Content-Type": "application/json",
2062
+ "x-api-key": this.config.apiKey,
2063
+ "anthropic-version": "2023-06-01"
2064
+ },
2065
+ body: JSON.stringify({
2066
+ model: this.config.model,
2067
+ max_tokens: this.config.maxTokens ?? 1024,
2068
+ temperature: this.config.temperature ?? 0,
2069
+ system: systemPrompt,
2070
+ messages: [{ role: "user", content: userMessage }]
2071
+ }),
2072
+ signal: controller.signal
2073
+ });
2074
+ if (!res.ok) {
2075
+ const body = await res.text().catch(() => "");
2076
+ throw new Error(`Anthropic API ${res.status}: ${body.slice(0, 200)}`);
2077
+ }
2078
+ const data = await res.json();
2079
+ return data.content?.[0]?.text ?? "";
2080
+ } finally {
2081
+ clearTimeout(timeout);
1857
2082
  }
1858
- if (metadataStr !== "{}" && metadataStr.includes(token)) {
1859
- tokenScore += 2 / tokens.length;
2083
+ }
2084
+ };
2085
+ });
2086
+
2087
+ // src/lib/providers/openai-compat.ts
2088
+ var OpenAICompatProvider;
2089
+ var init_openai_compat = __esm(() => {
2090
+ init_base();
2091
+ OpenAICompatProvider = class OpenAICompatProvider extends BaseProvider {
2092
+ constructor(config) {
2093
+ super(config);
2094
+ }
2095
+ async extractMemories(text, context) {
2096
+ if (!this.config.apiKey)
2097
+ return [];
2098
+ try {
2099
+ const response = await this.callWithRetry(MEMORY_EXTRACTION_SYSTEM_PROMPT, MEMORY_EXTRACTION_USER_TEMPLATE(text, context));
2100
+ const parsed = this.parseJSON(response);
2101
+ if (!Array.isArray(parsed))
2102
+ return [];
2103
+ return parsed.map((item) => this.normaliseMemory(item)).filter((m) => m !== null);
2104
+ } catch (err) {
2105
+ console.error(`[${this.name}] extractMemories failed:`, err);
2106
+ return [];
1860
2107
  }
1861
2108
  }
1862
- if (score > 0) {
1863
- score += tokenScore * 0.3;
1864
- } else {
1865
- score += tokenScore;
2109
+ async extractEntities(text) {
2110
+ const empty = { entities: [], relations: [] };
2111
+ if (!this.config.apiKey)
2112
+ return empty;
2113
+ try {
2114
+ const response = await this.callWithRetry(ENTITY_EXTRACTION_SYSTEM_PROMPT, ENTITY_EXTRACTION_USER_TEMPLATE(text));
2115
+ const parsed = this.parseJSON(response);
2116
+ if (!parsed || typeof parsed !== "object")
2117
+ return empty;
2118
+ return {
2119
+ entities: Array.isArray(parsed.entities) ? parsed.entities : [],
2120
+ relations: Array.isArray(parsed.relations) ? parsed.relations : []
2121
+ };
2122
+ } catch (err) {
2123
+ console.error(`[${this.name}] extractEntities failed:`, err);
2124
+ return empty;
2125
+ }
2126
+ }
2127
+ async scoreImportance(content, _context) {
2128
+ if (!this.config.apiKey)
2129
+ return 5;
2130
+ try {
2131
+ const response = await this.callWithRetry("You are an importance scorer. Return only a single integer 0-10. No explanation.", `How important is this memory for an AI agent to retain long-term?
2132
+
2133
+ "${content}"
2134
+
2135
+ Return only a number 0-10.`);
2136
+ return this.clampImportance(response.trim());
2137
+ } catch {
2138
+ return 5;
2139
+ }
2140
+ }
2141
+ async callWithRetry(systemPrompt, userMessage, retries = 3) {
2142
+ let lastError = null;
2143
+ for (let attempt = 0;attempt < retries; attempt++) {
2144
+ try {
2145
+ return await this.callAPI(systemPrompt, userMessage);
2146
+ } catch (err) {
2147
+ lastError = err instanceof Error ? err : new Error(String(err));
2148
+ const isRateLimit = lastError.message.includes("429") || lastError.message.toLowerCase().includes("rate limit");
2149
+ if (!isRateLimit || attempt === retries - 1)
2150
+ throw lastError;
2151
+ await new Promise((r) => setTimeout(r, 1000 * Math.pow(2, attempt)));
2152
+ }
2153
+ }
2154
+ throw lastError ?? new Error("Unknown error");
2155
+ }
2156
+ async callAPI(systemPrompt, userMessage) {
2157
+ const controller = new AbortController;
2158
+ const timeout = setTimeout(() => controller.abort(), this.config.timeoutMs ?? 15000);
2159
+ try {
2160
+ const res = await fetch(`${this.baseUrl}/chat/completions`, {
2161
+ method: "POST",
2162
+ headers: {
2163
+ "Content-Type": "application/json",
2164
+ [this.authHeader]: `Bearer ${this.config.apiKey}`
2165
+ },
2166
+ body: JSON.stringify({
2167
+ model: this.config.model,
2168
+ max_tokens: this.config.maxTokens ?? 1024,
2169
+ temperature: this.config.temperature ?? 0,
2170
+ messages: [
2171
+ { role: "system", content: systemPrompt },
2172
+ { role: "user", content: userMessage }
2173
+ ]
2174
+ }),
2175
+ signal: controller.signal
2176
+ });
2177
+ if (!res.ok) {
2178
+ const body = await res.text().catch(() => "");
2179
+ throw new Error(`${this.name} API ${res.status}: ${body.slice(0, 200)}`);
2180
+ }
2181
+ const data = await res.json();
2182
+ return data.choices?.[0]?.message?.content ?? "";
2183
+ } finally {
2184
+ clearTimeout(timeout);
2185
+ }
2186
+ }
2187
+ };
2188
+ });
2189
+
2190
+ // src/lib/providers/openai.ts
2191
+ var OPENAI_MODELS, OpenAIProvider;
2192
+ var init_openai = __esm(() => {
2193
+ init_openai_compat();
2194
+ OPENAI_MODELS = {
2195
+ default: "gpt-4.1-nano",
2196
+ mini: "gpt-4.1-mini",
2197
+ full: "gpt-4.1"
2198
+ };
2199
+ OpenAIProvider = class OpenAIProvider extends OpenAICompatProvider {
2200
+ name = "openai";
2201
+ baseUrl = "https://api.openai.com/v1";
2202
+ authHeader = "Authorization";
2203
+ constructor(config) {
2204
+ super({
2205
+ apiKey: config?.apiKey ?? process.env.OPENAI_API_KEY ?? "",
2206
+ model: config?.model ?? OPENAI_MODELS.default,
2207
+ maxTokens: config?.maxTokens ?? 1024,
2208
+ temperature: config?.temperature ?? 0,
2209
+ timeoutMs: config?.timeoutMs ?? 15000
2210
+ });
2211
+ }
2212
+ };
2213
+ });
2214
+
2215
+ // src/lib/providers/cerebras.ts
2216
+ var CEREBRAS_MODELS, CerebrasProvider;
2217
+ var init_cerebras = __esm(() => {
2218
+ init_openai_compat();
2219
+ CEREBRAS_MODELS = {
2220
+ default: "llama-3.3-70b",
2221
+ fast: "llama3.1-8b"
2222
+ };
2223
+ CerebrasProvider = class CerebrasProvider extends OpenAICompatProvider {
2224
+ name = "cerebras";
2225
+ baseUrl = "https://api.cerebras.ai/v1";
2226
+ authHeader = "Authorization";
2227
+ constructor(config) {
2228
+ super({
2229
+ apiKey: config?.apiKey ?? process.env.CEREBRAS_API_KEY ?? "",
2230
+ model: config?.model ?? CEREBRAS_MODELS.default,
2231
+ maxTokens: config?.maxTokens ?? 1024,
2232
+ temperature: config?.temperature ?? 0,
2233
+ timeoutMs: config?.timeoutMs ?? 1e4
2234
+ });
2235
+ }
2236
+ };
2237
+ });
2238
+
2239
+ // src/lib/providers/grok.ts
2240
+ var GROK_MODELS, GrokProvider;
2241
+ var init_grok = __esm(() => {
2242
+ init_openai_compat();
2243
+ GROK_MODELS = {
2244
+ default: "grok-3-mini",
2245
+ premium: "grok-3"
2246
+ };
2247
+ GrokProvider = class GrokProvider extends OpenAICompatProvider {
2248
+ name = "grok";
2249
+ baseUrl = "https://api.x.ai/v1";
2250
+ authHeader = "Authorization";
2251
+ constructor(config) {
2252
+ super({
2253
+ apiKey: config?.apiKey ?? process.env.XAI_API_KEY ?? "",
2254
+ model: config?.model ?? GROK_MODELS.default,
2255
+ maxTokens: config?.maxTokens ?? 1024,
2256
+ temperature: config?.temperature ?? 0,
2257
+ timeoutMs: config?.timeoutMs ?? 15000
2258
+ });
2259
+ }
2260
+ };
2261
+ });
2262
+
2263
+ // src/lib/providers/registry.ts
2264
+ class ProviderRegistry {
2265
+ config = { ...DEFAULT_AUTO_MEMORY_CONFIG };
2266
+ _instances = new Map;
2267
+ configure(partial) {
2268
+ this.config = { ...this.config, ...partial };
2269
+ this._instances.clear();
2270
+ }
2271
+ getConfig() {
2272
+ return this.config;
2273
+ }
2274
+ getPrimary() {
2275
+ return this.getProvider(this.config.provider);
2276
+ }
2277
+ getFallbacks() {
2278
+ const fallbackNames = this.config.fallback ?? [];
2279
+ return fallbackNames.filter((n) => n !== this.config.provider).map((n) => this.getProvider(n)).filter((p) => p !== null);
2280
+ }
2281
+ getAvailable() {
2282
+ const primary = this.getPrimary();
2283
+ if (primary)
2284
+ return primary;
2285
+ const fallbacks = this.getFallbacks();
2286
+ return fallbacks[0] ?? null;
2287
+ }
2288
+ getProvider(name) {
2289
+ const cached = this._instances.get(name);
2290
+ if (cached)
2291
+ return cached;
2292
+ const provider = this.createProvider(name);
2293
+ if (!provider)
2294
+ return null;
2295
+ if (!provider.config.apiKey)
2296
+ return null;
2297
+ this._instances.set(name, provider);
2298
+ return provider;
2299
+ }
2300
+ health() {
2301
+ const providers = ["anthropic", "openai", "cerebras", "grok"];
2302
+ const result = {};
2303
+ for (const name of providers) {
2304
+ const p = this.createProvider(name);
2305
+ result[name] = {
2306
+ available: Boolean(p?.config.apiKey),
2307
+ model: p?.config.model ?? "unknown"
2308
+ };
2309
+ }
2310
+ return result;
2311
+ }
2312
+ createProvider(name) {
2313
+ const modelOverride = name === this.config.provider ? this.config.model : undefined;
2314
+ switch (name) {
2315
+ case "anthropic":
2316
+ return new AnthropicProvider(modelOverride ? { model: modelOverride } : undefined);
2317
+ case "openai":
2318
+ return new OpenAIProvider(modelOverride ? { model: modelOverride } : undefined);
2319
+ case "cerebras":
2320
+ return new CerebrasProvider(modelOverride ? { model: modelOverride } : undefined);
2321
+ case "grok":
2322
+ return new GrokProvider(modelOverride ? { model: modelOverride } : undefined);
2323
+ default:
2324
+ return null;
1866
2325
  }
1867
2326
  }
1868
- return score;
1869
2327
  }
1870
- function extractQuotedPhrases(query) {
1871
- const phrases = [];
1872
- const remainder = query.replace(/"([^"]+)"/g, (_match, phrase) => {
1873
- phrases.push(phrase);
1874
- return "";
2328
+ function autoConfigureFromEnv() {
2329
+ const hasAnthropicKey = Boolean(process.env.ANTHROPIC_API_KEY);
2330
+ const hasCerebrasKey = Boolean(process.env.CEREBRAS_API_KEY);
2331
+ const hasOpenAIKey = Boolean(process.env.OPENAI_API_KEY);
2332
+ const hasGrokKey = Boolean(process.env.XAI_API_KEY);
2333
+ if (!hasAnthropicKey) {
2334
+ if (hasCerebrasKey) {
2335
+ providerRegistry.configure({ provider: "cerebras" });
2336
+ } else if (hasOpenAIKey) {
2337
+ providerRegistry.configure({ provider: "openai" });
2338
+ } else if (hasGrokKey) {
2339
+ providerRegistry.configure({ provider: "grok" });
2340
+ }
2341
+ }
2342
+ const allProviders = ["anthropic", "cerebras", "openai", "grok"];
2343
+ const available = allProviders.filter((p) => {
2344
+ switch (p) {
2345
+ case "anthropic":
2346
+ return hasAnthropicKey;
2347
+ case "cerebras":
2348
+ return hasCerebrasKey;
2349
+ case "openai":
2350
+ return hasOpenAIKey;
2351
+ case "grok":
2352
+ return hasGrokKey;
2353
+ }
1875
2354
  });
1876
- return { phrases, remainder: remainder.trim() };
2355
+ const primary = providerRegistry.getConfig().provider;
2356
+ const fallback = available.filter((p) => p !== primary);
2357
+ providerRegistry.configure({ fallback });
1877
2358
  }
1878
- function escapeFts5Query(query) {
1879
- const { phrases, remainder } = extractQuotedPhrases(query);
1880
- const parts = [];
1881
- for (const phrase of phrases) {
1882
- parts.push(`"${phrase.replace(/"/g, '""')}"`);
2359
+ var providerRegistry;
2360
+ var init_registry = __esm(() => {
2361
+ init_anthropic();
2362
+ init_openai();
2363
+ init_cerebras();
2364
+ init_grok();
2365
+ init_base();
2366
+ providerRegistry = new ProviderRegistry;
2367
+ autoConfigureFromEnv();
2368
+ });
2369
+
2370
+ // src/lib/auto-memory-queue.ts
2371
+ class AutoMemoryQueue {
2372
+ queue = [];
2373
+ handler = null;
2374
+ running = false;
2375
+ activeCount = 0;
2376
+ stats = {
2377
+ pending: 0,
2378
+ processing: 0,
2379
+ processed: 0,
2380
+ failed: 0,
2381
+ dropped: 0
2382
+ };
2383
+ setHandler(handler) {
2384
+ this.handler = handler;
2385
+ if (!this.running)
2386
+ this.startLoop();
2387
+ }
2388
+ enqueue(job) {
2389
+ if (this.queue.length >= MAX_QUEUE_SIZE) {
2390
+ this.queue.shift();
2391
+ this.stats.dropped++;
2392
+ this.stats.pending = Math.max(0, this.stats.pending - 1);
2393
+ }
2394
+ this.queue.push(job);
2395
+ this.stats.pending++;
2396
+ if (!this.running && this.handler)
2397
+ this.startLoop();
2398
+ }
2399
+ getStats() {
2400
+ return { ...this.stats, pending: this.queue.length };
2401
+ }
2402
+ startLoop() {
2403
+ this.running = true;
2404
+ this.loop();
2405
+ }
2406
+ async loop() {
2407
+ while (this.queue.length > 0 || this.activeCount > 0) {
2408
+ while (this.queue.length > 0 && this.activeCount < CONCURRENCY) {
2409
+ const job = this.queue.shift();
2410
+ if (!job)
2411
+ break;
2412
+ this.stats.pending = Math.max(0, this.stats.pending - 1);
2413
+ this.activeCount++;
2414
+ this.stats.processing = this.activeCount;
2415
+ this.processJob(job);
2416
+ }
2417
+ await new Promise((r) => setImmediate(r));
2418
+ }
2419
+ this.running = false;
1883
2420
  }
1884
- const tokens = removeStopWords(remainder.split(/\s+/).filter(Boolean));
1885
- for (const t of tokens) {
1886
- parts.push(`"${t.replace(/"/g, '""')}"`);
2421
+ async processJob(job) {
2422
+ if (!this.handler) {
2423
+ this.activeCount--;
2424
+ this.stats.processing = this.activeCount;
2425
+ return;
2426
+ }
2427
+ try {
2428
+ await this.handler(job);
2429
+ this.stats.processed++;
2430
+ } catch (err) {
2431
+ this.stats.failed++;
2432
+ console.error("[auto-memory-queue] job failed:", err);
2433
+ } finally {
2434
+ this.activeCount--;
2435
+ this.stats.processing = this.activeCount;
2436
+ }
1887
2437
  }
1888
- return parts.join(" ");
1889
2438
  }
1890
- function hasFts5Table(d) {
2439
+ var MAX_QUEUE_SIZE = 100, CONCURRENCY = 3, autoMemoryQueue;
2440
+ var init_auto_memory_queue = __esm(() => {
2441
+ autoMemoryQueue = new AutoMemoryQueue;
2442
+ });
2443
+
2444
+ // src/lib/auto-memory.ts
2445
+ var exports_auto_memory = {};
2446
+ __export(exports_auto_memory, {
2447
+ processConversationTurn: () => processConversationTurn,
2448
+ getAutoMemoryStats: () => getAutoMemoryStats,
2449
+ configureAutoMemory: () => configureAutoMemory
2450
+ });
2451
+ function isDuplicate(content, agentId, projectId) {
1891
2452
  try {
1892
- const row = d.query("SELECT name FROM sqlite_master WHERE type='table' AND name='memories_fts'").get();
1893
- return !!row;
2453
+ const query = content.split(/\s+/).filter((w) => w.length > 3).slice(0, 10).join(" ");
2454
+ if (!query)
2455
+ return false;
2456
+ const results = searchMemories(query, {
2457
+ agent_id: agentId,
2458
+ project_id: projectId,
2459
+ limit: 3
2460
+ });
2461
+ if (results.length === 0)
2462
+ return false;
2463
+ const contentWords = new Set(content.toLowerCase().split(/\W+/).filter((w) => w.length > 3));
2464
+ for (const result of results) {
2465
+ const existingWords = new Set(result.memory.value.toLowerCase().split(/\W+/).filter((w) => w.length > 3));
2466
+ if (contentWords.size === 0 || existingWords.size === 0)
2467
+ continue;
2468
+ const intersection = [...contentWords].filter((w) => existingWords.has(w)).length;
2469
+ const union = new Set([...contentWords, ...existingWords]).size;
2470
+ const similarity = intersection / union;
2471
+ if (similarity >= DEDUP_SIMILARITY_THRESHOLD)
2472
+ return true;
2473
+ }
2474
+ return false;
1894
2475
  } catch {
1895
2476
  return false;
1896
2477
  }
1897
2478
  }
1898
- function buildFilterConditions(filter) {
1899
- const conditions = [];
1900
- const params = [];
1901
- conditions.push("m.status = 'active'");
1902
- conditions.push("(m.expires_at IS NULL OR m.expires_at >= datetime('now'))");
1903
- if (!filter)
1904
- return { conditions, params };
1905
- if (filter.scope) {
1906
- if (Array.isArray(filter.scope)) {
1907
- conditions.push(`m.scope IN (${filter.scope.map(() => "?").join(",")})`);
1908
- params.push(...filter.scope);
1909
- } else {
1910
- conditions.push("m.scope = ?");
1911
- params.push(filter.scope);
1912
- }
1913
- }
1914
- if (filter.category) {
1915
- if (Array.isArray(filter.category)) {
1916
- conditions.push(`m.category IN (${filter.category.map(() => "?").join(",")})`);
1917
- params.push(...filter.category);
1918
- } else {
1919
- conditions.push("m.category = ?");
1920
- params.push(filter.category);
1921
- }
1922
- }
1923
- if (filter.source) {
1924
- if (Array.isArray(filter.source)) {
1925
- conditions.push(`m.source IN (${filter.source.map(() => "?").join(",")})`);
1926
- params.push(...filter.source);
1927
- } else {
1928
- conditions.push("m.source = ?");
1929
- params.push(filter.source);
1930
- }
1931
- }
1932
- if (filter.status) {
1933
- conditions.shift();
1934
- if (Array.isArray(filter.status)) {
1935
- conditions.push(`m.status IN (${filter.status.map(() => "?").join(",")})`);
1936
- params.push(...filter.status);
1937
- } else {
1938
- conditions.push("m.status = ?");
1939
- params.push(filter.status);
2479
+ async function linkEntitiesToMemory(memoryId, content, _agentId, projectId) {
2480
+ const provider = providerRegistry.getAvailable();
2481
+ if (!provider)
2482
+ return;
2483
+ try {
2484
+ const { entities, relations } = await provider.extractEntities(content);
2485
+ const entityIdMap = new Map;
2486
+ for (const extracted of entities) {
2487
+ if (extracted.confidence < 0.6)
2488
+ continue;
2489
+ try {
2490
+ const existing = getEntityByName(extracted.name);
2491
+ const entityId = existing ? existing.id : createEntity({
2492
+ name: extracted.name,
2493
+ type: extracted.type,
2494
+ project_id: projectId
2495
+ }).id;
2496
+ entityIdMap.set(extracted.name, entityId);
2497
+ linkEntityToMemory(entityId, memoryId, "subject");
2498
+ } catch {}
1940
2499
  }
1941
- }
1942
- if (filter.project_id) {
1943
- conditions.push("m.project_id = ?");
1944
- params.push(filter.project_id);
1945
- }
1946
- if (filter.agent_id) {
1947
- conditions.push("m.agent_id = ?");
1948
- params.push(filter.agent_id);
1949
- }
1950
- if (filter.session_id) {
1951
- conditions.push("m.session_id = ?");
1952
- params.push(filter.session_id);
1953
- }
1954
- if (filter.min_importance) {
1955
- conditions.push("m.importance >= ?");
1956
- params.push(filter.min_importance);
1957
- }
1958
- if (filter.pinned !== undefined) {
1959
- conditions.push("m.pinned = ?");
1960
- params.push(filter.pinned ? 1 : 0);
1961
- }
1962
- if (filter.tags && filter.tags.length > 0) {
1963
- for (const tag of filter.tags) {
1964
- conditions.push("m.id IN (SELECT memory_id FROM memory_tags WHERE tag = ?)");
1965
- params.push(tag);
2500
+ for (const rel of relations) {
2501
+ const fromId = entityIdMap.get(rel.from);
2502
+ const toId = entityIdMap.get(rel.to);
2503
+ if (!fromId || !toId)
2504
+ continue;
2505
+ try {
2506
+ createRelation({
2507
+ source_entity_id: fromId,
2508
+ target_entity_id: toId,
2509
+ relation_type: rel.type
2510
+ });
2511
+ } catch {}
1966
2512
  }
2513
+ } catch (err) {
2514
+ console.error("[auto-memory] entity linking failed:", err);
1967
2515
  }
1968
- return { conditions, params };
1969
2516
  }
1970
- function searchWithFts5(d, query, queryLower, filter, graphBoostedIds) {
1971
- const ftsQuery = escapeFts5Query(query);
1972
- if (!ftsQuery)
2517
+ async function saveExtractedMemory(extracted, context) {
2518
+ const minImportance = providerRegistry.getConfig().minImportance;
2519
+ if (extracted.importance < minImportance)
2520
+ return null;
2521
+ if (!extracted.content.trim())
1973
2522
  return null;
2523
+ if (isDuplicate(extracted.content, context.agentId, context.projectId)) {
2524
+ return null;
2525
+ }
1974
2526
  try {
1975
- const { conditions, params } = buildFilterConditions(filter);
1976
- const queryParam = `%${query}%`;
1977
- 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 ?)`;
1978
- const allConditions = [ftsCondition, ...conditions];
1979
- const allParams = [ftsQuery, queryParam, queryParam, ...params];
1980
- const candidateSql = `SELECT m.* FROM memories m WHERE ${allConditions.join(" AND ")}`;
1981
- const rows = d.query(candidateSql).all(...allParams);
1982
- return scoreResults(rows, queryLower, graphBoostedIds);
1983
- } catch {
2527
+ const input = {
2528
+ key: extracted.content.slice(0, 120).replace(/\s+/g, "-").toLowerCase(),
2529
+ value: extracted.content,
2530
+ category: extracted.category,
2531
+ scope: extracted.suggestedScope,
2532
+ importance: extracted.importance,
2533
+ tags: [
2534
+ ...extracted.tags,
2535
+ "auto-extracted",
2536
+ ...context.sessionId ? [`session:${context.sessionId}`] : []
2537
+ ],
2538
+ agent_id: context.agentId,
2539
+ project_id: context.projectId,
2540
+ session_id: context.sessionId,
2541
+ metadata: {
2542
+ reasoning: extracted.reasoning,
2543
+ auto_extracted: true,
2544
+ extracted_at: new Date().toISOString()
2545
+ }
2546
+ };
2547
+ const memory = createMemory(input, "merge");
2548
+ return memory.id;
2549
+ } catch (err) {
2550
+ console.error("[auto-memory] saveExtractedMemory failed:", err);
1984
2551
  return null;
1985
2552
  }
1986
2553
  }
1987
- function searchWithLike(d, query, queryLower, filter, graphBoostedIds) {
1988
- const { conditions, params } = buildFilterConditions(filter);
1989
- const rawTokens = query.trim().split(/\s+/).filter(Boolean);
1990
- const tokens = removeStopWords(rawTokens);
1991
- const escapedQuery = escapeLikePattern(query);
1992
- const likePatterns = [`%${escapedQuery}%`];
1993
- if (tokens.length > 1) {
1994
- for (const t of tokens)
1995
- likePatterns.push(`%${escapeLikePattern(t)}%`);
2554
+ async function processJob(job) {
2555
+ if (!providerRegistry.getConfig().enabled)
2556
+ return;
2557
+ const provider = providerRegistry.getAvailable();
2558
+ if (!provider)
2559
+ return;
2560
+ const context = {
2561
+ agentId: job.agentId,
2562
+ projectId: job.projectId,
2563
+ sessionId: job.sessionId
2564
+ };
2565
+ let extracted = [];
2566
+ try {
2567
+ extracted = await provider.extractMemories(job.turn, context);
2568
+ } catch {
2569
+ const fallbacks = providerRegistry.getFallbacks();
2570
+ for (const fallback of fallbacks) {
2571
+ try {
2572
+ extracted = await fallback.extractMemories(job.turn, context);
2573
+ if (extracted.length > 0)
2574
+ break;
2575
+ } catch {
2576
+ continue;
2577
+ }
2578
+ }
1996
2579
  }
1997
- const fieldClauses = [];
1998
- for (const pattern of likePatterns) {
1999
- fieldClauses.push("m.key LIKE ? ESCAPE '\\'");
2000
- params.push(pattern);
2001
- fieldClauses.push("m.value LIKE ? ESCAPE '\\'");
2002
- params.push(pattern);
2003
- fieldClauses.push("m.summary LIKE ? ESCAPE '\\'");
2004
- params.push(pattern);
2005
- fieldClauses.push("m.metadata LIKE ? ESCAPE '\\'");
2006
- params.push(pattern);
2007
- fieldClauses.push("m.id IN (SELECT memory_id FROM memory_tags WHERE tag LIKE ? ESCAPE '\\')");
2008
- params.push(pattern);
2580
+ if (extracted.length === 0)
2581
+ return;
2582
+ for (const memory of extracted) {
2583
+ const memoryId = await saveExtractedMemory(memory, context);
2584
+ if (!memoryId)
2585
+ continue;
2586
+ if (providerRegistry.getConfig().autoEntityLink) {
2587
+ linkEntitiesToMemory(memoryId, memory.content, job.agentId, job.projectId);
2588
+ }
2009
2589
  }
2010
- conditions.push(`(${fieldClauses.join(" OR ")})`);
2011
- const sql = `SELECT DISTINCT m.* FROM memories m WHERE ${conditions.join(" AND ")}`;
2012
- const rows = d.query(sql).all(...params);
2013
- return scoreResults(rows, queryLower, graphBoostedIds);
2014
2590
  }
2015
- function generateTrigrams(s) {
2016
- const lower = s.toLowerCase();
2017
- const trigrams = new Set;
2018
- for (let i = 0;i <= lower.length - 3; i++) {
2019
- trigrams.add(lower.slice(i, i + 3));
2591
+ function processConversationTurn(turn, context, source = "turn") {
2592
+ if (!turn?.trim())
2593
+ return;
2594
+ autoMemoryQueue.enqueue({
2595
+ ...context,
2596
+ turn,
2597
+ timestamp: Date.now(),
2598
+ source
2599
+ });
2600
+ }
2601
+ function getAutoMemoryStats() {
2602
+ return autoMemoryQueue.getStats();
2603
+ }
2604
+ function configureAutoMemory(config) {
2605
+ providerRegistry.configure(config);
2606
+ }
2607
+ var DEDUP_SIMILARITY_THRESHOLD = 0.85;
2608
+ var init_auto_memory = __esm(() => {
2609
+ init_memories();
2610
+ init_search();
2611
+ init_entities();
2612
+ init_relations();
2613
+ init_entity_memories();
2614
+ init_registry();
2615
+ init_auto_memory_queue();
2616
+ autoMemoryQueue.setHandler(processJob);
2617
+ });
2618
+
2619
+ // src/server/index.ts
2620
+ init_memories();
2621
+ import { existsSync as existsSync3 } from "fs";
2622
+ import { dirname as dirname3, extname, join as join3, resolve as resolve3, sep } from "path";
2623
+ import { fileURLToPath } from "url";
2624
+
2625
+ // src/db/agents.ts
2626
+ init_types();
2627
+ init_database();
2628
+ var CONFLICT_WINDOW_MS = 30 * 60 * 1000;
2629
+ function parseAgentRow(row) {
2630
+ return {
2631
+ id: row["id"],
2632
+ name: row["name"],
2633
+ session_id: row["session_id"] || null,
2634
+ description: row["description"] || null,
2635
+ role: row["role"] || null,
2636
+ metadata: JSON.parse(row["metadata"] || "{}"),
2637
+ active_project_id: row["active_project_id"] || null,
2638
+ created_at: row["created_at"],
2639
+ last_seen_at: row["last_seen_at"]
2640
+ };
2641
+ }
2642
+ function registerAgent(name, sessionId, description, role, projectId, db) {
2643
+ const d = db || getDatabase();
2644
+ const timestamp = now();
2645
+ const normalizedName = name.trim().toLowerCase();
2646
+ const existing = d.query("SELECT * FROM agents WHERE LOWER(name) = ?").get(normalizedName);
2647
+ if (existing) {
2648
+ const existingId = existing["id"];
2649
+ const existingSessionId = existing["session_id"] || null;
2650
+ const existingLastSeen = existing["last_seen_at"];
2651
+ if (sessionId && existingSessionId && existingSessionId !== sessionId) {
2652
+ const lastSeenMs = new Date(existingLastSeen).getTime();
2653
+ const nowMs = Date.now();
2654
+ if (nowMs - lastSeenMs < CONFLICT_WINDOW_MS) {
2655
+ throw new AgentConflictError({
2656
+ existing_id: existingId,
2657
+ existing_name: normalizedName,
2658
+ last_seen_at: existingLastSeen,
2659
+ session_hint: existingSessionId.slice(0, 8),
2660
+ working_dir: null
2661
+ });
2662
+ }
2663
+ }
2664
+ d.run("UPDATE agents SET last_seen_at = ?, session_id = ? WHERE id = ?", [
2665
+ timestamp,
2666
+ sessionId ?? existingSessionId,
2667
+ existingId
2668
+ ]);
2669
+ if (description) {
2670
+ d.run("UPDATE agents SET description = ? WHERE id = ?", [description, existingId]);
2671
+ }
2672
+ if (role) {
2673
+ d.run("UPDATE agents SET role = ? WHERE id = ?", [role, existingId]);
2674
+ }
2675
+ if (projectId !== undefined) {
2676
+ d.run("UPDATE agents SET active_project_id = ? WHERE id = ?", [projectId, existingId]);
2677
+ }
2678
+ return getAgent(existingId, d);
2020
2679
  }
2021
- return trigrams;
2680
+ const id = shortUuid();
2681
+ d.run("INSERT INTO agents (id, name, session_id, description, role, active_project_id, created_at, last_seen_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [id, normalizedName, sessionId ?? null, description || null, role || "agent", projectId ?? null, timestamp, timestamp]);
2682
+ return getAgent(id, d);
2022
2683
  }
2023
- function trigramSimilarity(a, b) {
2024
- const triA = generateTrigrams(a);
2025
- const triB = generateTrigrams(b);
2026
- if (triA.size === 0 || triB.size === 0)
2027
- return 0;
2028
- let intersection = 0;
2029
- for (const t of triA) {
2030
- if (triB.has(t))
2031
- intersection++;
2684
+ function getAgent(idOrName, db) {
2685
+ const d = db || getDatabase();
2686
+ let row = d.query("SELECT * FROM agents WHERE id = ?").get(idOrName);
2687
+ if (row)
2688
+ return parseAgentRow(row);
2689
+ row = d.query("SELECT * FROM agents WHERE LOWER(name) = ?").get(idOrName.trim().toLowerCase());
2690
+ if (row)
2691
+ return parseAgentRow(row);
2692
+ const rows = d.query("SELECT * FROM agents WHERE id LIKE ?").all(`${idOrName}%`);
2693
+ if (rows.length === 1)
2694
+ return parseAgentRow(rows[0]);
2695
+ return null;
2696
+ }
2697
+ function listAgents(db) {
2698
+ const d = db || getDatabase();
2699
+ const rows = d.query("SELECT * FROM agents ORDER BY last_seen_at DESC").all();
2700
+ return rows.map(parseAgentRow);
2701
+ }
2702
+ function listAgentsByProject(projectId, db) {
2703
+ const d = db || getDatabase();
2704
+ const rows = d.query("SELECT * FROM agents WHERE active_project_id = ? ORDER BY last_seen_at DESC").all(projectId);
2705
+ return rows.map(parseAgentRow);
2706
+ }
2707
+ function updateAgent(id, updates, db) {
2708
+ const d = db || getDatabase();
2709
+ const agent = getAgent(id, d);
2710
+ if (!agent)
2711
+ return null;
2712
+ const timestamp = now();
2713
+ if (updates.name) {
2714
+ const normalizedNewName = updates.name.trim().toLowerCase();
2715
+ if (normalizedNewName !== agent.name) {
2716
+ const existing = d.query("SELECT id FROM agents WHERE LOWER(name) = ? AND id != ?").get(normalizedNewName, agent.id);
2717
+ if (existing) {
2718
+ throw new Error(`Agent name already taken: ${normalizedNewName}`);
2719
+ }
2720
+ d.run("UPDATE agents SET name = ? WHERE id = ?", [normalizedNewName, agent.id]);
2721
+ }
2032
2722
  }
2033
- const union = triA.size + triB.size - intersection;
2034
- return union === 0 ? 0 : intersection / union;
2723
+ if (updates.description !== undefined) {
2724
+ d.run("UPDATE agents SET description = ? WHERE id = ?", [updates.description, agent.id]);
2725
+ }
2726
+ if (updates.role !== undefined) {
2727
+ d.run("UPDATE agents SET role = ? WHERE id = ?", [updates.role, agent.id]);
2728
+ }
2729
+ if (updates.metadata !== undefined) {
2730
+ d.run("UPDATE agents SET metadata = ? WHERE id = ?", [JSON.stringify(updates.metadata), agent.id]);
2731
+ }
2732
+ if ("active_project_id" in updates) {
2733
+ d.run("UPDATE agents SET active_project_id = ? WHERE id = ?", [updates.active_project_id ?? null, agent.id]);
2734
+ }
2735
+ d.run("UPDATE agents SET last_seen_at = ? WHERE id = ?", [timestamp, agent.id]);
2736
+ return getAgent(agent.id, d);
2035
2737
  }
2036
- function searchWithFuzzy(d, query, filter, graphBoostedIds) {
2037
- const { conditions, params } = buildFilterConditions(filter);
2038
- const sql = `SELECT m.* FROM memories m WHERE ${conditions.join(" AND ")}`;
2039
- const rows = d.query(sql).all(...params);
2040
- const MIN_SIMILARITY = 0.3;
2041
- const results = [];
2042
- for (const row of rows) {
2043
- const memory = parseMemoryRow2(row);
2044
- let bestSimilarity = 0;
2045
- bestSimilarity = Math.max(bestSimilarity, trigramSimilarity(query, memory.key));
2046
- bestSimilarity = Math.max(bestSimilarity, trigramSimilarity(query, memory.value.slice(0, 200)));
2047
- if (memory.summary) {
2048
- bestSimilarity = Math.max(bestSimilarity, trigramSimilarity(query, memory.summary));
2738
+
2739
+ // src/db/locks.ts
2740
+ init_database();
2741
+ function parseLockRow(row) {
2742
+ return {
2743
+ id: row["id"],
2744
+ resource_type: row["resource_type"],
2745
+ resource_id: row["resource_id"],
2746
+ agent_id: row["agent_id"],
2747
+ lock_type: row["lock_type"],
2748
+ locked_at: row["locked_at"],
2749
+ expires_at: row["expires_at"]
2750
+ };
2751
+ }
2752
+ function acquireLock(agentId, resourceType, resourceId, lockType = "exclusive", ttlSeconds = 300, db) {
2753
+ const d = db || getDatabase();
2754
+ cleanExpiredLocks(d);
2755
+ const ownLock = d.query("SELECT * FROM resource_locks WHERE resource_type = ? AND resource_id = ? AND agent_id = ? AND lock_type = ? AND expires_at > datetime('now')").get(resourceType, resourceId, agentId, lockType);
2756
+ if (ownLock) {
2757
+ const newExpiry = new Date(Date.now() + ttlSeconds * 1000).toISOString();
2758
+ d.run("UPDATE resource_locks SET expires_at = ? WHERE id = ?", [
2759
+ newExpiry,
2760
+ ownLock["id"]
2761
+ ]);
2762
+ return parseLockRow({ ...ownLock, expires_at: newExpiry });
2763
+ }
2764
+ if (lockType === "exclusive") {
2765
+ const existing = d.query("SELECT * FROM resource_locks WHERE resource_type = ? AND resource_id = ? AND lock_type = 'exclusive' AND agent_id != ? AND expires_at > datetime('now')").get(resourceType, resourceId, agentId);
2766
+ if (existing) {
2767
+ return null;
2049
2768
  }
2050
- for (const tag of memory.tags) {
2051
- bestSimilarity = Math.max(bestSimilarity, trigramSimilarity(query, tag));
2769
+ }
2770
+ const id = shortUuid();
2771
+ const lockedAt = now();
2772
+ const expiresAt = new Date(Date.now() + ttlSeconds * 1000).toISOString();
2773
+ d.run("INSERT INTO resource_locks (id, resource_type, resource_id, agent_id, lock_type, locked_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?)", [id, resourceType, resourceId, agentId, lockType, lockedAt, expiresAt]);
2774
+ return {
2775
+ id,
2776
+ resource_type: resourceType,
2777
+ resource_id: resourceId,
2778
+ agent_id: agentId,
2779
+ lock_type: lockType,
2780
+ locked_at: lockedAt,
2781
+ expires_at: expiresAt
2782
+ };
2783
+ }
2784
+ function releaseLock(lockId, agentId, db) {
2785
+ const d = db || getDatabase();
2786
+ const result = d.run("DELETE FROM resource_locks WHERE id = ? AND agent_id = ?", [lockId, agentId]);
2787
+ return result.changes > 0;
2788
+ }
2789
+ function releaseAllAgentLocks(agentId, db) {
2790
+ const d = db || getDatabase();
2791
+ const result = d.run("DELETE FROM resource_locks WHERE agent_id = ?", [agentId]);
2792
+ return result.changes;
2793
+ }
2794
+ function checkLock(resourceType, resourceId, lockType, db) {
2795
+ const d = db || getDatabase();
2796
+ cleanExpiredLocks(d);
2797
+ const query = lockType ? "SELECT * FROM resource_locks WHERE resource_type = ? AND resource_id = ? AND lock_type = ? AND expires_at > datetime('now')" : "SELECT * FROM resource_locks WHERE resource_type = ? AND resource_id = ? AND expires_at > datetime('now')";
2798
+ const rows = lockType ? d.query(query).all(resourceType, resourceId, lockType) : d.query(query).all(resourceType, resourceId);
2799
+ return rows.map(parseLockRow);
2800
+ }
2801
+ function listAgentLocks(agentId, db) {
2802
+ const d = db || getDatabase();
2803
+ cleanExpiredLocks(d);
2804
+ const rows = d.query("SELECT * FROM resource_locks WHERE agent_id = ? AND expires_at > datetime('now') ORDER BY locked_at DESC").all(agentId);
2805
+ return rows.map(parseLockRow);
2806
+ }
2807
+ function cleanExpiredLocks(db) {
2808
+ const d = db || getDatabase();
2809
+ const result = d.run("DELETE FROM resource_locks WHERE expires_at <= datetime('now')");
2810
+ return result.changes;
2811
+ }
2812
+
2813
+ // src/db/projects.ts
2814
+ init_database();
2815
+ function parseProjectRow(row) {
2816
+ return {
2817
+ id: row["id"],
2818
+ name: row["name"],
2819
+ path: row["path"],
2820
+ description: row["description"] || null,
2821
+ memory_prefix: row["memory_prefix"] || null,
2822
+ created_at: row["created_at"],
2823
+ updated_at: row["updated_at"]
2824
+ };
2825
+ }
2826
+ function registerProject(name, path, description, memoryPrefix, db) {
2827
+ const d = db || getDatabase();
2828
+ const timestamp = now();
2829
+ const existing = d.query("SELECT * FROM projects WHERE path = ?").get(path);
2830
+ if (existing) {
2831
+ const existingId = existing["id"];
2832
+ d.run("UPDATE projects SET updated_at = ? WHERE id = ?", [
2833
+ timestamp,
2834
+ existingId
2835
+ ]);
2836
+ return parseProjectRow(existing);
2837
+ }
2838
+ const id = uuid();
2839
+ 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]);
2840
+ return getProject(id, d);
2841
+ }
2842
+ function getProject(idOrPath, db) {
2843
+ const d = db || getDatabase();
2844
+ let row = d.query("SELECT * FROM projects WHERE id = ?").get(idOrPath);
2845
+ if (row)
2846
+ return parseProjectRow(row);
2847
+ row = d.query("SELECT * FROM projects WHERE path = ?").get(idOrPath);
2848
+ if (row)
2849
+ return parseProjectRow(row);
2850
+ row = d.query("SELECT * FROM projects WHERE LOWER(name) = ?").get(idOrPath.toLowerCase());
2851
+ if (row)
2852
+ return parseProjectRow(row);
2853
+ return null;
2854
+ }
2855
+ function listProjects(db) {
2856
+ const d = db || getDatabase();
2857
+ const rows = d.query("SELECT * FROM projects ORDER BY updated_at DESC").all();
2858
+ return rows.map(parseProjectRow);
2859
+ }
2860
+
2861
+ // src/lib/config.ts
2862
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, readdirSync, writeFileSync, unlinkSync } from "fs";
2863
+ import { homedir } from "os";
2864
+ import { basename, dirname as dirname2, join as join2, resolve as resolve2 } from "path";
2865
+ function findFileWalkingUp(filename) {
2866
+ let dir = process.cwd();
2867
+ while (true) {
2868
+ const candidate = join2(dir, filename);
2869
+ if (existsSync2(candidate)) {
2870
+ return candidate;
2052
2871
  }
2053
- if (bestSimilarity >= MIN_SIMILARITY) {
2054
- const graphBoost = graphBoostedIds?.has(memory.id) ? 2 : 0;
2055
- const score = bestSimilarity * 5 * memory.importance / 10 + graphBoost;
2056
- results.push({ memory, score, match_type: "fuzzy" });
2872
+ const parent = dirname2(dir);
2873
+ if (parent === dir) {
2874
+ return null;
2057
2875
  }
2876
+ dir = parent;
2058
2877
  }
2059
- results.sort((a, b) => b.score - a.score);
2060
- return results;
2061
2878
  }
2062
- function getGraphBoostedMemoryIds(query, d) {
2063
- const boostedIds = new Set;
2064
- try {
2065
- const matchingEntities = listEntities({ search: query, limit: 10 }, d);
2066
- const exactMatch = getEntityByName(query, undefined, undefined, d);
2067
- if (exactMatch && !matchingEntities.find((e) => e.id === exactMatch.id)) {
2068
- matchingEntities.push(exactMatch);
2879
+ function findGitRoot2() {
2880
+ let dir = process.cwd();
2881
+ while (true) {
2882
+ if (existsSync2(join2(dir, ".git"))) {
2883
+ return dir;
2069
2884
  }
2070
- for (const entity of matchingEntities) {
2071
- const memories = getMemoriesForEntity(entity.id, d);
2072
- for (const mem of memories) {
2073
- boostedIds.add(mem.id);
2074
- }
2885
+ const parent = dirname2(dir);
2886
+ if (parent === dir) {
2887
+ return null;
2075
2888
  }
2076
- } catch {}
2077
- return boostedIds;
2889
+ dir = parent;
2890
+ }
2078
2891
  }
2079
- function computeRecencyBoost(memory) {
2080
- if (memory.pinned)
2081
- return 1;
2082
- const mostRecent = memory.accessed_at || memory.updated_at;
2083
- if (!mostRecent)
2084
- return 0;
2085
- const daysSinceAccess = (Date.now() - Date.parse(mostRecent)) / (1000 * 60 * 60 * 24);
2086
- return Math.max(0, 1 - daysSinceAccess / 30);
2892
+ function profilesDir() {
2893
+ return join2(homedir(), ".mementos", "profiles");
2087
2894
  }
2088
- function scoreResults(rows, queryLower, graphBoostedIds) {
2089
- const scored = [];
2090
- for (const row of rows) {
2091
- const memory = parseMemoryRow2(row);
2092
- const rawScore = computeScore(memory, queryLower);
2093
- if (rawScore === 0)
2094
- continue;
2095
- const weightedScore = rawScore * memory.importance / 10;
2096
- const recencyBoost = computeRecencyBoost(memory);
2097
- const accessBoost = Math.min(memory.access_count / 20, 0.2);
2098
- const graphBoost = graphBoostedIds?.has(memory.id) ? 2 : 0;
2099
- const finalScore = (weightedScore + graphBoost) * (1 + recencyBoost * 0.3) * (1 + accessBoost);
2100
- const matchType = determineMatchType(memory, queryLower);
2101
- scored.push({
2102
- memory,
2103
- score: finalScore,
2104
- match_type: matchType,
2105
- highlights: extractHighlights(memory, queryLower)
2106
- });
2895
+ function globalConfigPath() {
2896
+ return join2(homedir(), ".mementos", "config.json");
2897
+ }
2898
+ function readGlobalConfig() {
2899
+ const p = globalConfigPath();
2900
+ if (!existsSync2(p))
2901
+ return {};
2902
+ try {
2903
+ return JSON.parse(readFileSync(p, "utf-8"));
2904
+ } catch {
2905
+ return {};
2107
2906
  }
2108
- scored.sort((a, b) => {
2109
- if (b.score !== a.score)
2110
- return b.score - a.score;
2111
- return b.memory.importance - a.memory.importance;
2112
- });
2113
- return scored;
2114
2907
  }
2115
- function searchMemories(query, filter, db) {
2116
- const d = db || getDatabase();
2117
- query = preprocessQuery(query);
2118
- if (!query)
2908
+ function getActiveProfile() {
2909
+ const envProfile = process.env["MEMENTOS_PROFILE"];
2910
+ if (envProfile)
2911
+ return envProfile.trim();
2912
+ const cfg = readGlobalConfig();
2913
+ return cfg["active_profile"] || null;
2914
+ }
2915
+ function listProfiles() {
2916
+ const dir = profilesDir();
2917
+ if (!existsSync2(dir))
2119
2918
  return [];
2120
- const queryLower = query.toLowerCase();
2121
- const graphBoostedIds = getGraphBoostedMemoryIds(query, d);
2122
- let scored;
2123
- if (hasFts5Table(d)) {
2124
- const ftsResult = searchWithFts5(d, query, queryLower, filter, graphBoostedIds);
2125
- if (ftsResult !== null) {
2126
- scored = ftsResult;
2127
- } else {
2128
- scored = searchWithLike(d, query, queryLower, filter, graphBoostedIds);
2129
- }
2130
- } else {
2131
- scored = searchWithLike(d, query, queryLower, filter, graphBoostedIds);
2919
+ return readdirSync(dir).filter((f) => f.endsWith(".db")).map((f) => basename(f, ".db")).sort();
2920
+ }
2921
+ function getDbPath2() {
2922
+ const envDbPath = process.env["MEMENTOS_DB_PATH"];
2923
+ if (envDbPath) {
2924
+ const resolved = resolve2(envDbPath);
2925
+ ensureDir2(dirname2(resolved));
2926
+ return resolved;
2132
2927
  }
2133
- if (scored.length < 3) {
2134
- const fuzzyResults = searchWithFuzzy(d, query, filter, graphBoostedIds);
2135
- const seenIds = new Set(scored.map((r) => r.memory.id));
2136
- for (const fr of fuzzyResults) {
2137
- if (!seenIds.has(fr.memory.id)) {
2138
- scored.push(fr);
2139
- seenIds.add(fr.memory.id);
2140
- }
2928
+ const profile = getActiveProfile();
2929
+ if (profile) {
2930
+ const profilePath = join2(profilesDir(), `${profile}.db`);
2931
+ ensureDir2(dirname2(profilePath));
2932
+ return profilePath;
2933
+ }
2934
+ const dbScope = process.env["MEMENTOS_DB_SCOPE"];
2935
+ if (dbScope === "project") {
2936
+ const gitRoot = findGitRoot2();
2937
+ if (gitRoot) {
2938
+ const dbPath = join2(gitRoot, ".mementos", "mementos.db");
2939
+ ensureDir2(dirname2(dbPath));
2940
+ return dbPath;
2141
2941
  }
2142
- scored.sort((a, b) => {
2143
- if (b.score !== a.score)
2144
- return b.score - a.score;
2145
- return b.memory.importance - a.memory.importance;
2146
- });
2147
2942
  }
2148
- const offset = filter?.offset ?? 0;
2149
- const limit = filter?.limit ?? scored.length;
2150
- const finalResults = scored.slice(offset, offset + limit);
2151
- logSearchQuery(query, scored.length, filter?.agent_id, filter?.project_id, d);
2152
- return finalResults;
2943
+ const found = findFileWalkingUp(join2(".mementos", "mementos.db"));
2944
+ if (found) {
2945
+ return found;
2946
+ }
2947
+ const fallback = join2(homedir(), ".mementos", "mementos.db");
2948
+ ensureDir2(dirname2(fallback));
2949
+ return fallback;
2153
2950
  }
2154
- function logSearchQuery(query, resultCount, agentId, projectId, db) {
2155
- try {
2156
- const d = db || getDatabase();
2157
- const id = crypto.randomUUID().slice(0, 8);
2158
- d.run("INSERT INTO search_history (id, query, result_count, agent_id, project_id) VALUES (?, ?, ?, ?, ?)", [id, query, resultCount, agentId || null, projectId || null]);
2159
- } catch {}
2951
+ function ensureDir2(dir) {
2952
+ if (!existsSync2(dir)) {
2953
+ mkdirSync2(dir, { recursive: true });
2954
+ }
2160
2955
  }
2161
2956
 
2957
+ // src/server/index.ts
2958
+ init_database();
2959
+ init_search();
2960
+ init_types();
2961
+ init_entities();
2962
+ init_relations();
2963
+ init_entity_memories();
2964
+
2162
2965
  // src/lib/duration.ts
2163
2966
  var UNIT_MS = {
2164
2967
  s: 1000,
@@ -2202,6 +3005,205 @@ var FORMAT_UNITS = [
2202
3005
  ["s", UNIT_MS["s"]]
2203
3006
  ];
2204
3007
 
3008
+ // src/server/index.ts
3009
+ init_auto_memory();
3010
+ init_registry();
3011
+ init_hooks();
3012
+
3013
+ // src/lib/built-in-hooks.ts
3014
+ init_hooks();
3015
+
3016
+ // src/db/webhook_hooks.ts
3017
+ init_database();
3018
+ function parseRow(row) {
3019
+ return {
3020
+ id: row["id"],
3021
+ type: row["type"],
3022
+ handlerUrl: row["handler_url"],
3023
+ priority: row["priority"],
3024
+ blocking: Boolean(row["blocking"]),
3025
+ agentId: row["agent_id"] || undefined,
3026
+ projectId: row["project_id"] || undefined,
3027
+ description: row["description"] || undefined,
3028
+ enabled: Boolean(row["enabled"]),
3029
+ createdAt: row["created_at"],
3030
+ invocationCount: row["invocation_count"],
3031
+ failureCount: row["failure_count"]
3032
+ };
3033
+ }
3034
+ function createWebhookHook(input, db) {
3035
+ const d = db || getDatabase();
3036
+ const id = shortUuid();
3037
+ const timestamp = now();
3038
+ d.run(`INSERT INTO webhook_hooks
3039
+ (id, type, handler_url, priority, blocking, agent_id, project_id, description, enabled, created_at, invocation_count, failure_count)
3040
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1, ?, 0, 0)`, [
3041
+ id,
3042
+ input.type,
3043
+ input.handlerUrl,
3044
+ input.priority ?? 50,
3045
+ input.blocking ? 1 : 0,
3046
+ input.agentId ?? null,
3047
+ input.projectId ?? null,
3048
+ input.description ?? null,
3049
+ timestamp
3050
+ ]);
3051
+ return getWebhookHook(id, d);
3052
+ }
3053
+ function getWebhookHook(id, db) {
3054
+ const d = db || getDatabase();
3055
+ const row = d.query("SELECT * FROM webhook_hooks WHERE id = ?").get(id);
3056
+ return row ? parseRow(row) : null;
3057
+ }
3058
+ function listWebhookHooks(filter = {}, db) {
3059
+ const d = db || getDatabase();
3060
+ const conditions = [];
3061
+ const params = [];
3062
+ if (filter.type) {
3063
+ conditions.push("type = ?");
3064
+ params.push(filter.type);
3065
+ }
3066
+ if (filter.enabled !== undefined) {
3067
+ conditions.push("enabled = ?");
3068
+ params.push(filter.enabled ? 1 : 0);
3069
+ }
3070
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
3071
+ const rows = d.query(`SELECT * FROM webhook_hooks ${where} ORDER BY priority ASC, created_at ASC`).all(...params);
3072
+ return rows.map(parseRow);
3073
+ }
3074
+ function updateWebhookHook(id, updates, db) {
3075
+ const d = db || getDatabase();
3076
+ const existing = getWebhookHook(id, d);
3077
+ if (!existing)
3078
+ return null;
3079
+ const sets = [];
3080
+ const params = [];
3081
+ if (updates.enabled !== undefined) {
3082
+ sets.push("enabled = ?");
3083
+ params.push(updates.enabled ? 1 : 0);
3084
+ }
3085
+ if (updates.description !== undefined) {
3086
+ sets.push("description = ?");
3087
+ params.push(updates.description);
3088
+ }
3089
+ if (updates.priority !== undefined) {
3090
+ sets.push("priority = ?");
3091
+ params.push(updates.priority);
3092
+ }
3093
+ if (sets.length > 0) {
3094
+ params.push(id);
3095
+ d.run(`UPDATE webhook_hooks SET ${sets.join(", ")} WHERE id = ?`, params);
3096
+ }
3097
+ return getWebhookHook(id, d);
3098
+ }
3099
+ function deleteWebhookHook(id, db) {
3100
+ const d = db || getDatabase();
3101
+ const result = d.run("DELETE FROM webhook_hooks WHERE id = ?", [id]);
3102
+ return result.changes > 0;
3103
+ }
3104
+ function recordWebhookInvocation(id, success, db) {
3105
+ const d = db || getDatabase();
3106
+ if (success) {
3107
+ d.run("UPDATE webhook_hooks SET invocation_count = invocation_count + 1 WHERE id = ?", [id]);
3108
+ } else {
3109
+ d.run("UPDATE webhook_hooks SET invocation_count = invocation_count + 1, failure_count = failure_count + 1 WHERE id = ?", [id]);
3110
+ }
3111
+ }
3112
+
3113
+ // src/lib/built-in-hooks.ts
3114
+ var _processConversationTurn = null;
3115
+ async function getAutoMemory() {
3116
+ if (!_processConversationTurn) {
3117
+ const mod = await Promise.resolve().then(() => (init_auto_memory(), exports_auto_memory));
3118
+ _processConversationTurn = mod.processConversationTurn;
3119
+ }
3120
+ return _processConversationTurn;
3121
+ }
3122
+ hookRegistry.register({
3123
+ type: "PostMemorySave",
3124
+ blocking: false,
3125
+ builtin: true,
3126
+ priority: 100,
3127
+ description: "Trigger async LLM entity extraction when a memory is saved",
3128
+ handler: async (ctx) => {
3129
+ if (ctx.wasUpdated)
3130
+ return;
3131
+ const processConversationTurn2 = await getAutoMemory();
3132
+ processConversationTurn2(`${ctx.memory.key}: ${ctx.memory.value}`, {
3133
+ agentId: ctx.agentId,
3134
+ projectId: ctx.projectId,
3135
+ sessionId: ctx.sessionId
3136
+ });
3137
+ }
3138
+ });
3139
+ hookRegistry.register({
3140
+ type: "OnSessionStart",
3141
+ blocking: false,
3142
+ builtin: true,
3143
+ priority: 100,
3144
+ description: "Record session start as a history memory for analytics",
3145
+ handler: async (ctx) => {
3146
+ const { createMemory: createMemory2 } = await Promise.resolve().then(() => (init_memories(), exports_memories));
3147
+ try {
3148
+ createMemory2({
3149
+ key: `session-start-${ctx.agentId}`,
3150
+ value: `Agent ${ctx.agentId} started session on project ${ctx.projectId} at ${new Date(ctx.timestamp).toISOString()}`,
3151
+ category: "history",
3152
+ scope: "shared",
3153
+ importance: 3,
3154
+ source: "system",
3155
+ agent_id: ctx.agentId,
3156
+ project_id: ctx.projectId,
3157
+ session_id: ctx.sessionId
3158
+ });
3159
+ } catch {}
3160
+ }
3161
+ });
3162
+ var _webhooksLoaded = false;
3163
+ function loadWebhooksFromDb() {
3164
+ if (_webhooksLoaded)
3165
+ return;
3166
+ _webhooksLoaded = true;
3167
+ try {
3168
+ const webhooks = listWebhookHooks({ enabled: true });
3169
+ for (const wh of webhooks) {
3170
+ hookRegistry.register({
3171
+ type: wh.type,
3172
+ blocking: wh.blocking,
3173
+ priority: wh.priority,
3174
+ agentId: wh.agentId,
3175
+ projectId: wh.projectId,
3176
+ description: wh.description ?? `Webhook: ${wh.handlerUrl}`,
3177
+ handler: makeWebhookHandler(wh.id, wh.handlerUrl)
3178
+ });
3179
+ }
3180
+ if (webhooks.length > 0) {
3181
+ console.log(`[hooks] Loaded ${webhooks.length} webhook(s) from DB`);
3182
+ }
3183
+ } catch (err) {
3184
+ console.error("[hooks] Failed to load webhooks from DB:", err);
3185
+ }
3186
+ }
3187
+ function makeWebhookHandler(webhookId, url) {
3188
+ return async (context) => {
3189
+ try {
3190
+ const res = await fetch(url, {
3191
+ method: "POST",
3192
+ headers: { "Content-Type": "application/json" },
3193
+ body: JSON.stringify(context),
3194
+ signal: AbortSignal.timeout(1e4)
3195
+ });
3196
+ recordWebhookInvocation(webhookId, res.ok);
3197
+ } catch {
3198
+ recordWebhookInvocation(webhookId, false);
3199
+ }
3200
+ };
3201
+ }
3202
+ function reloadWebhooks() {
3203
+ _webhooksLoaded = false;
3204
+ loadWebhooksFromDb();
3205
+ }
3206
+
2205
3207
  // src/server/index.ts
2206
3208
  var DEFAULT_PORT = 19428;
2207
3209
  function parsePort() {
@@ -3161,7 +4163,125 @@ async function findFreePort(start) {
3161
4163
  }
3162
4164
  return start;
3163
4165
  }
4166
+ addRoute("POST", "/api/auto-memory/process", async (req) => {
4167
+ const body = await readJson(req);
4168
+ const turn = body?.turn;
4169
+ if (!turn)
4170
+ return errorResponse("turn is required", 400);
4171
+ processConversationTurn(turn, { agentId: body?.agent_id, projectId: body?.project_id, sessionId: body?.session_id });
4172
+ const stats = getAutoMemoryStats();
4173
+ return json({ queued: true, queue: stats }, 202);
4174
+ });
4175
+ addRoute("GET", "/api/auto-memory/status", () => {
4176
+ return json({
4177
+ queue: getAutoMemoryStats(),
4178
+ config: providerRegistry.getConfig(),
4179
+ providers: providerRegistry.health()
4180
+ });
4181
+ });
4182
+ addRoute("GET", "/api/auto-memory/config", () => {
4183
+ return json(providerRegistry.getConfig());
4184
+ });
4185
+ addRoute("PATCH", "/api/auto-memory/config", async (req) => {
4186
+ const body = await readJson(req) ?? {};
4187
+ const patch = {};
4188
+ if (body.provider)
4189
+ patch.provider = body.provider;
4190
+ if (body.model)
4191
+ patch.model = body.model;
4192
+ if (body.enabled !== undefined)
4193
+ patch.enabled = Boolean(body.enabled);
4194
+ if (body.min_importance !== undefined)
4195
+ patch.minImportance = Number(body.min_importance);
4196
+ if (body.auto_entity_link !== undefined)
4197
+ patch.autoEntityLink = Boolean(body.auto_entity_link);
4198
+ configureAutoMemory(patch);
4199
+ return json({ updated: true, config: providerRegistry.getConfig() });
4200
+ });
4201
+ addRoute("POST", "/api/auto-memory/test", async (req) => {
4202
+ const body = await readJson(req) ?? {};
4203
+ const { turn, provider: providerName, agent_id, project_id } = body;
4204
+ if (!turn)
4205
+ return errorResponse("turn is required", 400);
4206
+ const provider = providerName ? providerRegistry.getProvider(providerName) : providerRegistry.getAvailable();
4207
+ if (!provider)
4208
+ return errorResponse("No LLM provider configured. Set an API key (ANTHROPIC_API_KEY, OPENAI_API_KEY, CEREBRAS_API_KEY, or XAI_API_KEY).", 503);
4209
+ const memories = await provider.extractMemories(turn, { agentId: agent_id, projectId: project_id });
4210
+ return json({
4211
+ provider: provider.name,
4212
+ model: provider.config.model,
4213
+ extracted: memories,
4214
+ count: memories.length,
4215
+ note: "DRY RUN \u2014 nothing was saved"
4216
+ });
4217
+ });
4218
+ addRoute("GET", "/api/hooks", (_req, url) => {
4219
+ const type = url.searchParams.get("type") ?? undefined;
4220
+ const hooks = hookRegistry.list(type);
4221
+ return json(hooks.map((h) => ({
4222
+ id: h.id,
4223
+ type: h.type,
4224
+ blocking: h.blocking,
4225
+ priority: h.priority,
4226
+ builtin: h.builtin ?? false,
4227
+ agentId: h.agentId,
4228
+ projectId: h.projectId,
4229
+ description: h.description
4230
+ })));
4231
+ });
4232
+ addRoute("GET", "/api/hooks/stats", () => json(hookRegistry.stats()));
4233
+ addRoute("GET", "/api/webhooks", (_req, url) => {
4234
+ const type = url.searchParams.get("type") ?? undefined;
4235
+ const enabledParam = url.searchParams.get("enabled");
4236
+ const enabled = enabledParam !== null ? enabledParam === "true" : undefined;
4237
+ return json(listWebhookHooks({
4238
+ type,
4239
+ enabled
4240
+ }));
4241
+ });
4242
+ addRoute("POST", "/api/webhooks", async (req) => {
4243
+ const body = await readJson(req) ?? {};
4244
+ if (!body.type || !body.handler_url) {
4245
+ return errorResponse("type and handler_url are required", 400);
4246
+ }
4247
+ const wh = createWebhookHook({
4248
+ type: body.type,
4249
+ handlerUrl: body.handler_url,
4250
+ priority: body.priority,
4251
+ blocking: body.blocking,
4252
+ agentId: body.agent_id,
4253
+ projectId: body.project_id,
4254
+ description: body.description
4255
+ });
4256
+ reloadWebhooks();
4257
+ return json(wh, 201);
4258
+ });
4259
+ addRoute("GET", "/api/webhooks/:id", (_req, _url, params) => {
4260
+ const wh = getWebhookHook(params["id"]);
4261
+ if (!wh)
4262
+ return errorResponse("Webhook not found", 404);
4263
+ return json(wh);
4264
+ });
4265
+ addRoute("PATCH", "/api/webhooks/:id", async (req, _url, params) => {
4266
+ const body = await readJson(req) ?? {};
4267
+ const updated = updateWebhookHook(params["id"], {
4268
+ enabled: body.enabled,
4269
+ priority: body.priority,
4270
+ description: body.description
4271
+ });
4272
+ if (!updated)
4273
+ return errorResponse("Webhook not found", 404);
4274
+ reloadWebhooks();
4275
+ return json(updated);
4276
+ });
4277
+ addRoute("DELETE", "/api/webhooks/:id", (_req, _url, params) => {
4278
+ const deleted = deleteWebhookHook(params["id"]);
4279
+ if (!deleted)
4280
+ return errorResponse("Webhook not found", 404);
4281
+ return new Response(null, { status: 204 });
4282
+ });
3164
4283
  function startServer(port) {
4284
+ loadWebhooksFromDb();
3165
4285
  const hostname = process.env["MEMENTOS_HOST"] ?? "127.0.0.1";
3166
4286
  Bun.serve({
3167
4287
  port,