@hasna/mementos 0.6.0 → 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.
@@ -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')),
@@ -352,71 +398,34 @@ var MIGRATIONS = [
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);
354
400
  `,
355
- `
401
+ `
356
402
  ALTER TABLE memories ADD COLUMN recall_count INTEGER NOT NULL DEFAULT 0;
357
403
  CREATE INDEX IF NOT EXISTS idx_memories_recall_count ON memories(recall_count DESC);
358
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);
359
424
  `
360
- ];
361
- var _db = null;
362
- function getDatabase(dbPath) {
363
- if (_db)
364
- return _db;
365
- const path = dbPath || getDbPath();
366
- ensureDir(path);
367
- _db = new Database(path, { create: true });
368
- _db.run("PRAGMA journal_mode = WAL");
369
- _db.run("PRAGMA busy_timeout = 5000");
370
- _db.run("PRAGMA foreign_keys = ON");
371
- runMigrations(_db);
372
- return _db;
373
- }
374
- function runMigrations(db) {
375
- try {
376
- const result = db.query("SELECT MAX(id) as max_id FROM _migrations").get();
377
- const currentLevel = result?.max_id ?? 0;
378
- for (let i = currentLevel;i < MIGRATIONS.length; i++) {
379
- try {
380
- db.exec(MIGRATIONS[i]);
381
- } catch {}
382
- }
383
- } catch {
384
- for (const migration of MIGRATIONS) {
385
- try {
386
- db.exec(migration);
387
- } catch {}
388
- }
389
- }
390
- }
391
- function now() {
392
- return new Date().toISOString();
393
- }
394
- function uuid() {
395
- return crypto.randomUUID();
396
- }
397
- function shortUuid() {
398
- return crypto.randomUUID().slice(0, 8);
399
- }
425
+ ];
426
+ });
400
427
 
401
428
  // src/lib/redact.ts
402
- var REDACTED = "[REDACTED]";
403
- var SECRET_PATTERNS = [
404
- { name: "openai_key", pattern: /sk-[a-zA-Z0-9_-]{20,}/g },
405
- { name: "anthropic_key", pattern: /sk-ant-[a-zA-Z0-9_-]{20,}/g },
406
- { name: "generic_key", pattern: /(?:pk|tok|key|token|api[_-]?key)[_-][a-zA-Z0-9_-]{16,}/gi },
407
- { name: "aws_key", pattern: /AKIA[A-Z0-9]{16}/g },
408
- { name: "aws_secret", pattern: /(?<=AWS_SECRET_ACCESS_KEY\s*=\s*)[A-Za-z0-9/+=]{40}/g },
409
- { name: "github_token", pattern: /gh[ps]_[a-zA-Z0-9]{36,}/g },
410
- { name: "github_oauth", pattern: /gho_[a-zA-Z0-9]{36,}/g },
411
- { name: "npm_token", pattern: /npm_[a-zA-Z0-9]{36,}/g },
412
- { name: "bearer", pattern: /Bearer\s+[a-zA-Z0-9_\-.]{20,}/g },
413
- { name: "conn_string", pattern: /(?:postgres|postgresql|mysql|mongodb|redis|amqp|mqtt):\/\/[^\s"'`]+@[^\s"'`]+/gi },
414
- { name: "env_secret", pattern: /(?:SECRET|TOKEN|PASSWORD|PASSPHRASE|API_KEY|PRIVATE_KEY|AUTH|CREDENTIAL)[_A-Z]*\s*=\s*["']?[^\s"'\n]{8,}["']?/gi },
415
- { name: "stripe_key", pattern: /(?:sk|pk|rk)_(?:test|live)_[a-zA-Z0-9]{20,}/g },
416
- { name: "slack_token", pattern: /xox[bpras]-[a-zA-Z0-9-]{20,}/g },
417
- { name: "jwt", pattern: /eyJ[a-zA-Z0-9_-]{10,}\.eyJ[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}/g },
418
- { name: "hex_secret", pattern: /(?<=(?:key|token|secret|password|hash)\s*[:=]\s*["']?)[0-9a-f]{32,}(?=["']?)/gi }
419
- ];
420
429
  function redactSecrets(text) {
421
430
  let result = text;
422
431
  for (const { pattern } of SECRET_PATTERNS) {
@@ -425,6 +434,109 @@ function redactSecrets(text) {
425
434
  }
426
435
  return result;
427
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
+ });
457
+
458
+ // src/lib/hooks.ts
459
+ function generateHookId() {
460
+ return `hook_${++_idCounter}_${Date.now().toString(36)}`;
461
+ }
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));
506
+ }
507
+ }
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;
527
+ }
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
+ };
534
+ }
535
+ }
536
+ var _idCounter = 0, hookRegistry;
537
+ var init_hooks = __esm(() => {
538
+ hookRegistry = new HookRegistry;
539
+ });
428
540
 
429
541
  // src/db/entity-memories.ts
430
542
  function parseEntityMemoryRow(row) {
@@ -475,8 +587,28 @@ function getEntityMemoryLinks(entityId, memoryId, db) {
475
587
  const rows = d.query(sql).all(...params);
476
588
  return rows.map(parseEntityMemoryRow);
477
589
  }
590
+ var init_entity_memories = __esm(() => {
591
+ init_database();
592
+ init_memories();
593
+ });
478
594
 
479
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
+ });
480
612
  function runEntityExtraction(_memory, _projectId, _d) {}
481
613
  function parseMemoryRow(row) {
482
614
  return {
@@ -579,9 +711,15 @@ function createMemory(input, dedupeMode = "merge", db) {
579
711
  insertTag.run(id, tag);
580
712
  }
581
713
  const memory = getMemory(id, d);
582
- try {
583
- runEntityExtraction(memory, input.project_id, d);
584
- } catch {}
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
+ });
585
723
  return memory;
586
724
  }
587
725
  function getMemory(id, db) {
@@ -591,6 +729,52 @@ function getMemory(id, db) {
591
729
  return null;
592
730
  return parseMemoryRow(row);
593
731
  }
732
+ function getMemoryByKey(key, scope, agentId, projectId, sessionId, db) {
733
+ const d = db || getDatabase();
734
+ let sql = "SELECT * FROM memories WHERE key = ?";
735
+ const params = [key];
736
+ if (scope) {
737
+ sql += " AND scope = ?";
738
+ params.push(scope);
739
+ }
740
+ if (agentId) {
741
+ sql += " AND agent_id = ?";
742
+ params.push(agentId);
743
+ }
744
+ if (projectId) {
745
+ sql += " AND project_id = ?";
746
+ params.push(projectId);
747
+ }
748
+ if (sessionId) {
749
+ sql += " AND session_id = ?";
750
+ params.push(sessionId);
751
+ }
752
+ sql += " AND status = 'active' ORDER BY importance DESC LIMIT 1";
753
+ const row = d.query(sql).get(...params);
754
+ if (!row)
755
+ return null;
756
+ return parseMemoryRow(row);
757
+ }
758
+ function getMemoriesByKey(key, scope, agentId, projectId, db) {
759
+ const d = db || getDatabase();
760
+ let sql = "SELECT * FROM memories WHERE key = ?";
761
+ const params = [key];
762
+ if (scope) {
763
+ sql += " AND scope = ?";
764
+ params.push(scope);
765
+ }
766
+ if (agentId) {
767
+ sql += " AND agent_id = ?";
768
+ params.push(agentId);
769
+ }
770
+ if (projectId) {
771
+ sql += " AND project_id = ?";
772
+ params.push(projectId);
773
+ }
774
+ sql += " AND status = 'active' ORDER BY importance DESC";
775
+ const rows = d.query(sql).all(...params);
776
+ return rows.map(parseMemoryRow);
777
+ }
594
778
  function listMemories(filter, db) {
595
779
  const d = db || getDatabase();
596
780
  const conditions = [];
@@ -759,26 +943,65 @@ function updateMemory(id, input, db) {
759
943
  params.push(id);
760
944
  d.run(`UPDATE memories SET ${sets.join(", ")} WHERE id = ?`, params);
761
945
  const updated = getMemory(id, d);
762
- try {
763
- if (input.value !== undefined) {
946
+ if (input.value !== undefined) {
947
+ try {
764
948
  const oldLinks = getEntityMemoryLinks(undefined, updated.id, d);
765
949
  for (const link of oldLinks) {
766
950
  unlinkEntityFromMemory(link.entity_id, updated.id, d);
767
951
  }
768
- runEntityExtraction(updated, existing.project_id || undefined, d);
769
- }
770
- } catch {}
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
+ });
771
962
  return updated;
772
963
  }
773
964
  function deleteMemory(id, db) {
774
965
  const d = db || getDatabase();
775
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
+ }
776
973
  return result.changes > 0;
777
974
  }
975
+ function bulkDeleteMemories(ids, db) {
976
+ const d = db || getDatabase();
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;
986
+ }
778
987
  function touchMemory(id, db) {
779
988
  const d = db || getDatabase();
780
989
  d.run("UPDATE memories SET access_count = access_count + 1, accessed_at = ? WHERE id = ?", [now(), id]);
781
990
  }
991
+ function incrementRecallCount(id, db) {
992
+ const d = db || getDatabase();
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 {}
1004
+ }
782
1005
  function cleanExpiredMemories(db) {
783
1006
  const d = db || getDatabase();
784
1007
  const timestamp = now();
@@ -811,994 +1034,595 @@ function getMemoryVersions(memoryId, db) {
811
1034
  return [];
812
1035
  }
813
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
+ });
814
1045
 
815
- // src/db/agents.ts
816
- var CONFLICT_WINDOW_MS = 30 * 60 * 1000;
817
- function parseAgentRow(row) {
1046
+ // src/db/entities.ts
1047
+ function parseEntityRow(row) {
818
1048
  return {
819
1049
  id: row["id"],
820
1050
  name: row["name"],
821
- session_id: row["session_id"] || null,
1051
+ type: row["type"],
822
1052
  description: row["description"] || null,
823
- role: row["role"] || null,
824
1053
  metadata: JSON.parse(row["metadata"] || "{}"),
825
- active_project_id: row["active_project_id"] || null,
1054
+ project_id: row["project_id"] || null,
826
1055
  created_at: row["created_at"],
827
- last_seen_at: row["last_seen_at"]
1056
+ updated_at: row["updated_at"]
828
1057
  };
829
1058
  }
830
- function registerAgent(name, sessionId, description, role, projectId, db) {
1059
+ function createEntity(input, db) {
831
1060
  const d = db || getDatabase();
832
1061
  const timestamp = now();
833
- const normalizedName = name.trim().toLowerCase();
834
- const existing = d.query("SELECT * FROM agents WHERE LOWER(name) = ?").get(normalizedName);
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 || "");
835
1065
  if (existing) {
836
- const existingId = existing["id"];
837
- const existingSessionId = existing["session_id"] || null;
838
- const existingLastSeen = existing["last_seen_at"];
839
- if (sessionId && existingSessionId && existingSessionId !== sessionId) {
840
- const lastSeenMs = new Date(existingLastSeen).getTime();
841
- const nowMs = Date.now();
842
- if (nowMs - lastSeenMs < CONFLICT_WINDOW_MS) {
843
- throw new AgentConflictError({
844
- existing_id: existingId,
845
- existing_name: normalizedName,
846
- last_seen_at: existingLastSeen,
847
- session_hint: existingSessionId.slice(0, 8),
848
- working_dir: null
849
- });
850
- }
851
- }
852
- d.run("UPDATE agents SET last_seen_at = ?, session_id = ? WHERE id = ?", [
853
- timestamp,
854
- sessionId ?? existingSessionId,
855
- existingId
856
- ]);
857
- if (description) {
858
- d.run("UPDATE agents SET description = ? WHERE id = ?", [description, existingId]);
1066
+ const sets = ["updated_at = ?"];
1067
+ const params = [timestamp];
1068
+ if (input.description !== undefined) {
1069
+ sets.push("description = ?");
1070
+ params.push(input.description);
859
1071
  }
860
- if (role) {
861
- d.run("UPDATE agents SET role = ? WHERE id = ?", [role, existingId]);
862
- }
863
- if (projectId !== undefined) {
864
- d.run("UPDATE agents SET active_project_id = ? WHERE id = ?", [projectId, existingId]);
1072
+ if (input.metadata !== undefined) {
1073
+ sets.push("metadata = ?");
1074
+ params.push(metadataJson);
865
1075
  }
866
- return getAgent(existingId, d);
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);
867
1080
  }
868
1081
  const id = shortUuid();
869
- d.run("INSERT INTO agents (id, name, session_id, description, role, active_project_id, created_at, last_seen_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [id, normalizedName, sessionId ?? null, description || null, role || "agent", projectId ?? null, timestamp, timestamp]);
870
- return getAgent(id, d);
871
- }
872
- function getAgent(idOrName, db) {
873
- const d = db || getDatabase();
874
- let row = d.query("SELECT * FROM agents WHERE id = ?").get(idOrName);
875
- if (row)
876
- return parseAgentRow(row);
877
- row = d.query("SELECT * FROM agents WHERE LOWER(name) = ?").get(idOrName.trim().toLowerCase());
878
- if (row)
879
- return parseAgentRow(row);
880
- const rows = d.query("SELECT * FROM agents WHERE id LIKE ?").all(`${idOrName}%`);
881
- if (rows.length === 1)
882
- return parseAgentRow(rows[0]);
883
- return null;
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);
884
1101
  }
885
- function listAgents(db) {
1102
+ function getEntity(id, db) {
886
1103
  const d = db || getDatabase();
887
- const rows = d.query("SELECT * FROM agents ORDER BY last_seen_at DESC").all();
888
- return rows.map(parseAgentRow);
1104
+ const row = d.query("SELECT * FROM entities WHERE id = ?").get(id);
1105
+ if (!row)
1106
+ throw new EntityNotFoundError(id);
1107
+ return parseEntityRow(row);
889
1108
  }
890
- function listAgentsByProject(projectId, db) {
1109
+ function getEntityByName(name, type, projectId, db) {
891
1110
  const d = db || getDatabase();
892
- const rows = d.query("SELECT * FROM agents WHERE active_project_id = ? ORDER BY last_seen_at DESC").all(projectId);
893
- return rows.map(parseAgentRow);
1111
+ let sql = "SELECT * FROM entities WHERE name = ?";
1112
+ const params = [name];
1113
+ if (type) {
1114
+ sql += " AND type = ?";
1115
+ params.push(type);
1116
+ }
1117
+ if (projectId !== undefined) {
1118
+ sql += " AND project_id = ?";
1119
+ params.push(projectId);
1120
+ }
1121
+ sql += " LIMIT 1";
1122
+ const row = d.query(sql).get(...params);
1123
+ if (!row)
1124
+ return null;
1125
+ return parseEntityRow(row);
894
1126
  }
895
- function updateAgent(id, updates, db) {
1127
+ function listEntities(filter = {}, db) {
896
1128
  const d = db || getDatabase();
897
- const agent = getAgent(id, d);
898
- if (!agent)
899
- return null;
900
- const timestamp = now();
901
- if (updates.name) {
902
- const normalizedNewName = updates.name.trim().toLowerCase();
903
- if (normalizedNewName !== agent.name) {
904
- const existing = d.query("SELECT id FROM agents WHERE LOWER(name) = ? AND id != ?").get(normalizedNewName, agent.id);
905
- if (existing) {
906
- throw new Error(`Agent name already taken: ${normalizedNewName}`);
907
- }
908
- d.run("UPDATE agents SET name = ? WHERE id = ?", [normalizedNewName, agent.id]);
909
- }
1129
+ const conditions = [];
1130
+ const params = [];
1131
+ if (filter.type) {
1132
+ conditions.push("type = ?");
1133
+ params.push(filter.type);
910
1134
  }
911
- if (updates.description !== undefined) {
912
- d.run("UPDATE agents SET description = ? WHERE id = ?", [updates.description, agent.id]);
1135
+ if (filter.project_id) {
1136
+ conditions.push("project_id = ?");
1137
+ params.push(filter.project_id);
913
1138
  }
914
- if (updates.role !== undefined) {
915
- d.run("UPDATE agents SET role = ? WHERE id = ?", [updates.role, agent.id]);
1139
+ if (filter.search) {
1140
+ conditions.push("(name LIKE ? OR description LIKE ?)");
1141
+ const term = `%${filter.search}%`;
1142
+ params.push(term, term);
916
1143
  }
917
- if (updates.metadata !== undefined) {
918
- d.run("UPDATE agents SET metadata = ? WHERE id = ?", [JSON.stringify(updates.metadata), agent.id]);
1144
+ let sql = "SELECT * FROM entities";
1145
+ if (conditions.length > 0) {
1146
+ sql += ` WHERE ${conditions.join(" AND ")}`;
919
1147
  }
920
- if ("active_project_id" in updates) {
921
- d.run("UPDATE agents SET active_project_id = ? WHERE id = ?", [updates.active_project_id ?? null, agent.id]);
1148
+ sql += " ORDER BY updated_at DESC";
1149
+ if (filter.limit) {
1150
+ sql += " LIMIT ?";
1151
+ params.push(filter.limit);
922
1152
  }
923
- d.run("UPDATE agents SET last_seen_at = ? WHERE id = ?", [timestamp, agent.id]);
924
- return getAgent(agent.id, d);
925
- }
926
-
927
- // src/db/locks.ts
928
- function parseLockRow(row) {
929
- return {
930
- id: row["id"],
931
- resource_type: row["resource_type"],
932
- resource_id: row["resource_id"],
933
- agent_id: row["agent_id"],
934
- lock_type: row["lock_type"],
935
- locked_at: row["locked_at"],
936
- expires_at: row["expires_at"]
937
- };
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);
938
1159
  }
939
- function acquireLock(agentId, resourceType, resourceId, lockType = "exclusive", ttlSeconds = 300, db) {
1160
+ function updateEntity(id, input, db) {
940
1161
  const d = db || getDatabase();
941
- cleanExpiredLocks(d);
942
- const ownLock = d.query("SELECT * FROM resource_locks WHERE resource_type = ? AND resource_id = ? AND agent_id = ? AND lock_type = ? AND expires_at > datetime('now')").get(resourceType, resourceId, agentId, lockType);
943
- if (ownLock) {
944
- const newExpiry = new Date(Date.now() + ttlSeconds * 1000).toISOString();
945
- d.run("UPDATE resource_locks SET expires_at = ? WHERE id = ?", [
946
- newExpiry,
947
- ownLock["id"]
948
- ]);
949
- return parseLockRow({ ...ownLock, expires_at: newExpiry });
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);
950
1170
  }
951
- if (lockType === "exclusive") {
952
- const existing = d.query("SELECT * FROM resource_locks WHERE resource_type = ? AND resource_id = ? AND lock_type = 'exclusive' AND agent_id != ? AND expires_at > datetime('now')").get(resourceType, resourceId, agentId);
953
- if (existing) {
954
- return null;
955
- }
1171
+ if (input.type !== undefined) {
1172
+ sets.push("type = ?");
1173
+ params.push(input.type);
956
1174
  }
957
- const id = shortUuid();
958
- const lockedAt = now();
959
- const expiresAt = new Date(Date.now() + ttlSeconds * 1000).toISOString();
960
- d.run("INSERT INTO resource_locks (id, resource_type, resource_id, agent_id, lock_type, locked_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?)", [id, resourceType, resourceId, agentId, lockType, lockedAt, expiresAt]);
961
- return {
962
- id,
963
- resource_type: resourceType,
964
- resource_id: resourceId,
965
- agent_id: agentId,
966
- lock_type: lockType,
967
- locked_at: lockedAt,
968
- expires_at: expiresAt
969
- };
970
- }
971
- function releaseLock(lockId, agentId, db) {
972
- const d = db || getDatabase();
973
- const result = d.run("DELETE FROM resource_locks WHERE id = ? AND agent_id = ?", [lockId, agentId]);
974
- return result.changes > 0;
975
- }
976
- function releaseAllAgentLocks(agentId, db) {
977
- const d = db || getDatabase();
978
- const result = d.run("DELETE FROM resource_locks WHERE agent_id = ?", [agentId]);
979
- return result.changes;
980
- }
981
- function checkLock(resourceType, resourceId, lockType, db) {
982
- const d = db || getDatabase();
983
- cleanExpiredLocks(d);
984
- const query = lockType ? "SELECT * FROM resource_locks WHERE resource_type = ? AND resource_id = ? AND lock_type = ? AND expires_at > datetime('now')" : "SELECT * FROM resource_locks WHERE resource_type = ? AND resource_id = ? AND expires_at > datetime('now')";
985
- const rows = lockType ? d.query(query).all(resourceType, resourceId, lockType) : d.query(query).all(resourceType, resourceId);
986
- return rows.map(parseLockRow);
1175
+ if (input.description !== undefined) {
1176
+ sets.push("description = ?");
1177
+ params.push(input.description);
1178
+ }
1179
+ if (input.metadata !== undefined) {
1180
+ sets.push("metadata = ?");
1181
+ params.push(JSON.stringify(input.metadata));
1182
+ }
1183
+ params.push(id);
1184
+ d.run(`UPDATE entities SET ${sets.join(", ")} WHERE id = ?`, params);
1185
+ return getEntity(id, d);
987
1186
  }
988
- function listAgentLocks(agentId, db) {
1187
+ function deleteEntity(id, db) {
989
1188
  const d = db || getDatabase();
990
- cleanExpiredLocks(d);
991
- const rows = d.query("SELECT * FROM resource_locks WHERE agent_id = ? AND expires_at > datetime('now') ORDER BY locked_at DESC").all(agentId);
992
- return rows.map(parseLockRow);
1189
+ const result = d.run("DELETE FROM entities WHERE id = ?", [id]);
1190
+ if (result.changes === 0)
1191
+ throw new EntityNotFoundError(id);
993
1192
  }
994
- function cleanExpiredLocks(db) {
1193
+ function mergeEntities(sourceId, targetId, db) {
995
1194
  const d = db || getDatabase();
996
- const result = d.run("DELETE FROM resource_locks WHERE expires_at <= datetime('now')");
997
- return result.changes;
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);
998
1208
  }
1209
+ var init_entities = __esm(() => {
1210
+ init_database();
1211
+ init_types();
1212
+ init_hooks();
1213
+ });
999
1214
 
1000
- // src/db/projects.ts
1001
- function parseProjectRow(row) {
1215
+ // src/lib/search.ts
1216
+ function parseMemoryRow2(row) {
1002
1217
  return {
1003
1218
  id: row["id"],
1004
- name: row["name"],
1005
- path: row["path"],
1006
- description: row["description"] || null,
1007
- memory_prefix: row["memory_prefix"] || null,
1219
+ key: row["key"],
1220
+ value: row["value"],
1221
+ category: row["category"],
1222
+ scope: row["scope"],
1223
+ summary: row["summary"] || null,
1224
+ tags: JSON.parse(row["tags"] || "[]"),
1225
+ importance: row["importance"],
1226
+ source: row["source"],
1227
+ status: row["status"],
1228
+ pinned: !!row["pinned"],
1229
+ agent_id: row["agent_id"] || null,
1230
+ project_id: row["project_id"] || null,
1231
+ session_id: row["session_id"] || null,
1232
+ metadata: JSON.parse(row["metadata"] || "{}"),
1233
+ access_count: row["access_count"],
1234
+ version: row["version"],
1235
+ expires_at: row["expires_at"] || null,
1008
1236
  created_at: row["created_at"],
1009
- updated_at: row["updated_at"]
1237
+ updated_at: row["updated_at"],
1238
+ accessed_at: row["accessed_at"] || null
1010
1239
  };
1011
1240
  }
1012
- function registerProject(name, path, description, memoryPrefix, db) {
1013
- const d = db || getDatabase();
1014
- const timestamp = now();
1015
- const existing = d.query("SELECT * FROM projects WHERE path = ?").get(path);
1016
- if (existing) {
1017
- const existingId = existing["id"];
1018
- d.run("UPDATE projects SET updated_at = ? WHERE id = ?", [
1019
- timestamp,
1020
- existingId
1021
- ]);
1022
- return parseProjectRow(existing);
1023
- }
1024
- const id = uuid();
1025
- d.run("INSERT INTO projects (id, name, path, description, memory_prefix, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)", [id, name, path, description || null, memoryPrefix || null, timestamp, timestamp]);
1026
- return getProject(id, d);
1241
+ function preprocessQuery(query) {
1242
+ let q = query.trim();
1243
+ q = q.replace(/\s+/g, " ");
1244
+ q = q.normalize("NFC");
1245
+ return q;
1027
1246
  }
1028
- function getProject(idOrPath, db) {
1029
- const d = db || getDatabase();
1030
- let row = d.query("SELECT * FROM projects WHERE id = ?").get(idOrPath);
1031
- if (row)
1032
- return parseProjectRow(row);
1033
- row = d.query("SELECT * FROM projects WHERE path = ?").get(idOrPath);
1034
- if (row)
1035
- return parseProjectRow(row);
1036
- row = d.query("SELECT * FROM projects WHERE LOWER(name) = ?").get(idOrPath.toLowerCase());
1037
- if (row)
1038
- return parseProjectRow(row);
1039
- return null;
1247
+ function escapeLikePattern(s) {
1248
+ return s.replace(/%/g, "\\%").replace(/_/g, "\\_");
1040
1249
  }
1041
- function listProjects(db) {
1042
- const d = db || getDatabase();
1043
- const rows = d.query("SELECT * FROM projects ORDER BY updated_at DESC").all();
1044
- return rows.map(parseProjectRow);
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;
1045
1255
  }
1046
-
1047
- // src/lib/config.ts
1048
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, readdirSync, writeFileSync, unlinkSync } from "fs";
1049
- import { homedir } from "os";
1050
- import { basename, dirname as dirname2, join as join2, resolve as resolve2 } from "path";
1051
- function findFileWalkingUp(filename) {
1052
- let dir = process.cwd();
1053
- while (true) {
1054
- const candidate = join2(dir, filename);
1055
- if (existsSync2(candidate)) {
1056
- return candidate;
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;
1277
+ }
1057
1278
  }
1058
- const parent = dirname2(dir);
1059
- if (parent === dir) {
1060
- return null;
1279
+ }
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 });
1061
1283
  }
1062
- dir = parent;
1063
1284
  }
1285
+ return highlights;
1064
1286
  }
1065
- function findGitRoot2() {
1066
- let dir = process.cwd();
1067
- while (true) {
1068
- if (existsSync2(join2(dir, ".git"))) {
1069
- return dir;
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";
1295
+ }
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;
1343
+ }
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;
1348
+ }
1349
+ if (memory.summary && memory.summary.toLowerCase().includes(token)) {
1350
+ tokenScore += 4 / tokens.length;
1351
+ }
1352
+ if (memory.value.toLowerCase().includes(token)) {
1353
+ tokenScore += 3 / tokens.length;
1354
+ }
1355
+ if (metadataStr !== "{}" && metadataStr.includes(token)) {
1356
+ tokenScore += 2 / tokens.length;
1357
+ }
1070
1358
  }
1071
- const parent = dirname2(dir);
1072
- if (parent === dir) {
1073
- return null;
1359
+ if (score > 0) {
1360
+ score += tokenScore * 0.3;
1361
+ } else {
1362
+ score += tokenScore;
1074
1363
  }
1075
- dir = parent;
1076
1364
  }
1365
+ return score;
1077
1366
  }
1078
- function profilesDir() {
1079
- return join2(homedir(), ".mementos", "profiles");
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() };
1080
1374
  }
1081
- function globalConfigPath() {
1082
- return join2(homedir(), ".mementos", "config.json");
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, '""')}"`);
1380
+ }
1381
+ const tokens = removeStopWords(remainder.split(/\s+/).filter(Boolean));
1382
+ for (const t of tokens) {
1383
+ parts.push(`"${t.replace(/"/g, '""')}"`);
1384
+ }
1385
+ return parts.join(" ");
1083
1386
  }
1084
- function readGlobalConfig() {
1085
- const p = globalConfigPath();
1086
- if (!existsSync2(p))
1087
- return {};
1387
+ function hasFts5Table(d) {
1088
1388
  try {
1089
- return JSON.parse(readFileSync(p, "utf-8"));
1389
+ const row = d.query("SELECT name FROM sqlite_master WHERE type='table' AND name='memories_fts'").get();
1390
+ return !!row;
1090
1391
  } catch {
1091
- return {};
1392
+ return false;
1092
1393
  }
1093
1394
  }
1094
- function getActiveProfile() {
1095
- const envProfile = process.env["MEMENTOS_PROFILE"];
1096
- if (envProfile)
1097
- return envProfile.trim();
1098
- const cfg = readGlobalConfig();
1099
- return cfg["active_profile"] || null;
1100
- }
1101
- function listProfiles() {
1102
- const dir = profilesDir();
1103
- if (!existsSync2(dir))
1104
- return [];
1105
- return readdirSync(dir).filter((f) => f.endsWith(".db")).map((f) => basename(f, ".db")).sort();
1106
- }
1107
- function getDbPath2() {
1108
- const envDbPath = process.env["MEMENTOS_DB_PATH"];
1109
- if (envDbPath) {
1110
- const resolved = resolve2(envDbPath);
1111
- ensureDir2(dirname2(resolved));
1112
- return resolved;
1113
- }
1114
- const profile = getActiveProfile();
1115
- if (profile) {
1116
- const profilePath = join2(profilesDir(), `${profile}.db`);
1117
- ensureDir2(dirname2(profilePath));
1118
- return profilePath;
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
+ }
1119
1410
  }
1120
- const dbScope = process.env["MEMENTOS_DB_SCOPE"];
1121
- if (dbScope === "project") {
1122
- const gitRoot = findGitRoot2();
1123
- if (gitRoot) {
1124
- const dbPath = join2(gitRoot, ".mementos", "mementos.db");
1125
- ensureDir2(dirname2(dbPath));
1126
- return dbPath;
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);
1127
1418
  }
1128
1419
  }
1129
- const found = findFileWalkingUp(join2(".mementos", "mementos.db"));
1130
- if (found) {
1131
- return found;
1132
- }
1133
- const fallback = join2(homedir(), ".mementos", "mementos.db");
1134
- ensureDir2(dirname2(fallback));
1135
- return fallback;
1136
- }
1137
- function ensureDir2(dir) {
1138
- if (!existsSync2(dir)) {
1139
- mkdirSync2(dir, { recursive: true });
1140
- }
1141
- }
1142
-
1143
- // src/db/entities.ts
1144
- function parseEntityRow(row) {
1145
- return {
1146
- id: row["id"],
1147
- name: row["name"],
1148
- type: row["type"],
1149
- description: row["description"] || null,
1150
- metadata: JSON.parse(row["metadata"] || "{}"),
1151
- project_id: row["project_id"] || null,
1152
- created_at: row["created_at"],
1153
- updated_at: row["updated_at"]
1154
- };
1155
- }
1156
- function createEntity(input, db) {
1157
- const d = db || getDatabase();
1158
- const timestamp = now();
1159
- const metadataJson = JSON.stringify(input.metadata || {});
1160
- const existing = d.query(`SELECT * FROM entities
1161
- WHERE name = ? AND type = ? AND COALESCE(project_id, '') = ?`).get(input.name, input.type, input.project_id || "");
1162
- if (existing) {
1163
- const sets = ["updated_at = ?"];
1164
- const params = [timestamp];
1165
- if (input.description !== undefined) {
1166
- sets.push("description = ?");
1167
- params.push(input.description);
1168
- }
1169
- if (input.metadata !== undefined) {
1170
- sets.push("metadata = ?");
1171
- params.push(metadataJson);
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);
1172
1427
  }
1173
- const existingId = existing["id"];
1174
- params.push(existingId);
1175
- d.run(`UPDATE entities SET ${sets.join(", ")} WHERE id = ?`, params);
1176
- return getEntity(existingId, d);
1177
- }
1178
- const id = shortUuid();
1179
- d.run(`INSERT INTO entities (id, name, type, description, metadata, project_id, created_at, updated_at)
1180
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
1181
- id,
1182
- input.name,
1183
- input.type,
1184
- input.description || null,
1185
- metadataJson,
1186
- input.project_id || null,
1187
- timestamp,
1188
- timestamp
1189
- ]);
1190
- return getEntity(id, d);
1191
- }
1192
- function getEntity(id, db) {
1193
- const d = db || getDatabase();
1194
- const row = d.query("SELECT * FROM entities WHERE id = ?").get(id);
1195
- if (!row)
1196
- throw new EntityNotFoundError(id);
1197
- return parseEntityRow(row);
1198
- }
1199
- function getEntityByName(name, type, projectId, db) {
1200
- const d = db || getDatabase();
1201
- let sql = "SELECT * FROM entities WHERE name = ?";
1202
- const params = [name];
1203
- if (type) {
1204
- sql += " AND type = ?";
1205
- params.push(type);
1206
1428
  }
1207
- if (projectId !== undefined) {
1208
- sql += " AND project_id = ?";
1209
- params.push(projectId);
1210
- }
1211
- sql += " LIMIT 1";
1212
- const row = d.query(sql).get(...params);
1213
- if (!row)
1214
- return null;
1215
- return parseEntityRow(row);
1216
- }
1217
- function listEntities(filter = {}, db) {
1218
- const d = db || getDatabase();
1219
- const conditions = [];
1220
- const params = [];
1221
- if (filter.type) {
1222
- conditions.push("type = ?");
1223
- params.push(filter.type);
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
+ }
1224
1438
  }
1225
1439
  if (filter.project_id) {
1226
- conditions.push("project_id = ?");
1440
+ conditions.push("m.project_id = ?");
1227
1441
  params.push(filter.project_id);
1228
1442
  }
1229
- if (filter.search) {
1230
- conditions.push("(name LIKE ? OR description LIKE ?)");
1231
- const term = `%${filter.search}%`;
1232
- params.push(term, term);
1443
+ if (filter.agent_id) {
1444
+ conditions.push("m.agent_id = ?");
1445
+ params.push(filter.agent_id);
1233
1446
  }
1234
- let sql = "SELECT * FROM entities";
1235
- if (conditions.length > 0) {
1236
- sql += ` WHERE ${conditions.join(" AND ")}`;
1447
+ if (filter.session_id) {
1448
+ conditions.push("m.session_id = ?");
1449
+ params.push(filter.session_id);
1237
1450
  }
1238
- sql += " ORDER BY updated_at DESC";
1239
- if (filter.limit) {
1240
- sql += " LIMIT ?";
1241
- params.push(filter.limit);
1451
+ if (filter.min_importance) {
1452
+ conditions.push("m.importance >= ?");
1453
+ params.push(filter.min_importance);
1242
1454
  }
1243
- if (filter.offset) {
1244
- sql += " OFFSET ?";
1245
- params.push(filter.offset);
1455
+ if (filter.pinned !== undefined) {
1456
+ conditions.push("m.pinned = ?");
1457
+ params.push(filter.pinned ? 1 : 0);
1246
1458
  }
1247
- const rows = d.query(sql).all(...params);
1248
- return rows.map(parseEntityRow);
1249
- }
1250
- function updateEntity(id, input, db) {
1251
- const d = db || getDatabase();
1252
- const existing = d.query("SELECT id FROM entities WHERE id = ?").get(id);
1253
- if (!existing)
1254
- throw new EntityNotFoundError(id);
1255
- const sets = ["updated_at = ?"];
1256
- const params = [now()];
1257
- if (input.name !== undefined) {
1258
- sets.push("name = ?");
1259
- params.push(input.name);
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);
1463
+ }
1260
1464
  }
1261
- if (input.type !== undefined) {
1262
- sets.push("type = ?");
1263
- params.push(input.type);
1465
+ return { conditions, params };
1466
+ }
1467
+ function searchWithFts5(d, query, queryLower, filter, graphBoostedIds) {
1468
+ const ftsQuery = escapeFts5Query(query);
1469
+ if (!ftsQuery)
1470
+ return null;
1471
+ try {
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);
1480
+ } catch {
1481
+ return null;
1264
1482
  }
1265
- if (input.description !== undefined) {
1266
- sets.push("description = ?");
1267
- params.push(input.description);
1483
+ }
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)}%`);
1268
1493
  }
1269
- if (input.metadata !== undefined) {
1270
- sets.push("metadata = ?");
1271
- params.push(JSON.stringify(input.metadata));
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);
1272
1506
  }
1273
- params.push(id);
1274
- d.run(`UPDATE entities SET ${sets.join(", ")} WHERE id = ?`, params);
1275
- return getEntity(id, d);
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);
1276
1511
  }
1277
- function deleteEntity(id, db) {
1278
- const d = db || getDatabase();
1279
- const result = d.run("DELETE FROM entities WHERE id = ?", [id]);
1280
- if (result.changes === 0)
1281
- throw new EntityNotFoundError(id);
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));
1517
+ }
1518
+ return trigrams;
1282
1519
  }
1283
- function mergeEntities(sourceId, targetId, db) {
1284
- const d = db || getDatabase();
1285
- getEntity(sourceId, d);
1286
- getEntity(targetId, d);
1287
- d.run(`UPDATE OR IGNORE relations SET source_entity_id = ? WHERE source_entity_id = ?`, [targetId, sourceId]);
1288
- d.run(`UPDATE OR IGNORE relations SET target_entity_id = ? WHERE target_entity_id = ?`, [targetId, sourceId]);
1289
- d.run("DELETE FROM relations WHERE source_entity_id = ? OR target_entity_id = ?", [
1290
- sourceId,
1291
- sourceId
1292
- ]);
1293
- d.run(`UPDATE OR IGNORE entity_memories SET entity_id = ? WHERE entity_id = ?`, [targetId, sourceId]);
1294
- d.run("DELETE FROM entity_memories WHERE entity_id = ?", [sourceId]);
1295
- d.run("DELETE FROM entities WHERE id = ?", [sourceId]);
1296
- d.run("UPDATE entities SET updated_at = ? WHERE id = ?", [now(), targetId]);
1297
- return getEntity(targetId, d);
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;
1298
1532
  }
1299
-
1300
- // src/lib/search.ts
1301
- function parseMemoryRow2(row) {
1302
- return {
1303
- id: row["id"],
1304
- key: row["key"],
1305
- value: row["value"],
1306
- category: row["category"],
1307
- scope: row["scope"],
1308
- summary: row["summary"] || null,
1309
- tags: JSON.parse(row["tags"] || "[]"),
1310
- importance: row["importance"],
1311
- source: row["source"],
1312
- status: row["status"],
1313
- pinned: !!row["pinned"],
1314
- agent_id: row["agent_id"] || null,
1315
- project_id: row["project_id"] || null,
1316
- session_id: row["session_id"] || null,
1317
- metadata: JSON.parse(row["metadata"] || "{}"),
1318
- access_count: row["access_count"],
1319
- version: row["version"],
1320
- expires_at: row["expires_at"] || null,
1321
- created_at: row["created_at"],
1322
- updated_at: row["updated_at"],
1323
- accessed_at: row["accessed_at"] || null
1324
- };
1325
- }
1326
- function preprocessQuery(query) {
1327
- let q = query.trim();
1328
- q = q.replace(/\s+/g, " ");
1329
- q = q.normalize("NFC");
1330
- return q;
1331
- }
1332
- function escapeLikePattern(s) {
1333
- return s.replace(/%/g, "\\%").replace(/_/g, "\\_");
1334
- }
1335
- var STOP_WORDS = new Set([
1336
- "a",
1337
- "an",
1338
- "the",
1339
- "is",
1340
- "are",
1341
- "was",
1342
- "were",
1343
- "be",
1344
- "been",
1345
- "being",
1346
- "have",
1347
- "has",
1348
- "had",
1349
- "do",
1350
- "does",
1351
- "did",
1352
- "will",
1353
- "would",
1354
- "could",
1355
- "should",
1356
- "may",
1357
- "might",
1358
- "shall",
1359
- "can",
1360
- "need",
1361
- "dare",
1362
- "ought",
1363
- "used",
1364
- "to",
1365
- "of",
1366
- "in",
1367
- "for",
1368
- "on",
1369
- "with",
1370
- "at",
1371
- "by",
1372
- "from",
1373
- "as",
1374
- "into",
1375
- "through",
1376
- "during",
1377
- "before",
1378
- "after",
1379
- "above",
1380
- "below",
1381
- "between",
1382
- "out",
1383
- "off",
1384
- "over",
1385
- "under",
1386
- "again",
1387
- "further",
1388
- "then",
1389
- "once",
1390
- "here",
1391
- "there",
1392
- "when",
1393
- "where",
1394
- "why",
1395
- "how",
1396
- "all",
1397
- "each",
1398
- "every",
1399
- "both",
1400
- "few",
1401
- "more",
1402
- "most",
1403
- "other",
1404
- "some",
1405
- "such",
1406
- "no",
1407
- "not",
1408
- "only",
1409
- "own",
1410
- "same",
1411
- "so",
1412
- "than",
1413
- "too",
1414
- "very",
1415
- "just",
1416
- "because",
1417
- "but",
1418
- "and",
1419
- "or",
1420
- "if",
1421
- "while",
1422
- "that",
1423
- "this",
1424
- "it"
1425
- ]);
1426
- function removeStopWords(tokens) {
1427
- if (tokens.length <= 1)
1428
- return tokens;
1429
- const filtered = tokens.filter((t) => !STOP_WORDS.has(t.toLowerCase()));
1430
- return filtered.length > 0 ? filtered : tokens;
1431
- }
1432
- function extractHighlights(memory, queryLower) {
1433
- const highlights = [];
1434
- const tokens = queryLower.split(/\s+/).filter(Boolean);
1435
- for (const field of ["key", "value", "summary"]) {
1436
- const text = field === "summary" ? memory.summary : memory[field];
1437
- if (!text)
1438
- continue;
1439
- const textLower = text.toLowerCase();
1440
- const searchTerms = [queryLower, ...tokens].filter(Boolean);
1441
- for (const term of searchTerms) {
1442
- const idx = textLower.indexOf(term);
1443
- if (idx !== -1) {
1444
- const start = Math.max(0, idx - 30);
1445
- const end = Math.min(text.length, idx + term.length + 30);
1446
- const prefix = start > 0 ? "..." : "";
1447
- const suffix = end < text.length ? "..." : "";
1448
- highlights.push({
1449
- field,
1450
- snippet: prefix + text.slice(start, end) + suffix
1451
- });
1452
- break;
1453
- }
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));
1454
1546
  }
1455
- }
1456
- for (const tag of memory.tags) {
1457
- if (tag.toLowerCase().includes(queryLower) || tokens.some((t) => tag.toLowerCase().includes(t))) {
1458
- highlights.push({ field: "tag", snippet: tag });
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" });
1459
1554
  }
1460
1555
  }
1461
- return highlights;
1462
- }
1463
- function determineMatchType(memory, queryLower) {
1464
- if (memory.key.toLowerCase() === queryLower)
1465
- return "exact";
1466
- if (memory.tags.some((t) => t.toLowerCase() === queryLower))
1467
- return "tag";
1468
- if (memory.tags.some((t) => t.toLowerCase().includes(queryLower)))
1469
- return "tag";
1470
- return "fuzzy";
1556
+ results.sort((a, b) => b.score - a.score);
1557
+ return results;
1471
1558
  }
1472
- function computeScore(memory, queryLower) {
1473
- const fieldScores = [];
1474
- const keyLower = memory.key.toLowerCase();
1475
- if (keyLower === queryLower) {
1476
- fieldScores.push(10);
1477
- } else if (keyLower.includes(queryLower)) {
1478
- fieldScores.push(7);
1479
- }
1480
- if (memory.tags.some((t) => t.toLowerCase() === queryLower)) {
1481
- fieldScores.push(6);
1482
- } else if (memory.tags.some((t) => t.toLowerCase().includes(queryLower))) {
1483
- fieldScores.push(3);
1484
- }
1485
- if (memory.summary && memory.summary.toLowerCase().includes(queryLower)) {
1486
- fieldScores.push(4);
1487
- }
1488
- if (memory.value.toLowerCase().includes(queryLower)) {
1489
- fieldScores.push(3);
1490
- }
1491
- const metadataStr = JSON.stringify(memory.metadata).toLowerCase();
1492
- if (metadataStr !== "{}" && metadataStr.includes(queryLower)) {
1493
- fieldScores.push(2);
1494
- }
1495
- fieldScores.sort((a, b) => b - a);
1496
- const diminishingMultipliers = [1, 0.5, 0.25, 0.15, 0.15];
1497
- let score = 0;
1498
- for (let i = 0;i < fieldScores.length; i++) {
1499
- score += fieldScores[i] * (diminishingMultipliers[i] ?? 0.15);
1500
- }
1501
- const { phrases } = extractQuotedPhrases(queryLower);
1502
- for (const phrase of phrases) {
1503
- if (keyLower.includes(phrase))
1504
- score += 8;
1505
- if (memory.value.toLowerCase().includes(phrase))
1506
- score += 5;
1507
- if (memory.summary && memory.summary.toLowerCase().includes(phrase))
1508
- score += 4;
1509
- }
1510
- const { remainder } = extractQuotedPhrases(queryLower);
1511
- const tokens = removeStopWords(remainder.split(/\s+/).filter(Boolean));
1512
- if (tokens.length > 1) {
1513
- let tokenScore = 0;
1514
- for (const token of tokens) {
1515
- if (keyLower === token) {
1516
- tokenScore += 10 / tokens.length;
1517
- } else if (keyLower.includes(token)) {
1518
- tokenScore += 7 / tokens.length;
1519
- }
1520
- if (memory.tags.some((t) => t.toLowerCase() === token)) {
1521
- tokenScore += 6 / tokens.length;
1522
- } else if (memory.tags.some((t) => t.toLowerCase().includes(token))) {
1523
- tokenScore += 3 / tokens.length;
1524
- }
1525
- if (memory.summary && memory.summary.toLowerCase().includes(token)) {
1526
- tokenScore += 4 / tokens.length;
1527
- }
1528
- if (memory.value.toLowerCase().includes(token)) {
1529
- tokenScore += 3 / tokens.length;
1530
- }
1531
- if (metadataStr !== "{}" && metadataStr.includes(token)) {
1532
- tokenScore += 2 / tokens.length;
1533
- }
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);
1534
1566
  }
1535
- if (score > 0) {
1536
- score += tokenScore * 0.3;
1537
- } else {
1538
- score += tokenScore;
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
+ }
1539
1572
  }
1540
- }
1541
- return score;
1542
- }
1543
- function extractQuotedPhrases(query) {
1544
- const phrases = [];
1545
- const remainder = query.replace(/"([^"]+)"/g, (_match, phrase) => {
1546
- phrases.push(phrase);
1547
- return "";
1548
- });
1549
- return { phrases, remainder: remainder.trim() };
1573
+ } catch {}
1574
+ return boostedIds;
1550
1575
  }
1551
- function escapeFts5Query(query) {
1552
- const { phrases, remainder } = extractQuotedPhrases(query);
1553
- const parts = [];
1554
- for (const phrase of phrases) {
1555
- parts.push(`"${phrase.replace(/"/g, '""')}"`);
1556
- }
1557
- const tokens = removeStopWords(remainder.split(/\s+/).filter(Boolean));
1558
- for (const t of tokens) {
1559
- parts.push(`"${t.replace(/"/g, '""')}"`);
1560
- }
1561
- return parts.join(" ");
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);
1562
1584
  }
1563
- function hasFts5Table(d) {
1564
- try {
1565
- const row = d.query("SELECT name FROM sqlite_master WHERE type='table' AND name='memories_fts'").get();
1566
- return !!row;
1567
- } catch {
1568
- return false;
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
+ });
1569
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;
1570
1611
  }
1571
- function buildFilterConditions(filter) {
1572
- const conditions = [];
1573
- const params = [];
1574
- conditions.push("m.status = 'active'");
1575
- conditions.push("(m.expires_at IS NULL OR m.expires_at >= datetime('now'))");
1576
- if (!filter)
1577
- return { conditions, params };
1578
- if (filter.scope) {
1579
- if (Array.isArray(filter.scope)) {
1580
- conditions.push(`m.scope IN (${filter.scope.map(() => "?").join(",")})`);
1581
- params.push(...filter.scope);
1612
+ function searchMemories(query, filter, db) {
1613
+ const d = db || getDatabase();
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;
1582
1624
  } else {
1583
- conditions.push("m.scope = ?");
1584
- params.push(filter.scope);
1585
- }
1586
- }
1587
- if (filter.category) {
1588
- if (Array.isArray(filter.category)) {
1589
- conditions.push(`m.category IN (${filter.category.map(() => "?").join(",")})`);
1590
- params.push(...filter.category);
1591
- } else {
1592
- conditions.push("m.category = ?");
1593
- params.push(filter.category);
1594
- }
1595
- }
1596
- if (filter.source) {
1597
- if (Array.isArray(filter.source)) {
1598
- conditions.push(`m.source IN (${filter.source.map(() => "?").join(",")})`);
1599
- params.push(...filter.source);
1600
- } else {
1601
- conditions.push("m.source = ?");
1602
- params.push(filter.source);
1603
- }
1604
- }
1605
- if (filter.status) {
1606
- conditions.shift();
1607
- if (Array.isArray(filter.status)) {
1608
- conditions.push(`m.status IN (${filter.status.map(() => "?").join(",")})`);
1609
- params.push(...filter.status);
1610
- } else {
1611
- conditions.push("m.status = ?");
1612
- params.push(filter.status);
1613
- }
1614
- }
1615
- if (filter.project_id) {
1616
- conditions.push("m.project_id = ?");
1617
- params.push(filter.project_id);
1618
- }
1619
- if (filter.agent_id) {
1620
- conditions.push("m.agent_id = ?");
1621
- params.push(filter.agent_id);
1622
- }
1623
- if (filter.session_id) {
1624
- conditions.push("m.session_id = ?");
1625
- params.push(filter.session_id);
1626
- }
1627
- if (filter.min_importance) {
1628
- conditions.push("m.importance >= ?");
1629
- params.push(filter.min_importance);
1630
- }
1631
- if (filter.pinned !== undefined) {
1632
- conditions.push("m.pinned = ?");
1633
- params.push(filter.pinned ? 1 : 0);
1634
- }
1635
- if (filter.tags && filter.tags.length > 0) {
1636
- for (const tag of filter.tags) {
1637
- conditions.push("m.id IN (SELECT memory_id FROM memory_tags WHERE tag = ?)");
1638
- params.push(tag);
1639
- }
1640
- }
1641
- return { conditions, params };
1642
- }
1643
- function searchWithFts5(d, query, queryLower, filter, graphBoostedIds) {
1644
- const ftsQuery = escapeFts5Query(query);
1645
- if (!ftsQuery)
1646
- return null;
1647
- try {
1648
- const { conditions, params } = buildFilterConditions(filter);
1649
- const queryParam = `%${query}%`;
1650
- 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 ?)`;
1651
- const allConditions = [ftsCondition, ...conditions];
1652
- const allParams = [ftsQuery, queryParam, queryParam, ...params];
1653
- const candidateSql = `SELECT m.* FROM memories m WHERE ${allConditions.join(" AND ")}`;
1654
- const rows = d.query(candidateSql).all(...allParams);
1655
- return scoreResults(rows, queryLower, graphBoostedIds);
1656
- } catch {
1657
- return null;
1658
- }
1659
- }
1660
- function searchWithLike(d, query, queryLower, filter, graphBoostedIds) {
1661
- const { conditions, params } = buildFilterConditions(filter);
1662
- const rawTokens = query.trim().split(/\s+/).filter(Boolean);
1663
- const tokens = removeStopWords(rawTokens);
1664
- const escapedQuery = escapeLikePattern(query);
1665
- const likePatterns = [`%${escapedQuery}%`];
1666
- if (tokens.length > 1) {
1667
- for (const t of tokens)
1668
- likePatterns.push(`%${escapeLikePattern(t)}%`);
1669
- }
1670
- const fieldClauses = [];
1671
- for (const pattern of likePatterns) {
1672
- fieldClauses.push("m.key LIKE ? ESCAPE '\\'");
1673
- params.push(pattern);
1674
- fieldClauses.push("m.value LIKE ? ESCAPE '\\'");
1675
- params.push(pattern);
1676
- fieldClauses.push("m.summary LIKE ? ESCAPE '\\'");
1677
- params.push(pattern);
1678
- fieldClauses.push("m.metadata LIKE ? ESCAPE '\\'");
1679
- params.push(pattern);
1680
- fieldClauses.push("m.id IN (SELECT memory_id FROM memory_tags WHERE tag LIKE ? ESCAPE '\\')");
1681
- params.push(pattern);
1682
- }
1683
- conditions.push(`(${fieldClauses.join(" OR ")})`);
1684
- const sql = `SELECT DISTINCT m.* FROM memories m WHERE ${conditions.join(" AND ")}`;
1685
- const rows = d.query(sql).all(...params);
1686
- return scoreResults(rows, queryLower, graphBoostedIds);
1687
- }
1688
- function generateTrigrams(s) {
1689
- const lower = s.toLowerCase();
1690
- const trigrams = new Set;
1691
- for (let i = 0;i <= lower.length - 3; i++) {
1692
- trigrams.add(lower.slice(i, i + 3));
1693
- }
1694
- return trigrams;
1695
- }
1696
- function trigramSimilarity(a, b) {
1697
- const triA = generateTrigrams(a);
1698
- const triB = generateTrigrams(b);
1699
- if (triA.size === 0 || triB.size === 0)
1700
- return 0;
1701
- let intersection = 0;
1702
- for (const t of triA) {
1703
- if (triB.has(t))
1704
- intersection++;
1705
- }
1706
- const union = triA.size + triB.size - intersection;
1707
- return union === 0 ? 0 : intersection / union;
1708
- }
1709
- function searchWithFuzzy(d, query, filter, graphBoostedIds) {
1710
- const { conditions, params } = buildFilterConditions(filter);
1711
- const sql = `SELECT m.* FROM memories m WHERE ${conditions.join(" AND ")}`;
1712
- const rows = d.query(sql).all(...params);
1713
- const MIN_SIMILARITY = 0.3;
1714
- const results = [];
1715
- for (const row of rows) {
1716
- const memory = parseMemoryRow2(row);
1717
- let bestSimilarity = 0;
1718
- bestSimilarity = Math.max(bestSimilarity, trigramSimilarity(query, memory.key));
1719
- bestSimilarity = Math.max(bestSimilarity, trigramSimilarity(query, memory.value.slice(0, 200)));
1720
- if (memory.summary) {
1721
- bestSimilarity = Math.max(bestSimilarity, trigramSimilarity(query, memory.summary));
1722
- }
1723
- for (const tag of memory.tags) {
1724
- bestSimilarity = Math.max(bestSimilarity, trigramSimilarity(query, tag));
1725
- }
1726
- if (bestSimilarity >= MIN_SIMILARITY) {
1727
- const graphBoost = graphBoostedIds?.has(memory.id) ? 2 : 0;
1728
- const score = bestSimilarity * 5 * memory.importance / 10 + graphBoost;
1729
- results.push({ memory, score, match_type: "fuzzy" });
1730
- }
1731
- }
1732
- results.sort((a, b) => b.score - a.score);
1733
- return results;
1734
- }
1735
- function getGraphBoostedMemoryIds(query, d) {
1736
- const boostedIds = new Set;
1737
- try {
1738
- const matchingEntities = listEntities({ search: query, limit: 10 }, d);
1739
- const exactMatch = getEntityByName(query, undefined, undefined, d);
1740
- if (exactMatch && !matchingEntities.find((e) => e.id === exactMatch.id)) {
1741
- matchingEntities.push(exactMatch);
1742
- }
1743
- for (const entity of matchingEntities) {
1744
- const memories = getMemoriesForEntity(entity.id, d);
1745
- for (const mem of memories) {
1746
- boostedIds.add(mem.id);
1747
- }
1748
- }
1749
- } catch {}
1750
- return boostedIds;
1751
- }
1752
- function computeRecencyBoost(memory) {
1753
- if (memory.pinned)
1754
- return 1;
1755
- const mostRecent = memory.accessed_at || memory.updated_at;
1756
- if (!mostRecent)
1757
- return 0;
1758
- const daysSinceAccess = (Date.now() - Date.parse(mostRecent)) / (1000 * 60 * 60 * 24);
1759
- return Math.max(0, 1 - daysSinceAccess / 30);
1760
- }
1761
- function scoreResults(rows, queryLower, graphBoostedIds) {
1762
- const scored = [];
1763
- for (const row of rows) {
1764
- const memory = parseMemoryRow2(row);
1765
- const rawScore = computeScore(memory, queryLower);
1766
- if (rawScore === 0)
1767
- continue;
1768
- const weightedScore = rawScore * memory.importance / 10;
1769
- const recencyBoost = computeRecencyBoost(memory);
1770
- const accessBoost = Math.min(memory.access_count / 20, 0.2);
1771
- const graphBoost = graphBoostedIds?.has(memory.id) ? 2 : 0;
1772
- const finalScore = (weightedScore + graphBoost) * (1 + recencyBoost * 0.3) * (1 + accessBoost);
1773
- const matchType = determineMatchType(memory, queryLower);
1774
- scored.push({
1775
- memory,
1776
- score: finalScore,
1777
- match_type: matchType,
1778
- highlights: extractHighlights(memory, queryLower)
1779
- });
1780
- }
1781
- scored.sort((a, b) => {
1782
- if (b.score !== a.score)
1783
- return b.score - a.score;
1784
- return b.memory.importance - a.memory.importance;
1785
- });
1786
- return scored;
1787
- }
1788
- function searchMemories(query, filter, db) {
1789
- const d = db || getDatabase();
1790
- query = preprocessQuery(query);
1791
- if (!query)
1792
- return [];
1793
- const queryLower = query.toLowerCase();
1794
- const graphBoostedIds = getGraphBoostedMemoryIds(query, d);
1795
- let scored;
1796
- if (hasFts5Table(d)) {
1797
- const ftsResult = searchWithFts5(d, query, queryLower, filter, graphBoostedIds);
1798
- if (ftsResult !== null) {
1799
- scored = ftsResult;
1800
- } else {
1801
- scored = searchWithLike(d, query, queryLower, filter, graphBoostedIds);
1625
+ scored = searchWithLike(d, query, queryLower, filter, graphBoostedIds);
1802
1626
  }
1803
1627
  } else {
1804
1628
  scored = searchWithLike(d, query, queryLower, filter, graphBoostedIds);
@@ -1837,6 +1661,103 @@ function logSearchQuery(query, resultCount, agentId, projectId, db) {
1837
1661
  d.run("INSERT INTO search_history (id, query, result_count, agent_id, project_id) VALUES (?, ?, ?, ?, ?)", [id, query, resultCount, agentId || null, projectId || null]);
1838
1662
  } catch {}
1839
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
+ });
1840
1761
 
1841
1762
  // src/db/relations.ts
1842
1763
  function parseRelationRow(row) {
@@ -1874,7 +1795,15 @@ function createRelation(input, db) {
1874
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]);
1875
1796
  const row = d.query(`SELECT * FROM relations
1876
1797
  WHERE source_entity_id = ? AND target_entity_id = ? AND relation_type = ?`).get(input.source_entity_id, input.target_entity_id, input.relation_type);
1877
- return parseRelationRow(row);
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;
1878
1807
  }
1879
1808
  function getRelation(id, db) {
1880
1809
  const d = db || getDatabase();
@@ -1961,732 +1890,1318 @@ function findPath(fromEntityId, toEntityId, maxDepth = 5, db) {
1961
1890
  }
1962
1891
  return entities.length > 0 ? entities : null;
1963
1892
  }
1893
+ var init_relations = __esm(() => {
1894
+ init_database();
1895
+ init_hooks();
1896
+ });
1964
1897
 
1965
- // src/lib/duration.ts
1966
- var UNIT_MS = {
1967
- s: 1000,
1968
- m: 60000,
1969
- h: 3600000,
1970
- d: 86400000,
1971
- w: 604800000
1972
- };
1973
- var DURATION_RE = /^(\d+[smhdw])+$/;
1974
- var SEGMENT_RE = /(\d+)([smhdw])/g;
1975
- function parseDuration(input) {
1976
- if (typeof input === "number")
1977
- return input;
1978
- const trimmed = input.trim();
1979
- if (trimmed === "")
1980
- throw new Error("Invalid duration: empty string");
1981
- if (/^\d+$/.test(trimmed)) {
1982
- return parseInt(trimmed, 10);
1898
+ // src/lib/providers/base.ts
1899
+ class BaseProvider {
1900
+ config;
1901
+ constructor(config) {
1902
+ this.config = config;
1983
1903
  }
1984
- if (!DURATION_RE.test(trimmed)) {
1985
- throw new Error(`Invalid duration format: "${trimmed}". Use combinations of Ns, Nm, Nh, Nd, Nw (e.g. "1d12h", "30m") or plain milliseconds.`);
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
+ }
1986
1911
  }
1987
- let total = 0;
1988
- let match;
1989
- SEGMENT_RE.lastIndex = 0;
1990
- while ((match = SEGMENT_RE.exec(trimmed)) !== null) {
1991
- const value = parseInt(match[1], 10);
1992
- const unit = match[2];
1993
- total += value * UNIT_MS[unit];
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)));
1917
+ }
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
+ };
1939
+ }
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 [];
2020
+ }
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;
2038
+ }
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;
2052
+ }
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);
2082
+ }
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 [];
2107
+ }
2108
+ }
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;
2325
+ }
2326
+ }
2327
+ }
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
+ }
2354
+ });
2355
+ const primary = providerRegistry.getConfig().provider;
2356
+ const fallback = available.filter((p) => p !== primary);
2357
+ providerRegistry.configure({ fallback });
2358
+ }
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;
1994
2420
  }
1995
- if (total === 0) {
1996
- throw new Error(`Invalid duration: "${trimmed}" resolves to 0ms`);
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
+ }
1997
2437
  }
1998
- return total;
1999
2438
  }
2000
- var FORMAT_UNITS = [
2001
- ["w", UNIT_MS["w"]],
2002
- ["d", UNIT_MS["d"]],
2003
- ["h", UNIT_MS["h"]],
2004
- ["m", UNIT_MS["m"]],
2005
- ["s", UNIT_MS["s"]]
2006
- ];
2007
-
2008
- // src/lib/providers/base.ts
2009
- var DEFAULT_AUTO_MEMORY_CONFIG = {
2010
- provider: "anthropic",
2011
- model: "claude-haiku-4-5",
2012
- enabled: true,
2013
- minImportance: 4,
2014
- autoEntityLink: true,
2015
- fallback: ["cerebras", "openai"]
2016
- };
2439
+ var MAX_QUEUE_SIZE = 100, CONCURRENCY = 3, autoMemoryQueue;
2440
+ var init_auto_memory_queue = __esm(() => {
2441
+ autoMemoryQueue = new AutoMemoryQueue;
2442
+ });
2017
2443
 
2018
- class BaseProvider {
2019
- config;
2020
- constructor(config) {
2021
- this.config = config;
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) {
2452
+ try {
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;
2475
+ } catch {
2476
+ return false;
2022
2477
  }
2023
- parseJSON(raw) {
2024
- try {
2025
- const cleaned = raw.replace(/^```(?:json)?\s*/m, "").replace(/\s*```$/m, "").trim();
2026
- return JSON.parse(cleaned);
2027
- } catch {
2028
- return null;
2478
+ }
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 {}
2499
+ }
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 {}
2029
2512
  }
2513
+ } catch (err) {
2514
+ console.error("[auto-memory] entity linking failed:", err);
2030
2515
  }
2031
- clampImportance(value) {
2032
- const n = Number(value);
2033
- if (isNaN(n))
2034
- return 5;
2035
- return Math.max(0, Math.min(10, Math.round(n)));
2516
+ }
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())
2522
+ return null;
2523
+ if (isDuplicate(extracted.content, context.agentId, context.projectId)) {
2524
+ return null;
2036
2525
  }
2037
- normaliseMemory(raw) {
2038
- if (!raw || typeof raw !== "object")
2039
- return null;
2040
- const m = raw;
2041
- if (typeof m.content !== "string" || !m.content.trim())
2042
- return null;
2043
- const validScopes = ["private", "shared", "global"];
2044
- const validCategories = [
2045
- "preference",
2046
- "fact",
2047
- "knowledge",
2048
- "history"
2049
- ];
2050
- return {
2051
- content: m.content.trim(),
2052
- category: validCategories.includes(m.category) ? m.category : "knowledge",
2053
- importance: this.clampImportance(m.importance),
2054
- tags: Array.isArray(m.tags) ? m.tags.filter((t) => typeof t === "string").map((t) => t.toLowerCase()) : [],
2055
- suggestedScope: validScopes.includes(m.suggestedScope) ? m.suggestedScope : "shared",
2056
- reasoning: typeof m.reasoning === "string" ? m.reasoning : undefined
2526
+ try {
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
+ }
2057
2546
  };
2547
+ const memory = createMemory(input, "merge");
2548
+ return memory.id;
2549
+ } catch (err) {
2550
+ console.error("[auto-memory] saveExtractedMemory failed:", err);
2551
+ return null;
2058
2552
  }
2059
2553
  }
2060
- var MEMORY_EXTRACTION_SYSTEM_PROMPT = `You are a precise memory extraction engine for an AI agent.
2061
- Given text, extract facts worth remembering as structured JSON.
2062
- Focus on: decisions made, preferences revealed, corrections, architectural choices, established facts, user preferences.
2063
- Ignore: greetings, filler, questions without answers, temporary states.
2064
- Output ONLY a JSON array \u2014 no markdown, no explanation.`;
2065
- var MEMORY_EXTRACTION_USER_TEMPLATE = (text, context) => `Extract memories from this text.
2066
- ${context.projectName ? `Project: ${context.projectName}` : ""}
2067
- ${context.existingMemoriesSummary ? `Existing memories (avoid duplicates):
2068
- ${context.existingMemoriesSummary}` : ""}
2069
-
2070
- Text:
2071
- ${text}
2072
-
2073
- Return a JSON array of objects with these exact fields:
2074
- - content: string (the memory, concise and specific)
2075
- - category: "preference" | "fact" | "knowledge" | "history"
2076
- - importance: number 0-10 (10 = critical, 0 = trivial)
2077
- - tags: string[] (lowercase keywords)
2078
- - suggestedScope: "private" | "shared" | "global"
2079
- - reasoning: string (one sentence why this is worth remembering)
2080
-
2081
- Return [] if nothing is worth remembering.`;
2082
- var ENTITY_EXTRACTION_SYSTEM_PROMPT = `You are a knowledge graph entity extractor.
2083
- Given text, identify named entities and their relationships.
2084
- Output ONLY valid JSON \u2014 no markdown, no explanation.`;
2085
- var ENTITY_EXTRACTION_USER_TEMPLATE = (text) => `Extract entities and relations from this text.
2086
-
2087
- Text: ${text}
2088
-
2089
- Return JSON with this exact shape:
2090
- {
2091
- "entities": [
2092
- { "name": string, "type": "person"|"project"|"tool"|"concept"|"file"|"api"|"pattern"|"organization", "confidence": 0-1 }
2093
- ],
2094
- "relations": [
2095
- { "from": string, "to": string, "type": "uses"|"knows"|"depends_on"|"created_by"|"related_to"|"contradicts"|"part_of"|"implements" }
2096
- ]
2097
- }`;
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
+ }
2579
+ }
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
+ }
2589
+ }
2590
+ }
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
+ });
2098
2618
 
2099
- // src/lib/providers/anthropic.ts
2100
- var ANTHROPIC_MODELS = {
2101
- default: "claude-haiku-4-5",
2102
- premium: "claude-sonnet-4-5"
2103
- };
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";
2104
2624
 
2105
- class AnthropicProvider extends BaseProvider {
2106
- name = "anthropic";
2107
- baseUrl = "https://api.anthropic.com/v1";
2108
- constructor(config) {
2109
- const apiKey = config?.apiKey ?? process.env.ANTHROPIC_API_KEY ?? "";
2110
- super({
2111
- apiKey,
2112
- model: config?.model ?? ANTHROPIC_MODELS.default,
2113
- maxTokens: config?.maxTokens ?? 1024,
2114
- temperature: config?.temperature ?? 0,
2115
- timeoutMs: config?.timeoutMs ?? 15000
2116
- });
2117
- }
2118
- async extractMemories(text, context) {
2119
- if (!this.config.apiKey)
2120
- return [];
2121
- try {
2122
- const response = await this.callAPI(MEMORY_EXTRACTION_SYSTEM_PROMPT, MEMORY_EXTRACTION_USER_TEMPLATE(text, context));
2123
- const parsed = this.parseJSON(response);
2124
- if (!Array.isArray(parsed))
2125
- return [];
2126
- return parsed.map((item) => this.normaliseMemory(item)).filter((m) => m !== null);
2127
- } catch (err) {
2128
- console.error("[anthropic] extractMemories failed:", err);
2129
- return [];
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
+ }
2130
2663
  }
2131
- }
2132
- async extractEntities(text) {
2133
- const empty = { entities: [], relations: [] };
2134
- if (!this.config.apiKey)
2135
- return empty;
2136
- try {
2137
- const response = await this.callAPI(ENTITY_EXTRACTION_SYSTEM_PROMPT, ENTITY_EXTRACTION_USER_TEMPLATE(text));
2138
- const parsed = this.parseJSON(response);
2139
- if (!parsed || typeof parsed !== "object")
2140
- return empty;
2141
- return {
2142
- entities: Array.isArray(parsed.entities) ? parsed.entities : [],
2143
- relations: Array.isArray(parsed.relations) ? parsed.relations : []
2144
- };
2145
- } catch (err) {
2146
- console.error("[anthropic] extractEntities failed:", err);
2147
- return empty;
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]);
2148
2671
  }
2149
- }
2150
- async scoreImportance(content, _context) {
2151
- if (!this.config.apiKey)
2152
- return 5;
2153
- try {
2154
- const response = await this.callAPI("You are an importance scorer. Return only a single integer 0-10. No explanation.", `How important is this memory for an AI agent to retain long-term?
2155
-
2156
- "${content}"
2157
-
2158
- Return only a number 0-10.`);
2159
- return this.clampImportance(response.trim());
2160
- } catch {
2161
- return 5;
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]);
2162
2677
  }
2678
+ return getAgent(existingId, d);
2163
2679
  }
2164
- async callAPI(systemPrompt, userMessage) {
2165
- const controller = new AbortController;
2166
- const timeout = setTimeout(() => controller.abort(), this.config.timeoutMs ?? 15000);
2167
- try {
2168
- const res = await fetch(`${this.baseUrl}/messages`, {
2169
- method: "POST",
2170
- headers: {
2171
- "Content-Type": "application/json",
2172
- "x-api-key": this.config.apiKey,
2173
- "anthropic-version": "2023-06-01"
2174
- },
2175
- body: JSON.stringify({
2176
- model: this.config.model,
2177
- max_tokens: this.config.maxTokens ?? 1024,
2178
- temperature: this.config.temperature ?? 0,
2179
- system: systemPrompt,
2180
- messages: [{ role: "user", content: userMessage }]
2181
- }),
2182
- signal: controller.signal
2183
- });
2184
- if (!res.ok) {
2185
- const body = await res.text().catch(() => "");
2186
- throw new Error(`Anthropic API ${res.status}: ${body.slice(0, 200)}`);
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);
2683
+ }
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}`);
2187
2719
  }
2188
- const data = await res.json();
2189
- return data.content?.[0]?.text ?? "";
2190
- } finally {
2191
- clearTimeout(timeout);
2720
+ d.run("UPDATE agents SET name = ? WHERE id = ?", [normalizedNewName, agent.id]);
2192
2721
  }
2193
2722
  }
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);
2194
2737
  }
2195
2738
 
2196
- // src/lib/providers/openai-compat.ts
2197
- class OpenAICompatProvider extends BaseProvider {
2198
- constructor(config) {
2199
- super(config);
2200
- }
2201
- async extractMemories(text, context) {
2202
- if (!this.config.apiKey)
2203
- return [];
2204
- try {
2205
- const response = await this.callWithRetry(MEMORY_EXTRACTION_SYSTEM_PROMPT, MEMORY_EXTRACTION_USER_TEMPLATE(text, context));
2206
- const parsed = this.parseJSON(response);
2207
- if (!Array.isArray(parsed))
2208
- return [];
2209
- return parsed.map((item) => this.normaliseMemory(item)).filter((m) => m !== null);
2210
- } catch (err) {
2211
- console.error(`[${this.name}] extractMemories failed:`, err);
2212
- return [];
2213
- }
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 });
2214
2763
  }
2215
- async extractEntities(text) {
2216
- const empty = { entities: [], relations: [] };
2217
- if (!this.config.apiKey)
2218
- return empty;
2219
- try {
2220
- const response = await this.callWithRetry(ENTITY_EXTRACTION_SYSTEM_PROMPT, ENTITY_EXTRACTION_USER_TEMPLATE(text));
2221
- const parsed = this.parseJSON(response);
2222
- if (!parsed || typeof parsed !== "object")
2223
- return empty;
2224
- return {
2225
- entities: Array.isArray(parsed.entities) ? parsed.entities : [],
2226
- relations: Array.isArray(parsed.relations) ? parsed.relations : []
2227
- };
2228
- } catch (err) {
2229
- console.error(`[${this.name}] extractEntities failed:`, err);
2230
- return empty;
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;
2231
2768
  }
2232
2769
  }
2233
- async scoreImportance(content, _context) {
2234
- if (!this.config.apiKey)
2235
- return 5;
2236
- try {
2237
- const response = await this.callWithRetry("You are an importance scorer. Return only a single integer 0-10. No explanation.", `How important is this memory for an AI agent to retain long-term?
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
+ }
2238
2812
 
2239
- "${content}"
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
+ }
2240
2860
 
2241
- Return only a number 0-10.`);
2242
- return this.clampImportance(response.trim());
2243
- } catch {
2244
- return 5;
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;
2245
2871
  }
2246
- }
2247
- async callWithRetry(systemPrompt, userMessage, retries = 3) {
2248
- let lastError = null;
2249
- for (let attempt = 0;attempt < retries; attempt++) {
2250
- try {
2251
- return await this.callAPI(systemPrompt, userMessage);
2252
- } catch (err) {
2253
- lastError = err instanceof Error ? err : new Error(String(err));
2254
- const isRateLimit = lastError.message.includes("429") || lastError.message.toLowerCase().includes("rate limit");
2255
- if (!isRateLimit || attempt === retries - 1)
2256
- throw lastError;
2257
- await new Promise((r) => setTimeout(r, 1000 * Math.pow(2, attempt)));
2258
- }
2872
+ const parent = dirname2(dir);
2873
+ if (parent === dir) {
2874
+ return null;
2259
2875
  }
2260
- throw lastError ?? new Error("Unknown error");
2876
+ dir = parent;
2261
2877
  }
2262
- async callAPI(systemPrompt, userMessage) {
2263
- const controller = new AbortController;
2264
- const timeout = setTimeout(() => controller.abort(), this.config.timeoutMs ?? 15000);
2265
- try {
2266
- const res = await fetch(`${this.baseUrl}/chat/completions`, {
2267
- method: "POST",
2268
- headers: {
2269
- "Content-Type": "application/json",
2270
- [this.authHeader]: `Bearer ${this.config.apiKey}`
2271
- },
2272
- body: JSON.stringify({
2273
- model: this.config.model,
2274
- max_tokens: this.config.maxTokens ?? 1024,
2275
- temperature: this.config.temperature ?? 0,
2276
- messages: [
2277
- { role: "system", content: systemPrompt },
2278
- { role: "user", content: userMessage }
2279
- ]
2280
- }),
2281
- signal: controller.signal
2282
- });
2283
- if (!res.ok) {
2284
- const body = await res.text().catch(() => "");
2285
- throw new Error(`${this.name} API ${res.status}: ${body.slice(0, 200)}`);
2286
- }
2287
- const data = await res.json();
2288
- return data.choices?.[0]?.message?.content ?? "";
2289
- } finally {
2290
- clearTimeout(timeout);
2878
+ }
2879
+ function findGitRoot2() {
2880
+ let dir = process.cwd();
2881
+ while (true) {
2882
+ if (existsSync2(join2(dir, ".git"))) {
2883
+ return dir;
2884
+ }
2885
+ const parent = dirname2(dir);
2886
+ if (parent === dir) {
2887
+ return null;
2291
2888
  }
2889
+ dir = parent;
2292
2890
  }
2293
2891
  }
2294
-
2295
- // src/lib/providers/openai.ts
2296
- var OPENAI_MODELS = {
2297
- default: "gpt-4.1-nano",
2298
- mini: "gpt-4.1-mini",
2299
- full: "gpt-4.1"
2300
- };
2301
-
2302
- class OpenAIProvider extends OpenAICompatProvider {
2303
- name = "openai";
2304
- baseUrl = "https://api.openai.com/v1";
2305
- authHeader = "Authorization";
2306
- constructor(config) {
2307
- super({
2308
- apiKey: config?.apiKey ?? process.env.OPENAI_API_KEY ?? "",
2309
- model: config?.model ?? OPENAI_MODELS.default,
2310
- maxTokens: config?.maxTokens ?? 1024,
2311
- temperature: config?.temperature ?? 0,
2312
- timeoutMs: config?.timeoutMs ?? 15000
2313
- });
2314
- }
2892
+ function profilesDir() {
2893
+ return join2(homedir(), ".mementos", "profiles");
2315
2894
  }
2316
-
2317
- // src/lib/providers/cerebras.ts
2318
- var CEREBRAS_MODELS = {
2319
- default: "llama-3.3-70b",
2320
- fast: "llama3.1-8b"
2321
- };
2322
-
2323
- class CerebrasProvider extends OpenAICompatProvider {
2324
- name = "cerebras";
2325
- baseUrl = "https://api.cerebras.ai/v1";
2326
- authHeader = "Authorization";
2327
- constructor(config) {
2328
- super({
2329
- apiKey: config?.apiKey ?? process.env.CEREBRAS_API_KEY ?? "",
2330
- model: config?.model ?? CEREBRAS_MODELS.default,
2331
- maxTokens: config?.maxTokens ?? 1024,
2332
- temperature: config?.temperature ?? 0,
2333
- timeoutMs: config?.timeoutMs ?? 1e4
2334
- });
2335
- }
2895
+ function globalConfigPath() {
2896
+ return join2(homedir(), ".mementos", "config.json");
2336
2897
  }
2337
-
2338
- // src/lib/providers/grok.ts
2339
- var GROK_MODELS = {
2340
- default: "grok-3-mini",
2341
- premium: "grok-3"
2342
- };
2343
-
2344
- class GrokProvider extends OpenAICompatProvider {
2345
- name = "grok";
2346
- baseUrl = "https://api.x.ai/v1";
2347
- authHeader = "Authorization";
2348
- constructor(config) {
2349
- super({
2350
- apiKey: config?.apiKey ?? process.env.XAI_API_KEY ?? "",
2351
- model: config?.model ?? GROK_MODELS.default,
2352
- maxTokens: config?.maxTokens ?? 1024,
2353
- temperature: config?.temperature ?? 0,
2354
- timeoutMs: config?.timeoutMs ?? 15000
2355
- });
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 {};
2356
2906
  }
2357
2907
  }
2358
-
2359
- // src/lib/providers/registry.ts
2360
- class ProviderRegistry {
2361
- config = { ...DEFAULT_AUTO_MEMORY_CONFIG };
2362
- _instances = new Map;
2363
- configure(partial) {
2364
- this.config = { ...this.config, ...partial };
2365
- this._instances.clear();
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))
2918
+ return [];
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;
2366
2927
  }
2367
- getConfig() {
2368
- return this.config;
2928
+ const profile = getActiveProfile();
2929
+ if (profile) {
2930
+ const profilePath = join2(profilesDir(), `${profile}.db`);
2931
+ ensureDir2(dirname2(profilePath));
2932
+ return profilePath;
2369
2933
  }
2370
- getPrimary() {
2371
- return this.getProvider(this.config.provider);
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;
2941
+ }
2372
2942
  }
2373
- getFallbacks() {
2374
- const fallbackNames = this.config.fallback ?? [];
2375
- return fallbackNames.filter((n) => n !== this.config.provider).map((n) => this.getProvider(n)).filter((p) => p !== null);
2943
+ const found = findFileWalkingUp(join2(".mementos", "mementos.db"));
2944
+ if (found) {
2945
+ return found;
2376
2946
  }
2377
- getAvailable() {
2378
- const primary = this.getPrimary();
2379
- if (primary)
2380
- return primary;
2381
- const fallbacks = this.getFallbacks();
2382
- return fallbacks[0] ?? null;
2947
+ const fallback = join2(homedir(), ".mementos", "mementos.db");
2948
+ ensureDir2(dirname2(fallback));
2949
+ return fallback;
2950
+ }
2951
+ function ensureDir2(dir) {
2952
+ if (!existsSync2(dir)) {
2953
+ mkdirSync2(dir, { recursive: true });
2383
2954
  }
2384
- getProvider(name) {
2385
- const cached = this._instances.get(name);
2386
- if (cached)
2387
- return cached;
2388
- const provider = this.createProvider(name);
2389
- if (!provider)
2390
- return null;
2391
- if (!provider.config.apiKey)
2392
- return null;
2393
- this._instances.set(name, provider);
2394
- return provider;
2955
+ }
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
+
2965
+ // src/lib/duration.ts
2966
+ var UNIT_MS = {
2967
+ s: 1000,
2968
+ m: 60000,
2969
+ h: 3600000,
2970
+ d: 86400000,
2971
+ w: 604800000
2972
+ };
2973
+ var DURATION_RE = /^(\d+[smhdw])+$/;
2974
+ var SEGMENT_RE = /(\d+)([smhdw])/g;
2975
+ function parseDuration(input) {
2976
+ if (typeof input === "number")
2977
+ return input;
2978
+ const trimmed = input.trim();
2979
+ if (trimmed === "")
2980
+ throw new Error("Invalid duration: empty string");
2981
+ if (/^\d+$/.test(trimmed)) {
2982
+ return parseInt(trimmed, 10);
2395
2983
  }
2396
- health() {
2397
- const providers = ["anthropic", "openai", "cerebras", "grok"];
2398
- const result = {};
2399
- for (const name of providers) {
2400
- const p = this.createProvider(name);
2401
- result[name] = {
2402
- available: Boolean(p?.config.apiKey),
2403
- model: p?.config.model ?? "unknown"
2404
- };
2405
- }
2406
- return result;
2984
+ if (!DURATION_RE.test(trimmed)) {
2985
+ throw new Error(`Invalid duration format: "${trimmed}". Use combinations of Ns, Nm, Nh, Nd, Nw (e.g. "1d12h", "30m") or plain milliseconds.`);
2407
2986
  }
2408
- createProvider(name) {
2409
- const modelOverride = name === this.config.provider ? this.config.model : undefined;
2410
- switch (name) {
2411
- case "anthropic":
2412
- return new AnthropicProvider(modelOverride ? { model: modelOverride } : undefined);
2413
- case "openai":
2414
- return new OpenAIProvider(modelOverride ? { model: modelOverride } : undefined);
2415
- case "cerebras":
2416
- return new CerebrasProvider(modelOverride ? { model: modelOverride } : undefined);
2417
- case "grok":
2418
- return new GrokProvider(modelOverride ? { model: modelOverride } : undefined);
2419
- default:
2420
- return null;
2421
- }
2987
+ let total = 0;
2988
+ let match;
2989
+ SEGMENT_RE.lastIndex = 0;
2990
+ while ((match = SEGMENT_RE.exec(trimmed)) !== null) {
2991
+ const value = parseInt(match[1], 10);
2992
+ const unit = match[2];
2993
+ total += value * UNIT_MS[unit];
2422
2994
  }
2423
- }
2424
- var providerRegistry = new ProviderRegistry;
2425
- function autoConfigureFromEnv() {
2426
- const hasAnthropicKey = Boolean(process.env.ANTHROPIC_API_KEY);
2427
- const hasCerebrasKey = Boolean(process.env.CEREBRAS_API_KEY);
2428
- const hasOpenAIKey = Boolean(process.env.OPENAI_API_KEY);
2429
- const hasGrokKey = Boolean(process.env.XAI_API_KEY);
2430
- if (!hasAnthropicKey) {
2431
- if (hasCerebrasKey) {
2432
- providerRegistry.configure({ provider: "cerebras" });
2433
- } else if (hasOpenAIKey) {
2434
- providerRegistry.configure({ provider: "openai" });
2435
- } else if (hasGrokKey) {
2436
- providerRegistry.configure({ provider: "grok" });
2437
- }
2995
+ if (total === 0) {
2996
+ throw new Error(`Invalid duration: "${trimmed}" resolves to 0ms`);
2438
2997
  }
2439
- const allProviders = ["anthropic", "cerebras", "openai", "grok"];
2440
- const available = allProviders.filter((p) => {
2441
- switch (p) {
2442
- case "anthropic":
2443
- return hasAnthropicKey;
2444
- case "cerebras":
2445
- return hasCerebrasKey;
2446
- case "openai":
2447
- return hasOpenAIKey;
2448
- case "grok":
2449
- return hasGrokKey;
2450
- }
2451
- });
2452
- const primary = providerRegistry.getConfig().provider;
2453
- const fallback = available.filter((p) => p !== primary);
2454
- providerRegistry.configure({ fallback });
2998
+ return total;
2455
2999
  }
2456
- autoConfigureFromEnv();
3000
+ var FORMAT_UNITS = [
3001
+ ["w", UNIT_MS["w"]],
3002
+ ["d", UNIT_MS["d"]],
3003
+ ["h", UNIT_MS["h"]],
3004
+ ["m", UNIT_MS["m"]],
3005
+ ["s", UNIT_MS["s"]]
3006
+ ];
2457
3007
 
2458
- // src/lib/auto-memory-queue.ts
2459
- var MAX_QUEUE_SIZE = 100;
2460
- var CONCURRENCY = 3;
3008
+ // src/server/index.ts
3009
+ init_auto_memory();
3010
+ init_registry();
3011
+ init_hooks();
2461
3012
 
2462
- class AutoMemoryQueue {
2463
- queue = [];
2464
- handler = null;
2465
- running = false;
2466
- activeCount = 0;
2467
- stats = {
2468
- pending: 0,
2469
- processing: 0,
2470
- processed: 0,
2471
- failed: 0,
2472
- dropped: 0
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"]
2473
3032
  };
2474
- setHandler(handler) {
2475
- this.handler = handler;
2476
- if (!this.running)
2477
- this.startLoop();
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);
2478
3065
  }
2479
- enqueue(job) {
2480
- if (this.queue.length >= MAX_QUEUE_SIZE) {
2481
- this.queue.shift();
2482
- this.stats.dropped++;
2483
- this.stats.pending = Math.max(0, this.stats.pending - 1);
2484
- }
2485
- this.queue.push(job);
2486
- this.stats.pending++;
2487
- if (!this.running && this.handler)
2488
- this.startLoop();
3066
+ if (filter.enabled !== undefined) {
3067
+ conditions.push("enabled = ?");
3068
+ params.push(filter.enabled ? 1 : 0);
2489
3069
  }
2490
- getStats() {
2491
- return { ...this.stats, pending: this.queue.length };
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);
2492
3084
  }
2493
- startLoop() {
2494
- this.running = true;
2495
- this.loop();
3085
+ if (updates.description !== undefined) {
3086
+ sets.push("description = ?");
3087
+ params.push(updates.description);
2496
3088
  }
2497
- async loop() {
2498
- while (this.queue.length > 0 || this.activeCount > 0) {
2499
- while (this.queue.length > 0 && this.activeCount < CONCURRENCY) {
2500
- const job = this.queue.shift();
2501
- if (!job)
2502
- break;
2503
- this.stats.pending = Math.max(0, this.stats.pending - 1);
2504
- this.activeCount++;
2505
- this.stats.processing = this.activeCount;
2506
- this.processJob(job);
2507
- }
2508
- await new Promise((r) => setImmediate(r));
2509
- }
2510
- this.running = false;
3089
+ if (updates.priority !== undefined) {
3090
+ sets.push("priority = ?");
3091
+ params.push(updates.priority);
2511
3092
  }
2512
- async processJob(job) {
2513
- if (!this.handler) {
2514
- this.activeCount--;
2515
- this.stats.processing = this.activeCount;
2516
- return;
2517
- }
2518
- try {
2519
- await this.handler(job);
2520
- this.stats.processed++;
2521
- } catch (err) {
2522
- this.stats.failed++;
2523
- console.error("[auto-memory-queue] job failed:", err);
2524
- } finally {
2525
- this.activeCount--;
2526
- this.stats.processing = this.activeCount;
2527
- }
3093
+ if (sets.length > 0) {
3094
+ params.push(id);
3095
+ d.run(`UPDATE webhook_hooks SET ${sets.join(", ")} WHERE id = ?`, params);
2528
3096
  }
3097
+ return getWebhookHook(id, d);
2529
3098
  }
2530
- var autoMemoryQueue = new AutoMemoryQueue;
2531
-
2532
- // src/lib/auto-memory.ts
2533
- var DEDUP_SIMILARITY_THRESHOLD = 0.85;
2534
- function isDuplicate(content, agentId, projectId) {
2535
- try {
2536
- const query = content.split(/\s+/).filter((w) => w.length > 3).slice(0, 10).join(" ");
2537
- if (!query)
2538
- return false;
2539
- const results = searchMemories(query, {
2540
- agent_id: agentId,
2541
- project_id: projectId,
2542
- limit: 3
2543
- });
2544
- if (results.length === 0)
2545
- return false;
2546
- const contentWords = new Set(content.toLowerCase().split(/\W+/).filter((w) => w.length > 3));
2547
- for (const result of results) {
2548
- const existingWords = new Set(result.memory.value.toLowerCase().split(/\W+/).filter((w) => w.length > 3));
2549
- if (contentWords.size === 0 || existingWords.size === 0)
2550
- continue;
2551
- const intersection = [...contentWords].filter((w) => existingWords.has(w)).length;
2552
- const union = new Set([...contentWords, ...existingWords]).size;
2553
- const similarity = intersection / union;
2554
- if (similarity >= DEDUP_SIMILARITY_THRESHOLD)
2555
- return true;
2556
- }
2557
- return false;
2558
- } catch {
2559
- return false;
2560
- }
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;
2561
3103
  }
2562
- async function linkEntitiesToMemory(memoryId, content, _agentId, projectId) {
2563
- const provider = providerRegistry.getAvailable();
2564
- if (!provider)
2565
- return;
2566
- try {
2567
- const { entities, relations } = await provider.extractEntities(content);
2568
- const entityIdMap = new Map;
2569
- for (const extracted of entities) {
2570
- if (extracted.confidence < 0.6)
2571
- continue;
2572
- try {
2573
- const existing = getEntityByName(extracted.name);
2574
- const entityId = existing ? existing.id : createEntity({
2575
- name: extracted.name,
2576
- type: extracted.type,
2577
- project_id: projectId
2578
- }).id;
2579
- entityIdMap.set(extracted.name, entityId);
2580
- linkEntityToMemory(entityId, memoryId, "subject");
2581
- } catch {}
2582
- }
2583
- for (const rel of relations) {
2584
- const fromId = entityIdMap.get(rel.from);
2585
- const toId = entityIdMap.get(rel.to);
2586
- if (!fromId || !toId)
2587
- continue;
2588
- try {
2589
- createRelation({
2590
- source_entity_id: fromId,
2591
- target_entity_id: toId,
2592
- relation_type: rel.type
2593
- });
2594
- } catch {}
2595
- }
2596
- } catch (err) {
2597
- console.error("[auto-memory] entity linking failed:", err);
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]);
2598
3110
  }
2599
3111
  }
2600
- async function saveExtractedMemory(extracted, context) {
2601
- const minImportance = providerRegistry.getConfig().minImportance;
2602
- if (extracted.importance < minImportance)
2603
- return null;
2604
- if (!extracted.content.trim())
2605
- return null;
2606
- if (isDuplicate(extracted.content, context.agentId, context.projectId)) {
2607
- return null;
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
+ });
2608
3137
  }
2609
- try {
2610
- const input = {
2611
- key: extracted.content.slice(0, 120).replace(/\s+/g, "-").toLowerCase(),
2612
- value: extracted.content,
2613
- category: extracted.category,
2614
- scope: extracted.suggestedScope,
2615
- importance: extracted.importance,
2616
- tags: [
2617
- ...extracted.tags,
2618
- "auto-extracted",
2619
- ...context.sessionId ? [`session:${context.sessionId}`] : []
2620
- ],
2621
- agent_id: context.agentId,
2622
- project_id: context.projectId,
2623
- session_id: context.sessionId,
2624
- metadata: {
2625
- reasoning: extracted.reasoning,
2626
- auto_extracted: true,
2627
- extracted_at: new Date().toISOString()
2628
- }
2629
- };
2630
- const memory = createMemory(input, "merge");
2631
- return memory.id;
2632
- } catch (err) {
2633
- console.error("[auto-memory] saveExtractedMemory failed:", err);
2634
- return null;
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 {}
2635
3160
  }
2636
- }
2637
- async function processJob(job) {
2638
- if (!providerRegistry.getConfig().enabled)
2639
- return;
2640
- const provider = providerRegistry.getAvailable();
2641
- if (!provider)
3161
+ });
3162
+ var _webhooksLoaded = false;
3163
+ function loadWebhooksFromDb() {
3164
+ if (_webhooksLoaded)
2642
3165
  return;
2643
- const context = {
2644
- agentId: job.agentId,
2645
- projectId: job.projectId,
2646
- sessionId: job.sessionId
2647
- };
2648
- let extracted = [];
3166
+ _webhooksLoaded = true;
2649
3167
  try {
2650
- extracted = await provider.extractMemories(job.turn, context);
2651
- } catch {
2652
- const fallbacks = providerRegistry.getFallbacks();
2653
- for (const fallback of fallbacks) {
2654
- try {
2655
- extracted = await fallback.extractMemories(job.turn, context);
2656
- if (extracted.length > 0)
2657
- break;
2658
- } catch {
2659
- continue;
2660
- }
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
+ });
2661
3179
  }
2662
- }
2663
- if (extracted.length === 0)
2664
- return;
2665
- for (const memory of extracted) {
2666
- const memoryId = await saveExtractedMemory(memory, context);
2667
- if (!memoryId)
2668
- continue;
2669
- if (providerRegistry.getConfig().autoEntityLink) {
2670
- linkEntitiesToMemory(memoryId, memory.content, job.agentId, job.projectId);
3180
+ if (webhooks.length > 0) {
3181
+ console.log(`[hooks] Loaded ${webhooks.length} webhook(s) from DB`);
2671
3182
  }
3183
+ } catch (err) {
3184
+ console.error("[hooks] Failed to load webhooks from DB:", err);
2672
3185
  }
2673
3186
  }
2674
- autoMemoryQueue.setHandler(processJob);
2675
- function processConversationTurn(turn, context, source = "turn") {
2676
- if (!turn?.trim())
2677
- return;
2678
- autoMemoryQueue.enqueue({
2679
- ...context,
2680
- turn,
2681
- timestamp: Date.now(),
2682
- source
2683
- });
2684
- }
2685
- function getAutoMemoryStats() {
2686
- return autoMemoryQueue.getStats();
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
+ };
2687
3201
  }
2688
- function configureAutoMemory(config) {
2689
- providerRegistry.configure(config);
3202
+ function reloadWebhooks() {
3203
+ _webhooksLoaded = false;
3204
+ loadWebhooksFromDb();
2690
3205
  }
2691
3206
 
2692
3207
  // src/server/index.ts
@@ -3700,7 +4215,73 @@ addRoute("POST", "/api/auto-memory/test", async (req) => {
3700
4215
  note: "DRY RUN \u2014 nothing was saved"
3701
4216
  });
3702
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
+ });
3703
4283
  function startServer(port) {
4284
+ loadWebhooksFromDb();
3704
4285
  const hostname = process.env["MEMENTOS_HOST"] ?? "127.0.0.1";
3705
4286
  Bun.serve({
3706
4287
  port,