@hasna/mementos 0.14.17 → 0.14.18

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.
@@ -10167,6 +10167,35 @@ ALTER TABLE memories ADD COLUMN sequence_group TEXT DEFAULT NULL;
10167
10167
  ALTER TABLE memories ADD COLUMN sequence_order INTEGER DEFAULT NULL;
10168
10168
  CREATE INDEX IF NOT EXISTS idx_memories_sequence_group ON memories(sequence_group) WHERE sequence_group IS NOT NULL;
10169
10169
  INSERT OR IGNORE INTO _migrations (id) VALUES (32);
10170
+ `,
10171
+ `
10172
+ ALTER TABLE machines ADD COLUMN is_primary INTEGER NOT NULL DEFAULT 0;
10173
+ CREATE INDEX IF NOT EXISTS idx_machines_primary ON machines(is_primary);
10174
+ CREATE TRIGGER IF NOT EXISTS machines_single_primary_insert
10175
+ AFTER INSERT ON machines
10176
+ WHEN NEW.is_primary = 1
10177
+ BEGIN
10178
+ UPDATE machines
10179
+ SET is_primary = 0,
10180
+ last_seen_at = COALESCE(NEW.last_seen_at, datetime('now'))
10181
+ WHERE id != NEW.id AND is_primary = 1;
10182
+ END;
10183
+ CREATE TRIGGER IF NOT EXISTS machines_single_primary_update
10184
+ AFTER UPDATE OF is_primary ON machines
10185
+ WHEN NEW.is_primary = 1
10186
+ BEGIN
10187
+ UPDATE machines
10188
+ SET is_primary = 0,
10189
+ last_seen_at = COALESCE(NEW.last_seen_at, datetime('now'))
10190
+ WHERE id != NEW.id AND is_primary = 1;
10191
+ END;
10192
+ CREATE TRIGGER IF NOT EXISTS machines_prevent_delete_primary
10193
+ BEFORE DELETE ON machines
10194
+ WHEN OLD.is_primary = 1
10195
+ BEGIN
10196
+ SELECT RAISE(ABORT, 'Primary machine cannot be deleted');
10197
+ END;
10198
+ INSERT OR IGNORE INTO _migrations (id) VALUES (33);
10170
10199
  `
10171
10200
  ];
10172
10201
  });
@@ -10250,11 +10279,16 @@ function ensureDir2(filePath) {
10250
10279
  }
10251
10280
  }
10252
10281
  function getDatabase(dbPath) {
10253
- if (_db)
10254
- return _db;
10255
10282
  const path = dbPath || getDbPath3();
10283
+ if (_db) {
10284
+ if (_dbPath === path)
10285
+ return _db;
10286
+ _db.close();
10287
+ _db = null;
10288
+ }
10289
+ _dbPath = path;
10256
10290
  ensureDir2(path);
10257
- _db = new SqliteAdapter(path, { create: true });
10291
+ _db = new SqliteAdapter(path);
10258
10292
  _db.run("PRAGMA journal_mode = WAL");
10259
10293
  _db.run("PRAGMA busy_timeout = 5000");
10260
10294
  _db.run("PRAGMA foreign_keys = ON");
@@ -10278,13 +10312,17 @@ function runMigrations(db) {
10278
10312
  for (let i = currentLevel;i < MIGRATIONS.length; i++) {
10279
10313
  try {
10280
10314
  db.exec(MIGRATIONS[i]);
10281
- } catch {}
10315
+ } catch (e) {
10316
+ console.warn(`[mementos] Migration ${i + 1} failed: ${e instanceof Error ? e.message : String(e)}`);
10317
+ }
10282
10318
  }
10283
10319
  } catch {
10284
- for (const migration of MIGRATIONS) {
10320
+ for (let i = 0;i < MIGRATIONS.length; i++) {
10285
10321
  try {
10286
- db.exec(migration);
10287
- } catch {}
10322
+ db.exec(MIGRATIONS[i]);
10323
+ } catch (e) {
10324
+ console.warn(`[mementos] Migration ${i + 1} failed: ${e instanceof Error ? e.message : String(e)}`);
10325
+ }
10288
10326
  }
10289
10327
  }
10290
10328
  }
@@ -10307,6 +10345,9 @@ function shortUuid() {
10307
10345
  return crypto.randomUUID().slice(0, 8);
10308
10346
  }
10309
10347
  function resolvePartialId(db, table, partialId) {
10348
+ if (!ALLOWED_TABLES.has(table)) {
10349
+ throw new Error(`Invalid table name: ${table}`);
10350
+ }
10310
10351
  if (partialId.length >= 36) {
10311
10352
  const row = db.query(`SELECT id FROM ${table} WHERE id = ?`).get(partialId);
10312
10353
  return row?.id ?? null;
@@ -10317,10 +10358,25 @@ function resolvePartialId(db, table, partialId) {
10317
10358
  }
10318
10359
  return null;
10319
10360
  }
10320
- var _db = null;
10361
+ var _db = null, _dbPath = null, ALLOWED_TABLES;
10321
10362
  var init_database = __esm(() => {
10322
10363
  init_dist();
10323
10364
  init_migrations();
10365
+ ALLOWED_TABLES = new Set([
10366
+ "memories",
10367
+ "agents",
10368
+ "entities",
10369
+ "projects",
10370
+ "relations",
10371
+ "memory_audit_log",
10372
+ "locks",
10373
+ "sessions",
10374
+ "session_memory_jobs",
10375
+ "synthesis_runs",
10376
+ "synthesis_proposals",
10377
+ "tool_events",
10378
+ "webhook_hooks"
10379
+ ]);
10324
10380
  });
10325
10381
 
10326
10382
  // src/lib/hooks.ts
@@ -10993,6 +11049,22 @@ function listMemories(filter, db) {
10993
11049
  conditions.push("session_id = ?");
10994
11050
  params.push(filter.session_id);
10995
11051
  }
11052
+ if ("machine_id" in filter) {
11053
+ if (filter.machine_id === null) {
11054
+ conditions.push("machine_id IS NULL");
11055
+ } else if (filter.machine_id) {
11056
+ conditions.push("machine_id = ?");
11057
+ params.push(filter.machine_id);
11058
+ }
11059
+ }
11060
+ if ("visible_to_machine_id" in filter) {
11061
+ if (filter.visible_to_machine_id === null) {
11062
+ conditions.push("machine_id IS NULL");
11063
+ } else if (filter.visible_to_machine_id !== undefined) {
11064
+ conditions.push("(machine_id IS NULL OR machine_id = ?)");
11065
+ params.push(filter.visible_to_machine_id);
11066
+ }
11067
+ }
10996
11068
  if (filter.min_importance) {
10997
11069
  conditions.push("importance >= ?");
10998
11070
  params.push(filter.min_importance);
@@ -13355,12 +13427,14 @@ async function synthesizeProfile(options) {
13355
13427
  const prefMemories = listMemories({
13356
13428
  category: "preference",
13357
13429
  project_id: options.project_id,
13430
+ machine_id: null,
13358
13431
  status: "active",
13359
13432
  limit: 30
13360
13433
  });
13361
13434
  const factMemories = listMemories({
13362
13435
  category: "fact",
13363
13436
  project_id: options.project_id,
13437
+ machine_id: null,
13364
13438
  status: "active",
13365
13439
  limit: 30
13366
13440
  });
@@ -13671,6 +13745,65 @@ function ensureDir(dir) {
13671
13745
  // src/server/index.ts
13672
13746
  init_database();
13673
13747
 
13748
+ // src/db/machines.ts
13749
+ init_database();
13750
+ import { hostname, platform as platform2 } from "os";
13751
+ function parseMachine(row) {
13752
+ if (!row)
13753
+ return null;
13754
+ return {
13755
+ ...row,
13756
+ is_primary: Boolean(row.is_primary)
13757
+ };
13758
+ }
13759
+ function registerMachine(name, db = getDatabase()) {
13760
+ const host = hostname();
13761
+ const plat = platform2();
13762
+ const machineName = name?.trim() || host;
13763
+ const existing = parseMachine(db.query("SELECT * FROM machines WHERE hostname = ?").get(host));
13764
+ if (existing) {
13765
+ db.run("UPDATE machines SET last_seen_at = ? WHERE id = ?", [now(), existing.id]);
13766
+ return parseMachine(db.query("SELECT * FROM machines WHERE id = ?").get(existing.id));
13767
+ }
13768
+ let finalName = machineName;
13769
+ let suffix = 2;
13770
+ while (db.query("SELECT id FROM machines WHERE name = ?").get(finalName)) {
13771
+ finalName = `${machineName}-${suffix++}`;
13772
+ }
13773
+ const id = uuid();
13774
+ db.run("INSERT INTO machines (id, name, hostname, platform) VALUES (?, ?, ?, ?)", [id, finalName, host, plat]);
13775
+ return parseMachine(db.query("SELECT * FROM machines WHERE id = ?").get(id));
13776
+ }
13777
+ function getPrimaryMachine(db = getDatabase()) {
13778
+ return parseMachine(db.query("SELECT * FROM machines WHERE is_primary = 1 LIMIT 1").get());
13779
+ }
13780
+ function getPrimaryMachineCandidate(db = getDatabase()) {
13781
+ if (getPrimaryMachine(db))
13782
+ return null;
13783
+ return parseMachine(db.query("SELECT * FROM machines ORDER BY created_at ASC, id ASC LIMIT 1").get());
13784
+ }
13785
+ function getPrimaryMachineStartupWarning(db = getDatabase()) {
13786
+ if (getPrimaryMachine(db))
13787
+ return null;
13788
+ const candidate = getPrimaryMachineCandidate(db);
13789
+ if (!candidate) {
13790
+ return "No primary machine configured. Fallback sync target is unset because no machines are registered yet.";
13791
+ }
13792
+ return `No primary machine configured. Fallback sync target is unset. Candidate: ${candidate.name} (${candidate.id.slice(0, 8)} / ${candidate.hostname}). Confirm it with set_primary_machine.`;
13793
+ }
13794
+ function touchMachine(id, db = getDatabase()) {
13795
+ db.run("UPDATE machines SET last_seen_at = ? WHERE id = ?", [now(), id]);
13796
+ }
13797
+ function getCurrentMachineId(db = getDatabase()) {
13798
+ const host = hostname();
13799
+ const m = db.query("SELECT id FROM machines WHERE hostname = ?").get(host);
13800
+ if (m) {
13801
+ touchMachine(m.id, db);
13802
+ return m.id;
13803
+ }
13804
+ return registerMachine(undefined, db).id;
13805
+ }
13806
+
13674
13807
  // src/lib/built-in-hooks.ts
13675
13808
  init_hooks();
13676
13809
 
@@ -14710,6 +14843,10 @@ async function _processNext() {
14710
14843
 
14711
14844
  // src/server/router.ts
14712
14845
  var routes = [];
14846
+ var nextRouteOrder = 0;
14847
+ function computeSpecificity(path) {
14848
+ return path.split("/").filter(Boolean).reduce((score, segment) => score + (segment.startsWith(":") ? 1 : 10), 0);
14849
+ }
14713
14850
  function addRoute(method, path, handler) {
14714
14851
  const paramNames = [];
14715
14852
  const patternStr = path.replace(/:(\w+)/g, (_match, name) => {
@@ -14718,12 +14855,16 @@ function addRoute(method, path, handler) {
14718
14855
  });
14719
14856
  routes.push({
14720
14857
  method,
14858
+ path,
14721
14859
  pattern: new RegExp(`^${patternStr}$`),
14722
14860
  paramNames,
14861
+ specificity: computeSpecificity(path),
14862
+ order: nextRouteOrder++,
14723
14863
  handler
14724
14864
  });
14725
14865
  }
14726
14866
  function matchRoute(method, pathname) {
14867
+ let bestMatch = null;
14727
14868
  for (const route of routes) {
14728
14869
  if (route.method !== method)
14729
14870
  continue;
@@ -14733,10 +14874,14 @@ function matchRoute(method, pathname) {
14733
14874
  route.paramNames.forEach((name, i) => {
14734
14875
  params[name] = match[i + 1];
14735
14876
  });
14736
- return { handler: route.handler, params };
14877
+ if (!bestMatch || route.specificity > bestMatch.route.specificity || route.specificity === bestMatch.route.specificity && route.order < bestMatch.route.order) {
14878
+ bestMatch = { route, params };
14879
+ }
14737
14880
  }
14738
14881
  }
14739
- return null;
14882
+ if (!bestMatch)
14883
+ return null;
14884
+ return { handler: bestMatch.route.handler, params: bestMatch.params };
14740
14885
  }
14741
14886
 
14742
14887
  // src/server/helpers.ts
@@ -14744,7 +14889,7 @@ import { existsSync as existsSync6 } from "fs";
14744
14889
  import { dirname as dirname4, extname, join as join7 } from "path";
14745
14890
  import { fileURLToPath } from "url";
14746
14891
  var CORS_HEADERS = {
14747
- "Access-Control-Allow-Origin": "*",
14892
+ "Access-Control-Allow-Origin": process.env["MEMENTOS_CORS_ORIGIN"] ?? "http://localhost:19428",
14748
14893
  "Access-Control-Allow-Methods": "GET, POST, PATCH, DELETE, OPTIONS",
14749
14894
  "Access-Control-Allow-Headers": "Content-Type, Authorization",
14750
14895
  "Access-Control-Max-Age": "86400"
@@ -14779,6 +14924,37 @@ async function readJson(req) {
14779
14924
  return null;
14780
14925
  }
14781
14926
  }
14927
+ function getCorsHeaders(req) {
14928
+ const allowedOrigin = process.env["MEMENTOS_CORS_ORIGIN"] ?? "http://localhost:19428";
14929
+ const origin = req?.headers.get("origin");
14930
+ const finalOrigin = origin === allowedOrigin ? origin : allowedOrigin;
14931
+ return {
14932
+ "Access-Control-Allow-Origin": finalOrigin,
14933
+ "Access-Control-Allow-Methods": "GET, POST, PATCH, DELETE, OPTIONS",
14934
+ "Access-Control-Allow-Headers": "Content-Type, Authorization",
14935
+ "Access-Control-Max-Age": "86400"
14936
+ };
14937
+ }
14938
+ function authenticateRequest(req) {
14939
+ const requiredKey = process.env["MEMENTOS_API_KEY"];
14940
+ if (!requiredKey)
14941
+ return null;
14942
+ const authHeader = req.headers.get("authorization");
14943
+ if (!authHeader || !authHeader.startsWith("Bearer ")) {
14944
+ return new Response(JSON.stringify({ error: "Unauthorized. Provide a Bearer token in the Authorization header." }), {
14945
+ status: 401,
14946
+ headers: { "Content-Type": "application/json", ...getCorsHeaders(req) }
14947
+ });
14948
+ }
14949
+ const provided = authHeader.slice("Bearer ".length);
14950
+ if (provided !== requiredKey) {
14951
+ return new Response(JSON.stringify({ error: "Forbidden. Invalid API key." }), {
14952
+ status: 403,
14953
+ headers: { "Content-Type": "application/json", ...getCorsHeaders(req) }
14954
+ });
14955
+ }
14956
+ return null;
14957
+ }
14782
14958
  function getSearchParams(url) {
14783
14959
  const params = {};
14784
14960
  url.searchParams.forEach((v, k) => {
@@ -14814,11 +14990,8 @@ function serveStaticFile(filePath) {
14814
14990
  });
14815
14991
  }
14816
14992
 
14817
- // src/server/routes/memories.ts
14993
+ // src/server/routes/memories-crud.ts
14818
14994
  init_memories();
14819
- init_database();
14820
- init_search();
14821
- init_types2();
14822
14995
 
14823
14996
  // src/lib/duration.ts
14824
14997
  var UNIT_MS = {
@@ -14863,10 +15036,8 @@ var FORMAT_UNITS = [
14863
15036
  ["s", UNIT_MS["s"]]
14864
15037
  ];
14865
15038
 
14866
- // src/server/routes/memories.ts
14867
- addRoute("GET", "/api/health", () => {
14868
- return json({ ok: true, version: "1", db: getDbPath() });
14869
- });
15039
+ // src/server/routes/memories-crud.ts
15040
+ init_types2();
14870
15041
  addRoute("GET", "/api/memories", (_req, url) => {
14871
15042
  const q = getSearchParams(url);
14872
15043
  const filter = {};
@@ -14900,6 +15071,80 @@ addRoute("GET", "/api/memories", (_req, url) => {
14900
15071
  }
14901
15072
  return json({ memories, count: memories.length });
14902
15073
  });
15074
+ addRoute("POST", "/api/memories", async (req) => {
15075
+ const body = await readJson(req);
15076
+ if (!body) {
15077
+ return errorResponse("Invalid JSON body", 400);
15078
+ }
15079
+ if (!body["key"] || !body["value"]) {
15080
+ return errorResponse("Missing required fields: key, value", 400);
15081
+ }
15082
+ try {
15083
+ if (body["ttl_ms"] !== undefined && typeof body["ttl_ms"] === "string") {
15084
+ body["ttl_ms"] = parseDuration(body["ttl_ms"]);
15085
+ }
15086
+ const memory = createMemory(body);
15087
+ return json(memory, 201);
15088
+ } catch (e) {
15089
+ if (e instanceof DuplicateMemoryError) {
15090
+ return errorResponse(e.message, 409);
15091
+ }
15092
+ throw e;
15093
+ }
15094
+ });
15095
+ addRoute("GET", "/api/memories/:id", (_req, _url, params) => {
15096
+ const memory = getMemory(params["id"]);
15097
+ if (!memory) {
15098
+ return errorResponse("Memory not found", 404);
15099
+ }
15100
+ touchMemory(memory.id);
15101
+ return json(memory);
15102
+ });
15103
+ addRoute("PATCH", "/api/memories/:id", async (req, _url, params) => {
15104
+ const body = await readJson(req);
15105
+ if (!body) {
15106
+ return errorResponse("Invalid JSON body", 400);
15107
+ }
15108
+ const updateBody = { ...body };
15109
+ if (updateBody["version"] === undefined) {
15110
+ const existing = getMemory(params["id"]);
15111
+ if (!existing)
15112
+ return errorResponse("Memory not found", 404);
15113
+ updateBody["version"] = existing.version;
15114
+ }
15115
+ try {
15116
+ const memory = updateMemory(params["id"], updateBody);
15117
+ return json(memory);
15118
+ } catch (e) {
15119
+ if (e instanceof MemoryNotFoundError) {
15120
+ return errorResponse(e.message, 404);
15121
+ }
15122
+ if (e instanceof VersionConflictError) {
15123
+ return errorResponse(e.message, 409, {
15124
+ expected: e.expected,
15125
+ actual: e.actual
15126
+ });
15127
+ }
15128
+ throw e;
15129
+ }
15130
+ });
15131
+ addRoute("GET", "/api/memories/:id/versions", (_req, _url, params) => {
15132
+ const memory = getMemory(params["id"]);
15133
+ if (!memory)
15134
+ return errorResponse("Memory not found", 404);
15135
+ const versions = getMemoryVersions(memory.id);
15136
+ return json({ versions, count: versions.length, current_version: memory.version });
15137
+ });
15138
+ addRoute("DELETE", "/api/memories/:id", (_req, _url, params) => {
15139
+ const deleted = deleteMemory(params["id"]);
15140
+ if (!deleted) {
15141
+ return errorResponse("Memory not found", 404);
15142
+ }
15143
+ return json({ deleted: true });
15144
+ });
15145
+
15146
+ // src/server/routes/memories-stats.ts
15147
+ init_database();
14903
15148
  addRoute("GET", "/api/memories/stats", (_req) => {
14904
15149
  const db = getDatabase();
14905
15150
  const total = db.query("SELECT COUNT(*) as c FROM memories WHERE status = 'active'").get().c;
@@ -14975,6 +15220,8 @@ addRoute("GET", "/api/activity", (_req, url) => {
14975
15220
  params.push(projectId);
14976
15221
  }
14977
15222
  const where = conditions.map((c) => `AND ${c}`).join(" ");
15223
+ const cutoffDate = new Date(Date.now() - days * 86400000).toISOString().slice(0, 10);
15224
+ params.push(cutoffDate);
14978
15225
  const rows = db.query(`
14979
15226
  SELECT
14980
15227
  date(created_at) AS date,
@@ -14984,7 +15231,7 @@ addRoute("GET", "/api/activity", (_req, url) => {
14984
15231
  SUM(CASE WHEN scope = 'private' THEN 1 ELSE 0 END) AS private_count,
14985
15232
  AVG(importance) AS avg_importance
14986
15233
  FROM memories
14987
- WHERE date(created_at) >= date('now', '-${days} days') ${where}
15234
+ WHERE date(created_at) >= ? ${where}
14988
15235
  GROUP BY date(created_at)
14989
15236
  ORDER BY date ASC
14990
15237
  `).all(...params);
@@ -14997,12 +15244,13 @@ addRoute("GET", "/api/memories/stale", (_req, url) => {
14997
15244
  const agentId = q["agent_id"];
14998
15245
  const limit = Math.min(parseInt(q["limit"] || "20", 10), 100);
14999
15246
  const db = getDatabase();
15247
+ const cutoffDate = new Date(Date.now() - days * 86400000).toISOString();
15000
15248
  const conds = [
15001
15249
  "status = 'active'",
15002
- `(accessed_at IS NULL OR accessed_at < datetime('now', '-${days} days'))`,
15250
+ "(accessed_at IS NULL OR accessed_at < ?)",
15003
15251
  "pinned = 0"
15004
15252
  ];
15005
- const params = [];
15253
+ const params = [cutoffDate];
15006
15254
  if (projectId) {
15007
15255
  conds.push("project_id = ?");
15008
15256
  params.push(projectId);
@@ -15020,23 +15268,28 @@ addRoute("GET", "/api/report", (_req, url) => {
15020
15268
  const projectId = q["project_id"];
15021
15269
  const agentId = q["agent_id"];
15022
15270
  const db = getDatabase();
15023
- const cond = [
15271
+ const cutoffDate = new Date(Date.now() - days * 86400000).toISOString().slice(0, 10);
15272
+ const scopedCond = [
15024
15273
  projectId ? "AND project_id = ?" : "",
15025
15274
  agentId ? "AND agent_id = ?" : ""
15026
15275
  ].filter(Boolean).join(" ");
15027
- const params = [...projectId ? [projectId] : [], ...agentId ? [agentId] : []];
15028
- const total = db.query(`SELECT COUNT(*) as c FROM memories WHERE status = 'active' ${cond}`).get(...params).c;
15029
- const pinned = db.query(`SELECT COUNT(*) as c FROM memories WHERE status = 'active' AND pinned = 1 ${cond}`).get(...params).c;
15276
+ const scopedParams = [
15277
+ ...projectId ? [projectId] : [],
15278
+ ...agentId ? [agentId] : []
15279
+ ];
15280
+ const recentParams = [cutoffDate, ...scopedParams];
15281
+ const total = db.query(`SELECT COUNT(*) as c FROM memories WHERE status = 'active' ${scopedCond}`).get(...scopedParams).c;
15282
+ const pinned = db.query(`SELECT COUNT(*) as c FROM memories WHERE status = 'active' AND pinned = 1 ${scopedCond}`).get(...scopedParams).c;
15030
15283
  const actRows = db.query(`
15031
15284
  SELECT date(created_at) AS date, COUNT(*) AS memories_created
15032
- FROM memories WHERE status = 'active' AND date(created_at) >= date('now', '-${days} days') ${cond}
15285
+ FROM memories WHERE status = 'active' AND date(created_at) >= ? ${scopedCond}
15033
15286
  GROUP BY date(created_at) ORDER BY date(created_at) ASC
15034
- `).all(...params);
15287
+ `).all(...recentParams);
15035
15288
  const recentTotal = actRows.reduce((s, r) => s + r.memories_created, 0);
15036
- const byScopeRows = db.query(`SELECT scope, COUNT(*) as c FROM memories WHERE status = 'active' ${cond} GROUP BY scope`).all(...params);
15037
- const byCatRows = db.query(`SELECT category, COUNT(*) as c FROM memories WHERE status = 'active' ${cond} GROUP BY category`).all(...params);
15038
- const topMems = db.query(`SELECT id, key, value, importance, scope, category FROM memories WHERE status = 'active' ${cond} ORDER BY importance DESC, access_count DESC LIMIT 5`).all(...params);
15039
- const topAgents = db.query(`SELECT agent_id, COUNT(*) as c FROM memories WHERE status = 'active' AND agent_id IS NOT NULL ${cond} GROUP BY agent_id ORDER BY c DESC LIMIT 5`).all(...params);
15289
+ const byScopeRows = db.query(`SELECT scope, COUNT(*) as c FROM memories WHERE status = 'active' ${scopedCond} GROUP BY scope`).all(...scopedParams);
15290
+ const byCatRows = db.query(`SELECT category, COUNT(*) as c FROM memories WHERE status = 'active' ${scopedCond} GROUP BY category`).all(...scopedParams);
15291
+ const topMems = db.query(`SELECT id, key, value, importance, scope, category FROM memories WHERE status = 'active' ${scopedCond} ORDER BY importance DESC, access_count DESC LIMIT 5`).all(...scopedParams);
15292
+ const topAgents = db.query(`SELECT agent_id, COUNT(*) as c FROM memories WHERE status = 'active' AND agent_id IS NOT NULL ${scopedCond} GROUP BY agent_id ORDER BY c DESC LIMIT 5`).all(...scopedParams);
15040
15293
  return json({
15041
15294
  total,
15042
15295
  pinned,
@@ -15048,6 +15301,9 @@ addRoute("GET", "/api/report", (_req, url) => {
15048
15301
  top_agents: topAgents
15049
15302
  });
15050
15303
  });
15304
+
15305
+ // src/server/routes/memories-search.ts
15306
+ init_search();
15051
15307
  addRoute("POST", "/api/memories/search", async (req) => {
15052
15308
  const body = await readJson(req);
15053
15309
  if (!body || typeof body["query"] !== "string") {
@@ -15109,6 +15365,51 @@ addRoute("POST", "/api/memories/search/bm25", async (req) => {
15109
15365
  const results = searchWithBm25(body["query"], filter);
15110
15366
  return json({ results, count: results.length });
15111
15367
  });
15368
+
15369
+ // src/server/routes/memories-bulk.ts
15370
+ init_memories();
15371
+ addRoute("POST", "/api/memories/bulk-forget", async (req) => {
15372
+ const body = await readJson(req);
15373
+ if (!body || !Array.isArray(body["ids"])) {
15374
+ return errorResponse("Missing required field: ids (array)", 400);
15375
+ }
15376
+ const ids = body["ids"];
15377
+ let deleted = 0;
15378
+ for (const id of ids) {
15379
+ try {
15380
+ if (deleteMemory(id))
15381
+ deleted++;
15382
+ } catch {}
15383
+ }
15384
+ return json({ deleted, total: ids.length });
15385
+ });
15386
+ addRoute("POST", "/api/memories/bulk-update", async (req) => {
15387
+ const body = await readJson(req);
15388
+ if (!body || !Array.isArray(body["ids"])) {
15389
+ return errorResponse("Missing required fields: ids (array)", 400);
15390
+ }
15391
+ const ids = body["ids"];
15392
+ const { ids: _ids, ...fields } = body;
15393
+ let updated = 0;
15394
+ const errors = [];
15395
+ for (const id of ids) {
15396
+ try {
15397
+ const memory = getMemory(id);
15398
+ if (memory) {
15399
+ updateMemory(id, { ...fields, version: memory.version });
15400
+ updated++;
15401
+ } else {
15402
+ errors.push(`Memory not found: ${id}`);
15403
+ }
15404
+ } catch (e) {
15405
+ errors.push(`Failed ${id}: ${e instanceof Error ? e.message : String(e)}`);
15406
+ }
15407
+ }
15408
+ return json({ updated, errors, total: ids.length });
15409
+ });
15410
+
15411
+ // src/server/routes/memories-io.ts
15412
+ init_memories();
15112
15413
  addRoute("POST", "/api/memories/export", async (req) => {
15113
15414
  const body = await readJson(req) || {};
15114
15415
  const filter = {};
@@ -15149,49 +15450,35 @@ addRoute("POST", "/api/memories/import", async (req) => {
15149
15450
  }
15150
15451
  return json({ imported, errors, total: memoriesArr.length }, 201);
15151
15452
  });
15152
- addRoute("POST", "/api/memories/bulk-forget", async (req) => {
15153
- const body = await readJson(req);
15154
- if (!body || !Array.isArray(body["ids"])) {
15155
- return errorResponse("Missing required field: ids (array)", 400);
15156
- }
15157
- const ids = body["ids"];
15158
- let deleted = 0;
15159
- for (const id of ids) {
15160
- try {
15161
- if (deleteMemory(id))
15162
- deleted++;
15163
- } catch {}
15164
- }
15165
- return json({ deleted, total: ids.length });
15166
- });
15167
- addRoute("POST", "/api/memories/bulk-update", async (req) => {
15168
- const body = await readJson(req);
15169
- if (!body || !Array.isArray(body["ids"])) {
15170
- return errorResponse("Missing required fields: ids (array)", 400);
15453
+
15454
+ // src/server/routes/memories-misc.ts
15455
+ init_memories();
15456
+
15457
+ // src/lib/machine-visibility.ts
15458
+ function resolveVisibleMachineId(machineId, db) {
15459
+ if (machineId !== undefined) {
15460
+ return machineId;
15171
15461
  }
15172
- const ids = body["ids"];
15173
- const { ids: _ids, ...fields } = body;
15174
- let updated = 0;
15175
- const errors = [];
15176
- for (const id of ids) {
15177
- try {
15178
- const memory = getMemory(id);
15179
- if (memory) {
15180
- updateMemory(id, { ...fields, version: memory.version });
15181
- updated++;
15182
- } else {
15183
- errors.push(`Memory not found: ${id}`);
15184
- }
15185
- } catch (e) {
15186
- errors.push(`Failed ${id}: ${e instanceof Error ? e.message : String(e)}`);
15187
- }
15462
+ try {
15463
+ return getCurrentMachineId(db);
15464
+ } catch {
15465
+ return null;
15188
15466
  }
15189
- return json({ updated, errors, total: ids.length });
15467
+ }
15468
+ function visibleToMachineFilter(machineId, db) {
15469
+ return {
15470
+ visible_to_machine_id: resolveVisibleMachineId(machineId, db)
15471
+ };
15472
+ }
15473
+
15474
+ // src/server/routes/memories-misc.ts
15475
+ addRoute("GET", "/api/health", () => {
15476
+ return json({ ok: true, version: "1", db: getDbPath() });
15190
15477
  });
15191
15478
  addRoute("POST", "/api/memories/extract", async (req) => {
15192
15479
  const body = await readJson(req);
15193
15480
  if (!body)
15194
- return errorResponse("Invalid JSON body", 400);
15481
+ return json({ error: "Invalid JSON body" }, 400);
15195
15482
  const sessionId = body["session_id"];
15196
15483
  const agentId = body["agent_id"];
15197
15484
  const projectId = body["project_id"];
@@ -15259,77 +15546,6 @@ addRoute("POST", "/api/memories/clean", () => {
15259
15546
  const cleaned = cleanExpiredMemories();
15260
15547
  return json({ cleaned });
15261
15548
  });
15262
- addRoute("POST", "/api/memories", async (req) => {
15263
- const body = await readJson(req);
15264
- if (!body) {
15265
- return errorResponse("Invalid JSON body", 400);
15266
- }
15267
- if (!body["key"] || !body["value"]) {
15268
- return errorResponse("Missing required fields: key, value", 400);
15269
- }
15270
- try {
15271
- if (body["ttl_ms"] !== undefined && typeof body["ttl_ms"] === "string") {
15272
- body["ttl_ms"] = parseDuration(body["ttl_ms"]);
15273
- }
15274
- const memory = createMemory(body);
15275
- return json(memory, 201);
15276
- } catch (e) {
15277
- if (e instanceof DuplicateMemoryError) {
15278
- return errorResponse(e.message, 409);
15279
- }
15280
- throw e;
15281
- }
15282
- });
15283
- addRoute("GET", "/api/memories/:id", (_req, _url, params) => {
15284
- const memory = getMemory(params["id"]);
15285
- if (!memory) {
15286
- return errorResponse("Memory not found", 404);
15287
- }
15288
- touchMemory(memory.id);
15289
- return json(memory);
15290
- });
15291
- addRoute("PATCH", "/api/memories/:id", async (req, _url, params) => {
15292
- const body = await readJson(req);
15293
- if (!body) {
15294
- return errorResponse("Invalid JSON body", 400);
15295
- }
15296
- const updateBody = { ...body };
15297
- if (updateBody["version"] === undefined) {
15298
- const existing = getMemory(params["id"]);
15299
- if (!existing)
15300
- return errorResponse("Memory not found", 404);
15301
- updateBody["version"] = existing.version;
15302
- }
15303
- try {
15304
- const memory = updateMemory(params["id"], updateBody);
15305
- return json(memory);
15306
- } catch (e) {
15307
- if (e instanceof MemoryNotFoundError) {
15308
- return errorResponse(e.message, 404);
15309
- }
15310
- if (e instanceof VersionConflictError) {
15311
- return errorResponse(e.message, 409, {
15312
- expected: e.expected,
15313
- actual: e.actual
15314
- });
15315
- }
15316
- throw e;
15317
- }
15318
- });
15319
- addRoute("GET", "/api/memories/:id/versions", (_req, _url, params) => {
15320
- const memory = getMemory(params["id"]);
15321
- if (!memory)
15322
- return errorResponse("Memory not found", 404);
15323
- const versions = getMemoryVersions(memory.id);
15324
- return json({ versions, count: versions.length, current_version: memory.version });
15325
- });
15326
- addRoute("DELETE", "/api/memories/:id", (_req, _url, params) => {
15327
- const deleted = deleteMemory(params["id"]);
15328
- if (!deleted) {
15329
- return errorResponse("Memory not found", 404);
15330
- }
15331
- return json({ deleted: true });
15332
- });
15333
15549
  addRoute("GET", "/api/inject", (_req, url) => {
15334
15550
  const q = getSearchParams(url);
15335
15551
  const maxTokens = q["max_tokens"] ? parseInt(q["max_tokens"], 10) : 500;
@@ -15339,6 +15555,7 @@ addRoute("GET", "/api/inject", (_req, url) => {
15339
15555
  "fact",
15340
15556
  "knowledge"
15341
15557
  ];
15558
+ const visibleMachineId = resolveVisibleMachineId(q["machine_id"]);
15342
15559
  const allMemories = [];
15343
15560
  const globalMems = listMemories({
15344
15561
  scope: "global",
@@ -15346,6 +15563,7 @@ addRoute("GET", "/api/inject", (_req, url) => {
15346
15563
  min_importance: minImportance,
15347
15564
  status: "active",
15348
15565
  project_id: q["project_id"],
15566
+ ...visibleToMachineFilter(visibleMachineId),
15349
15567
  limit: 50
15350
15568
  });
15351
15569
  allMemories.push(...globalMems);
@@ -15356,6 +15574,7 @@ addRoute("GET", "/api/inject", (_req, url) => {
15356
15574
  min_importance: minImportance,
15357
15575
  status: "active",
15358
15576
  project_id: q["project_id"],
15577
+ ...visibleToMachineFilter(visibleMachineId),
15359
15578
  limit: 50
15360
15579
  });
15361
15580
  allMemories.push(...sharedMems);
@@ -15367,6 +15586,7 @@ addRoute("GET", "/api/inject", (_req, url) => {
15367
15586
  min_importance: minImportance,
15368
15587
  status: "active",
15369
15588
  agent_id: q["agent_id"],
15589
+ ...visibleToMachineFilter(visibleMachineId),
15370
15590
  limit: 50
15371
15591
  });
15372
15592
  allMemories.push(...privateMems);
@@ -16028,10 +16248,133 @@ addRoute("GET", "/api/graph/:entityId", (_req, url, params) => {
16028
16248
  }
16029
16249
  });
16030
16250
 
16031
- // src/server/routes/system.ts
16251
+ // src/server/routes/system-auto-memory.ts
16032
16252
  init_auto_memory();
16033
16253
  init_registry();
16254
+ function registerSystemAutoMemoryRoutes() {
16255
+ addRoute("POST", "/api/auto-memory/process", async (req) => {
16256
+ const body = await readJson(req);
16257
+ const turn = body?.turn;
16258
+ if (!turn)
16259
+ return errorResponse("turn is required", 400);
16260
+ processConversationTurn(turn, { agentId: body?.agent_id, projectId: body?.project_id, sessionId: body?.session_id });
16261
+ const stats = getAutoMemoryStats();
16262
+ return json({ queued: true, queue: stats }, 202);
16263
+ });
16264
+ addRoute("GET", "/api/auto-memory/status", () => {
16265
+ return json({
16266
+ queue: getAutoMemoryStats(),
16267
+ config: providerRegistry.getConfig(),
16268
+ providers: providerRegistry.health()
16269
+ });
16270
+ });
16271
+ addRoute("GET", "/api/auto-memory/config", () => {
16272
+ return json(providerRegistry.getConfig());
16273
+ });
16274
+ addRoute("PATCH", "/api/auto-memory/config", async (req) => {
16275
+ const body = await readJson(req) ?? {};
16276
+ const patch = {};
16277
+ if (body.provider)
16278
+ patch.provider = body.provider;
16279
+ if (body.model)
16280
+ patch.model = body.model;
16281
+ if (body.enabled !== undefined)
16282
+ patch.enabled = Boolean(body.enabled);
16283
+ if (body.min_importance !== undefined)
16284
+ patch.minImportance = Number(body.min_importance);
16285
+ if (body.auto_entity_link !== undefined)
16286
+ patch.autoEntityLink = Boolean(body.auto_entity_link);
16287
+ configureAutoMemory(patch);
16288
+ return json({ updated: true, config: providerRegistry.getConfig() });
16289
+ });
16290
+ addRoute("POST", "/api/auto-memory/test", async (req) => {
16291
+ const body = await readJson(req) ?? {};
16292
+ const { turn, provider: providerName, agent_id, project_id } = body;
16293
+ if (!turn)
16294
+ return errorResponse("turn is required", 400);
16295
+ const provider = providerName ? providerRegistry.getProvider(providerName) : providerRegistry.getAvailable();
16296
+ if (!provider)
16297
+ return errorResponse("No LLM provider configured. Set an API key (ANTHROPIC_API_KEY, OPENAI_API_KEY, CEREBRAS_API_KEY, or XAI_API_KEY).", 503);
16298
+ const memories = await provider.extractMemories(turn, { agentId: agent_id, projectId: project_id });
16299
+ return json({
16300
+ provider: provider.name,
16301
+ model: provider.config.model,
16302
+ extracted: memories,
16303
+ count: memories.length,
16304
+ note: "DRY RUN \u2014 nothing was saved"
16305
+ });
16306
+ });
16307
+ }
16308
+
16309
+ // src/server/routes/system-hooks.ts
16034
16310
  init_hooks();
16311
+ function registerSystemHookRoutes() {
16312
+ addRoute("GET", "/api/hooks", (_req, url) => {
16313
+ const type = url.searchParams.get("type") ?? undefined;
16314
+ const hooks = hookRegistry.list(type);
16315
+ return json(hooks.map((h) => ({
16316
+ id: h.id,
16317
+ type: h.type,
16318
+ blocking: h.blocking,
16319
+ priority: h.priority,
16320
+ builtin: h.builtin ?? false,
16321
+ agentId: h.agentId,
16322
+ projectId: h.projectId,
16323
+ description: h.description
16324
+ })));
16325
+ });
16326
+ addRoute("GET", "/api/hooks/stats", () => json(hookRegistry.stats()));
16327
+ addRoute("GET", "/api/webhooks", (_req, url) => {
16328
+ const type = url.searchParams.get("type") ?? undefined;
16329
+ const enabledParam = url.searchParams.get("enabled");
16330
+ const enabled = enabledParam !== null ? enabledParam === "true" : undefined;
16331
+ return json(listWebhookHooks({
16332
+ type,
16333
+ enabled
16334
+ }));
16335
+ });
16336
+ addRoute("POST", "/api/webhooks", async (req) => {
16337
+ const body = await readJson(req) ?? {};
16338
+ if (!body.type || !body.handler_url) {
16339
+ return errorResponse("type and handler_url are required", 400);
16340
+ }
16341
+ const wh = createWebhookHook({
16342
+ type: body.type,
16343
+ handlerUrl: body.handler_url,
16344
+ priority: body.priority,
16345
+ blocking: body.blocking,
16346
+ agentId: body.agent_id,
16347
+ projectId: body.project_id,
16348
+ description: body.description
16349
+ });
16350
+ reloadWebhooks();
16351
+ return json(wh, 201);
16352
+ });
16353
+ addRoute("GET", "/api/webhooks/:id", (_req, _url, params) => {
16354
+ const wh = getWebhookHook(params["id"]);
16355
+ if (!wh)
16356
+ return errorResponse("Webhook not found", 404);
16357
+ return json(wh);
16358
+ });
16359
+ addRoute("PATCH", "/api/webhooks/:id", async (req, _url, params) => {
16360
+ const body = await readJson(req) ?? {};
16361
+ const updated = updateWebhookHook(params["id"], {
16362
+ enabled: body.enabled,
16363
+ priority: body.priority,
16364
+ description: body.description
16365
+ });
16366
+ if (!updated)
16367
+ return errorResponse("Webhook not found", 404);
16368
+ reloadWebhooks();
16369
+ return json(updated);
16370
+ });
16371
+ addRoute("DELETE", "/api/webhooks/:id", (_req, _url, params) => {
16372
+ const deleted = deleteWebhookHook(params["id"]);
16373
+ if (!deleted)
16374
+ return errorResponse("Webhook not found", 404);
16375
+ return new Response(null, { status: 204 });
16376
+ });
16377
+ }
16035
16378
 
16036
16379
  // src/lib/synthesis/index.ts
16037
16380
  init_database();
@@ -16935,9 +17278,49 @@ function getSynthesisStatus(runId, projectId, db) {
16935
17278
  };
16936
17279
  }
16937
17280
 
16938
- // src/server/routes/system.ts
17281
+ // src/server/routes/system-synthesis.ts
16939
17282
  init_synthesis();
16940
17283
  init_profile_synthesizer();
17284
+ function registerSystemSynthesisRoutes() {
17285
+ addRoute("POST", "/api/synthesis/run", async (req) => {
17286
+ const body = await readJson(req) ?? {};
17287
+ const result = await runSynthesis({
17288
+ projectId: body.project_id,
17289
+ agentId: body.agent_id,
17290
+ dryRun: body.dry_run,
17291
+ maxProposals: body.max_proposals,
17292
+ provider: body.provider
17293
+ });
17294
+ return json(result, result.dryRun ? 200 : 201);
17295
+ });
17296
+ addRoute("GET", "/api/synthesis/runs", (_req, url) => {
17297
+ const projectId = url.searchParams.get("project_id") ?? undefined;
17298
+ const limit = url.searchParams.get("limit") ? parseInt(url.searchParams.get("limit")) : 20;
17299
+ const runs = listSynthesisRuns({ project_id: projectId, limit });
17300
+ return json({ runs, count: runs.length });
17301
+ });
17302
+ addRoute("GET", "/api/synthesis/status", (_req, url) => {
17303
+ const projectId = url.searchParams.get("project_id") ?? undefined;
17304
+ const runId = url.searchParams.get("run_id") ?? undefined;
17305
+ return json(getSynthesisStatus(runId, projectId));
17306
+ });
17307
+ addRoute("POST", "/api/synthesis/rollback/:run_id", async (_req, _url, params) => {
17308
+ const result = await rollbackSynthesis(params["run_id"]);
17309
+ return json(result);
17310
+ });
17311
+ addRoute("GET", "/api/profile/synthesize", async (_req, url) => {
17312
+ const q = getSearchParams(url);
17313
+ const result = await synthesizeProfile({
17314
+ project_id: q["project_id"] || undefined,
17315
+ agent_id: q["agent_id"] || undefined,
17316
+ force_refresh: q["force_refresh"] === "true"
17317
+ });
17318
+ if (!result) {
17319
+ return json({ profile: null, message: "No preference/fact memories found to synthesize" });
17320
+ }
17321
+ return json(result);
17322
+ });
17323
+ }
16941
17324
 
16942
17325
  // src/lib/session-auto-resolve.ts
16943
17326
  function autoResolveAgentProject(metadata, db) {
@@ -17006,255 +17389,136 @@ function autoResolveAgentProject(metadata, db) {
17006
17389
  };
17007
17390
  }
17008
17391
 
17009
- // src/server/routes/system.ts
17010
- init_database();
17011
- addRoute("POST", "/api/auto-memory/process", async (req) => {
17012
- const body = await readJson(req);
17013
- const turn = body?.turn;
17014
- if (!turn)
17015
- return errorResponse("turn is required", 400);
17016
- processConversationTurn(turn, { agentId: body?.agent_id, projectId: body?.project_id, sessionId: body?.session_id });
17017
- const stats = getAutoMemoryStats();
17018
- return json({ queued: true, queue: stats }, 202);
17019
- });
17020
- addRoute("GET", "/api/auto-memory/status", () => {
17021
- return json({
17022
- queue: getAutoMemoryStats(),
17023
- config: providerRegistry.getConfig(),
17024
- providers: providerRegistry.health()
17392
+ // src/server/routes/system-sessions.ts
17393
+ function registerSystemSessionRoutes() {
17394
+ addRoute("POST", "/api/sessions/ingest", async (req) => {
17395
+ const body = await readJson(req) ?? {};
17396
+ const { transcript, session_id, agent_id, project_id, source, metadata } = body;
17397
+ if (!transcript || typeof transcript !== "string")
17398
+ return errorResponse("transcript is required", 400);
17399
+ if (!session_id || typeof session_id !== "string")
17400
+ return errorResponse("session_id is required", 400);
17401
+ let resolvedAgentId = agent_id;
17402
+ let resolvedProjectId = project_id;
17403
+ if (!resolvedAgentId || !resolvedProjectId) {
17404
+ const resolved = autoResolveAgentProject(metadata ?? {});
17405
+ if (!resolvedAgentId && resolved.agentId)
17406
+ resolvedAgentId = resolved.agentId;
17407
+ if (!resolvedProjectId && resolved.projectId)
17408
+ resolvedProjectId = resolved.projectId;
17409
+ }
17410
+ const job = createSessionJob({
17411
+ session_id,
17412
+ transcript,
17413
+ source: source ?? "manual",
17414
+ agent_id: resolvedAgentId,
17415
+ project_id: resolvedProjectId,
17416
+ metadata: metadata ?? {}
17417
+ });
17418
+ enqueueSessionJob(job.id);
17419
+ return json({ job_id: job.id, status: "queued", message: "Session queued for memory extraction" }, 202);
17025
17420
  });
17026
- });
17027
- addRoute("GET", "/api/auto-memory/config", () => {
17028
- return json(providerRegistry.getConfig());
17029
- });
17030
- addRoute("PATCH", "/api/auto-memory/config", async (req) => {
17031
- const body = await readJson(req) ?? {};
17032
- const patch = {};
17033
- if (body.provider)
17034
- patch.provider = body.provider;
17035
- if (body.model)
17036
- patch.model = body.model;
17037
- if (body.enabled !== undefined)
17038
- patch.enabled = Boolean(body.enabled);
17039
- if (body.min_importance !== undefined)
17040
- patch.minImportance = Number(body.min_importance);
17041
- if (body.auto_entity_link !== undefined)
17042
- patch.autoEntityLink = Boolean(body.auto_entity_link);
17043
- configureAutoMemory(patch);
17044
- return json({ updated: true, config: providerRegistry.getConfig() });
17045
- });
17046
- addRoute("POST", "/api/auto-memory/test", async (req) => {
17047
- const body = await readJson(req) ?? {};
17048
- const { turn, provider: providerName, agent_id, project_id } = body;
17049
- if (!turn)
17050
- return errorResponse("turn is required", 400);
17051
- const provider = providerName ? providerRegistry.getProvider(providerName) : providerRegistry.getAvailable();
17052
- if (!provider)
17053
- return errorResponse("No LLM provider configured. Set an API key (ANTHROPIC_API_KEY, OPENAI_API_KEY, CEREBRAS_API_KEY, or XAI_API_KEY).", 503);
17054
- const memories = await provider.extractMemories(turn, { agentId: agent_id, projectId: project_id });
17055
- return json({
17056
- provider: provider.name,
17057
- model: provider.config.model,
17058
- extracted: memories,
17059
- count: memories.length,
17060
- note: "DRY RUN \u2014 nothing was saved"
17421
+ addRoute("GET", "/api/sessions/jobs", (_req, url) => {
17422
+ const agentId = url.searchParams.get("agent_id") ?? undefined;
17423
+ const projectId = url.searchParams.get("project_id") ?? undefined;
17424
+ const status = url.searchParams.get("status") ?? undefined;
17425
+ const limit = url.searchParams.get("limit") ? parseInt(url.searchParams.get("limit")) : 20;
17426
+ const jobs = listSessionJobs({ agent_id: agentId, project_id: projectId, status, limit });
17427
+ return json({ jobs, count: jobs.length });
17061
17428
  });
17062
- });
17063
- addRoute("GET", "/api/hooks", (_req, url) => {
17064
- const type = url.searchParams.get("type") ?? undefined;
17065
- const hooks = hookRegistry.list(type);
17066
- return json(hooks.map((h) => ({
17067
- id: h.id,
17068
- type: h.type,
17069
- blocking: h.blocking,
17070
- priority: h.priority,
17071
- builtin: h.builtin ?? false,
17072
- agentId: h.agentId,
17073
- projectId: h.projectId,
17074
- description: h.description
17075
- })));
17076
- });
17077
- addRoute("GET", "/api/hooks/stats", () => json(hookRegistry.stats()));
17078
- addRoute("GET", "/api/webhooks", (_req, url) => {
17079
- const type = url.searchParams.get("type") ?? undefined;
17080
- const enabledParam = url.searchParams.get("enabled");
17081
- const enabled = enabledParam !== null ? enabledParam === "true" : undefined;
17082
- return json(listWebhookHooks({
17083
- type,
17084
- enabled
17085
- }));
17086
- });
17087
- addRoute("POST", "/api/webhooks", async (req) => {
17088
- const body = await readJson(req) ?? {};
17089
- if (!body.type || !body.handler_url) {
17090
- return errorResponse("type and handler_url are required", 400);
17091
- }
17092
- const wh = createWebhookHook({
17093
- type: body.type,
17094
- handlerUrl: body.handler_url,
17095
- priority: body.priority,
17096
- blocking: body.blocking,
17097
- agentId: body.agent_id,
17098
- projectId: body.project_id,
17099
- description: body.description
17429
+ addRoute("GET", "/api/sessions/jobs/:id", (_req, _url, params) => {
17430
+ const job = getSessionJob(params["id"]);
17431
+ if (!job)
17432
+ return errorResponse("Session job not found", 404);
17433
+ return json(job);
17100
17434
  });
17101
- reloadWebhooks();
17102
- return json(wh, 201);
17103
- });
17104
- addRoute("GET", "/api/webhooks/:id", (_req, _url, params) => {
17105
- const wh = getWebhookHook(params["id"]);
17106
- if (!wh)
17107
- return errorResponse("Webhook not found", 404);
17108
- return json(wh);
17109
- });
17110
- addRoute("PATCH", "/api/webhooks/:id", async (req, _url, params) => {
17111
- const body = await readJson(req) ?? {};
17112
- const updated = updateWebhookHook(params["id"], {
17113
- enabled: body.enabled,
17114
- priority: body.priority,
17115
- description: body.description
17435
+ addRoute("GET", "/api/sessions/queue/stats", () => json(getSessionQueueStats()));
17436
+ }
17437
+
17438
+ // src/server/routes/system-tools.ts
17439
+ function registerSystemToolRoutes() {
17440
+ addRoute("POST", "/api/tool-events", async (req) => {
17441
+ const body = await readJson(req);
17442
+ if (!body || !body["tool_name"]) {
17443
+ return errorResponse("Missing required field: tool_name", 400);
17444
+ }
17445
+ const event = saveToolEvent(body);
17446
+ return json(event, 201);
17116
17447
  });
17117
- if (!updated)
17118
- return errorResponse("Webhook not found", 404);
17119
- reloadWebhooks();
17120
- return json(updated);
17121
- });
17122
- addRoute("DELETE", "/api/webhooks/:id", (_req, _url, params) => {
17123
- const deleted = deleteWebhookHook(params["id"]);
17124
- if (!deleted)
17125
- return errorResponse("Webhook not found", 404);
17126
- return new Response(null, { status: 204 });
17127
- });
17128
- addRoute("POST", "/api/synthesis/run", async (req) => {
17129
- const body = await readJson(req) ?? {};
17130
- const result = await runSynthesis({
17131
- projectId: body.project_id,
17132
- agentId: body.agent_id,
17133
- dryRun: body.dry_run,
17134
- maxProposals: body.max_proposals,
17135
- provider: body.provider
17448
+ addRoute("GET", "/api/tool-events", (_req, url) => {
17449
+ const q = getSearchParams(url);
17450
+ const filters = {};
17451
+ if (q["tool_name"])
17452
+ filters.tool_name = q["tool_name"];
17453
+ if (q["agent_id"])
17454
+ filters.agent_id = q["agent_id"];
17455
+ if (q["project_id"])
17456
+ filters.project_id = q["project_id"];
17457
+ if (q["success"] !== undefined && q["success"] !== "")
17458
+ filters.success = q["success"] === "true";
17459
+ if (q["from_date"])
17460
+ filters.from_date = q["from_date"];
17461
+ if (q["to_date"])
17462
+ filters.to_date = q["to_date"];
17463
+ if (q["limit"])
17464
+ filters.limit = parseInt(q["limit"], 10);
17465
+ if (q["offset"])
17466
+ filters.offset = parseInt(q["offset"], 10);
17467
+ const events = getToolEvents(filters);
17468
+ return json({ events, count: events.length });
17136
17469
  });
17137
- return json(result, result.dryRun ? 200 : 201);
17138
- });
17139
- addRoute("GET", "/api/synthesis/runs", (_req, url) => {
17140
- const projectId = url.searchParams.get("project_id") ?? undefined;
17141
- const limit = url.searchParams.get("limit") ? parseInt(url.searchParams.get("limit")) : 20;
17142
- const runs = listSynthesisRuns({ project_id: projectId, limit });
17143
- return json({ runs, count: runs.length });
17144
- });
17145
- addRoute("GET", "/api/synthesis/status", (_req, url) => {
17146
- const projectId = url.searchParams.get("project_id") ?? undefined;
17147
- const runId = url.searchParams.get("run_id") ?? undefined;
17148
- return json(getSynthesisStatus(runId, projectId));
17149
- });
17150
- addRoute("POST", "/api/synthesis/rollback/:run_id", async (_req, _url, params) => {
17151
- const result = await rollbackSynthesis(params["run_id"]);
17152
- return json(result);
17153
- });
17154
- addRoute("POST", "/api/sessions/ingest", async (req) => {
17155
- const body = await readJson(req) ?? {};
17156
- const { transcript, session_id, agent_id, project_id, source, metadata } = body;
17157
- if (!transcript || typeof transcript !== "string")
17158
- return errorResponse("transcript is required", 400);
17159
- if (!session_id || typeof session_id !== "string")
17160
- return errorResponse("session_id is required", 400);
17161
- let resolvedAgentId = agent_id;
17162
- let resolvedProjectId = project_id;
17163
- if (!resolvedAgentId || !resolvedProjectId) {
17164
- const resolved = autoResolveAgentProject(metadata ?? {});
17165
- if (!resolvedAgentId && resolved.agentId)
17166
- resolvedAgentId = resolved.agentId;
17167
- if (!resolvedProjectId && resolved.projectId)
17168
- resolvedProjectId = resolved.projectId;
17169
- }
17170
- const job = createSessionJob({
17171
- session_id,
17172
- transcript,
17173
- source: source ?? "manual",
17174
- agent_id: resolvedAgentId,
17175
- project_id: resolvedProjectId,
17176
- metadata: metadata ?? {}
17470
+ addRoute("GET", "/api/tool-insights/:tool_name", (_req, url, params) => {
17471
+ const q = getSearchParams(url);
17472
+ const toolName = decodeURIComponent(params["tool_name"]);
17473
+ const projectId = q["project_id"];
17474
+ const lessonsLimit = q["limit"] ? parseInt(q["limit"], 10) : 20;
17475
+ const stats = getToolStats(toolName, projectId || undefined);
17476
+ const lessons = getToolLessons(toolName, projectId || undefined, lessonsLimit);
17477
+ return json({ stats, lessons });
17177
17478
  });
17178
- enqueueSessionJob(job.id);
17179
- return json({ job_id: job.id, status: "queued", message: "Session queued for memory extraction" }, 202);
17180
- });
17181
- addRoute("GET", "/api/sessions/jobs", (_req, url) => {
17182
- const agentId = url.searchParams.get("agent_id") ?? undefined;
17183
- const projectId = url.searchParams.get("project_id") ?? undefined;
17184
- const status = url.searchParams.get("status") ?? undefined;
17185
- const limit = url.searchParams.get("limit") ? parseInt(url.searchParams.get("limit")) : 20;
17186
- const jobs = listSessionJobs({ agent_id: agentId, project_id: projectId, status, limit });
17187
- return json({ jobs, count: jobs.length });
17188
- });
17189
- addRoute("GET", "/api/sessions/jobs/:id", (_req, _url, params) => {
17190
- const job = getSessionJob(params["id"]);
17191
- if (!job)
17192
- return errorResponse("Session job not found", 404);
17193
- return json(job);
17194
- });
17195
- addRoute("GET", "/api/sessions/queue/stats", () => json(getSessionQueueStats()));
17196
- addRoute("POST", "/api/tool-events", async (req) => {
17197
- const body = await readJson(req);
17198
- if (!body || !body["tool_name"]) {
17199
- return errorResponse("Missing required field: tool_name", 400);
17200
- }
17201
- const event = saveToolEvent(body);
17202
- return json(event, 201);
17203
- });
17204
- addRoute("GET", "/api/tool-events", (_req, url) => {
17205
- const q = getSearchParams(url);
17206
- const filters = {};
17207
- if (q["tool_name"])
17208
- filters.tool_name = q["tool_name"];
17209
- if (q["agent_id"])
17210
- filters.agent_id = q["agent_id"];
17211
- if (q["project_id"])
17212
- filters.project_id = q["project_id"];
17213
- if (q["success"] !== undefined && q["success"] !== "")
17214
- filters.success = q["success"] === "true";
17215
- if (q["from_date"])
17216
- filters.from_date = q["from_date"];
17217
- if (q["to_date"])
17218
- filters.to_date = q["to_date"];
17219
- if (q["limit"])
17220
- filters.limit = parseInt(q["limit"], 10);
17221
- if (q["offset"])
17222
- filters.offset = parseInt(q["offset"], 10);
17223
- const events = getToolEvents(filters);
17224
- return json({ events, count: events.length });
17225
- });
17226
- addRoute("GET", "/api/tool-insights/:tool_name", (_req, url, params) => {
17227
- const q = getSearchParams(url);
17228
- const toolName = decodeURIComponent(params["tool_name"]);
17229
- const projectId = q["project_id"];
17230
- const lessonsLimit = q["limit"] ? parseInt(q["limit"], 10) : 20;
17231
- const stats = getToolStats(toolName, projectId || undefined);
17232
- const lessons = getToolLessons(toolName, projectId || undefined, lessonsLimit);
17233
- return json({ stats, lessons });
17234
- });
17235
- addRoute("GET", "/api/profile/synthesize", async (_req, url) => {
17236
- const q = getSearchParams(url);
17237
- const result = await synthesizeProfile({
17238
- project_id: q["project_id"] || undefined,
17239
- agent_id: q["agent_id"] || undefined,
17240
- force_refresh: q["force_refresh"] === "true"
17479
+ }
17480
+
17481
+ // src/server/routes/system-chain.ts
17482
+ init_database();
17483
+ function registerSystemChainRoutes() {
17484
+ addRoute("GET", "/api/chains/:sequence_group", (_req, _url, params) => {
17485
+ const db = getDatabase();
17486
+ const sequenceGroup = decodeURIComponent(params["sequence_group"]);
17487
+ const rows = db.query(`SELECT * FROM memories WHERE sequence_group = ? AND status = 'active' ORDER BY sequence_order ASC`).all(sequenceGroup);
17488
+ if (rows.length === 0) {
17489
+ return json({ chain: [], count: 0, sequence_group: sequenceGroup });
17490
+ }
17491
+ return json({ chain: rows, count: rows.length, sequence_group: sequenceGroup });
17241
17492
  });
17242
- if (!result) {
17243
- return json({ profile: null, message: "No preference/fact memories found to synthesize" });
17244
- }
17245
- return json(result);
17246
- });
17247
- addRoute("GET", "/api/chains/:sequence_group", (_req, _url, params) => {
17248
- const db = getDatabase();
17249
- const sequenceGroup = decodeURIComponent(params["sequence_group"]);
17250
- const rows = db.query(`SELECT * FROM memories WHERE sequence_group = ? AND status = 'active' ORDER BY sequence_order ASC`).all(sequenceGroup);
17251
- if (rows.length === 0) {
17252
- return json({ chain: [], count: 0, sequence_group: sequenceGroup });
17253
- }
17254
- return json({ chain: rows, count: rows.length, sequence_group: sequenceGroup });
17255
- });
17493
+ }
17494
+
17495
+ // src/server/routes/system.ts
17496
+ registerSystemAutoMemoryRoutes();
17497
+ registerSystemHookRoutes();
17498
+ registerSystemSynthesisRoutes();
17499
+ registerSystemSessionRoutes();
17500
+ registerSystemToolRoutes();
17501
+ registerSystemChainRoutes();
17256
17502
 
17257
17503
  // src/server/index.ts
17504
+ async function findFreePort(start) {
17505
+ const net = await import("net");
17506
+ return new Promise((resolve4) => {
17507
+ const server = net.createServer();
17508
+ server.unref();
17509
+ server.on("error", () => {
17510
+ resolve4(findFreePort(start + 1));
17511
+ });
17512
+ server.listen(start, () => {
17513
+ const address = server.address();
17514
+ if (address && typeof address === "object") {
17515
+ server.close(() => resolve4(address.port));
17516
+ } else {
17517
+ resolve4(start);
17518
+ }
17519
+ });
17520
+ });
17521
+ }
17258
17522
  var DEFAULT_PORT = 19428;
17259
17523
  function hasFlag(...flags) {
17260
17524
  return process.argv.some((arg) => flags.includes(arg));
@@ -17302,115 +17566,142 @@ function parsePort() {
17302
17566
  }
17303
17567
  return DEFAULT_PORT;
17304
17568
  }
17305
- async function findFreePort(start) {
17306
- for (let port = start;port < start + 100; port++) {
17307
- try {
17308
- const server = Bun.serve({ port, fetch: () => new Response("") });
17309
- server.stop(true);
17310
- return port;
17311
- } catch {}
17312
- }
17313
- return start;
17569
+ var _serverInitialized = false;
17570
+ function warnIfPrimaryMachineUnset() {
17571
+ try {
17572
+ const warning = getPrimaryMachineStartupWarning(getDatabase());
17573
+ if (warning) {
17574
+ console.warn(`[mementos-serve] ${warning}`);
17575
+ }
17576
+ } catch {}
17314
17577
  }
17315
- function startServer(port) {
17578
+ function initServer() {
17579
+ if (_serverInitialized)
17580
+ return;
17581
+ _serverInitialized = true;
17582
+ warnIfPrimaryMachineUnset();
17316
17583
  loadWebhooksFromDb();
17317
17584
  startSessionQueueWorker();
17318
- const hostname = process.env["MEMENTOS_HOST"] ?? "127.0.0.1";
17319
- Bun.serve({
17320
- port,
17321
- hostname,
17322
- async fetch(req) {
17323
- const url = new URL(req.url);
17324
- const { pathname } = url;
17325
- if (req.method === "OPTIONS") {
17326
- return new Response(null, { status: 204, headers: CORS_HEADERS });
17327
- }
17328
- if (pathname === "/api/health" || pathname === "/health") {
17329
- const profile = getActiveProfile();
17330
- const { createRequire: createRequire2 } = await import("module");
17331
- const req2 = createRequire2(import.meta.url);
17332
- const pkg = req2("../../package.json");
17333
- const db = getDatabase();
17334
- const total = db.query("SELECT COUNT(*) as c FROM memories WHERE status = 'active'").get().c;
17335
- const expired = db.query("SELECT COUNT(*) as c FROM memories WHERE status = 'expired' OR (expires_at IS NOT NULL AND expires_at < datetime('now'))").get().c;
17336
- const pinned = db.query("SELECT COUNT(*) as c FROM memories WHERE status = 'active' AND pinned = 1").get().c;
17337
- const agents = db.query("SELECT COUNT(*) as c FROM agents").get().c;
17338
- const projects = db.query("SELECT COUNT(*) as c FROM projects").get().c;
17339
- const status = expired > 50 ? "warn" : "ok";
17340
- return json({ status, version: pkg.version, profile: profile ?? "default", db_path: getDbPath(), hostname, memories: { total, expired, pinned }, agents, projects });
17341
- }
17342
- if (pathname === "/api/profile" && req.method === "GET") {
17343
- const profile = getActiveProfile();
17344
- return json({ active: profile ?? null, profiles: listProfiles(), db_path: getDbPath() });
17345
- }
17346
- if (pathname === "/api/memories/stream" && req.method === "GET") {
17347
- const stream = new ReadableStream({
17348
- start(controller) {
17349
- const encoder = new TextEncoder;
17350
- let lastSeen = new Date().toISOString();
17351
- const send = (data) => {
17352
- controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}
17585
+ }
17586
+ function startServer(port, attempt = 0) {
17587
+ const maxRetries = 100;
17588
+ initServer();
17589
+ const hostname2 = process.env["MEMENTOS_HOST"] ?? "127.0.0.1";
17590
+ try {
17591
+ Bun.serve({
17592
+ port,
17593
+ hostname: hostname2,
17594
+ async fetch(req) {
17595
+ const url = new URL(req.url);
17596
+ const { pathname } = url;
17597
+ if (req.method === "OPTIONS") {
17598
+ const origin = req.headers.get("origin");
17599
+ const allowedOrigin = process.env["MEMENTOS_CORS_ORIGIN"] ?? "http://localhost:19428";
17600
+ if (origin && origin !== allowedOrigin) {
17601
+ return new Response(null, { status: 403 });
17602
+ }
17603
+ return new Response(null, { status: 204, headers: getCorsHeaders(req) });
17604
+ }
17605
+ if (pathname === "/api/health" || pathname === "/health") {
17606
+ const profile = getActiveProfile();
17607
+ const { createRequire: createRequire2 } = await import("module");
17608
+ const req2 = createRequire2(import.meta.url);
17609
+ const pkg = req2("../../package.json");
17610
+ const db = getDatabase();
17611
+ const total = db.query("SELECT COUNT(*) as c FROM memories WHERE status = 'active'").get().c;
17612
+ const expired = db.query("SELECT COUNT(*) as c FROM memories WHERE status = 'expired' OR (expires_at IS NOT NULL AND expires_at < datetime('now'))").get().c;
17613
+ const pinned = db.query("SELECT COUNT(*) as c FROM memories WHERE status = 'active' AND pinned = 1").get().c;
17614
+ const agents = db.query("SELECT COUNT(*) as c FROM agents").get().c;
17615
+ const projects = db.query("SELECT COUNT(*) as c FROM projects").get().c;
17616
+ const status = expired > 50 ? "warn" : "ok";
17617
+ return json({ status, version: pkg.version, profile: profile ?? "default", db_path: getDbPath(), hostname: hostname2, memories: { total, expired, pinned }, agents, projects });
17618
+ }
17619
+ if (pathname.startsWith("/api/") && pathname !== "/api/health") {
17620
+ const authError = authenticateRequest(req);
17621
+ if (authError)
17622
+ return authError;
17623
+ }
17624
+ if (pathname === "/api/profile" && req.method === "GET") {
17625
+ const profile = getActiveProfile();
17626
+ return json({ active: profile ?? null, profiles: listProfiles(), db_path: getDbPath() });
17627
+ }
17628
+ if (pathname === "/api/memories/stream" && req.method === "GET") {
17629
+ const stream = new ReadableStream({
17630
+ start(controller) {
17631
+ const encoder = new TextEncoder;
17632
+ let lastSeen = new Date().toISOString();
17633
+ const send = (data) => {
17634
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}
17353
17635
 
17354
17636
  `));
17355
- };
17356
- send({ type: "connected", timestamp: lastSeen });
17357
- const interval = setInterval(() => {
17358
- try {
17359
- const db = getDatabase();
17360
- const rows = db.query("SELECT * FROM memories WHERE updated_at > ? OR created_at > ? ORDER BY updated_at DESC LIMIT 50").all(lastSeen, lastSeen);
17361
- if (rows.length > 0) {
17362
- lastSeen = new Date().toISOString();
17363
- send({ type: "memories", data: rows, count: rows.length });
17364
- }
17365
- } catch {}
17366
- }, 1000);
17367
- req.signal.addEventListener("abort", () => {
17368
- clearInterval(interval);
17369
- controller.close();
17370
- });
17637
+ };
17638
+ send({ type: "connected", timestamp: lastSeen });
17639
+ const interval = setInterval(() => {
17640
+ try {
17641
+ const db = getDatabase();
17642
+ const rows = db.query("SELECT * FROM memories WHERE updated_at > ? OR created_at > ? ORDER BY updated_at DESC LIMIT 50").all(lastSeen, lastSeen);
17643
+ if (rows.length > 0) {
17644
+ lastSeen = new Date().toISOString();
17645
+ send({ type: "memories", data: rows, count: rows.length });
17646
+ }
17647
+ } catch {}
17648
+ }, 1000);
17649
+ req.signal.addEventListener("abort", () => {
17650
+ clearInterval(interval);
17651
+ controller.close();
17652
+ });
17653
+ }
17654
+ });
17655
+ return new Response(stream, {
17656
+ headers: {
17657
+ "Content-Type": "text/event-stream",
17658
+ "Cache-Control": "no-cache",
17659
+ Connection: "keep-alive",
17660
+ ...CORS_HEADERS
17661
+ }
17662
+ });
17663
+ }
17664
+ const matched = matchRoute(req.method, pathname);
17665
+ if (!matched) {
17666
+ if (pathname.startsWith("/api/")) {
17667
+ return errorResponse("Not found", 404);
17371
17668
  }
17372
- });
17373
- return new Response(stream, {
17374
- headers: {
17375
- "Content-Type": "text/event-stream",
17376
- "Cache-Control": "no-cache",
17377
- Connection: "keep-alive",
17378
- ...CORS_HEADERS
17669
+ const dashDir = resolveDashboardDir();
17670
+ if (existsSync7(dashDir) && (req.method === "GET" || req.method === "HEAD")) {
17671
+ if (pathname !== "/") {
17672
+ const resolvedDash = resolve3(dashDir) + sep;
17673
+ const requestedPath = resolve3(join8(dashDir, pathname));
17674
+ if (requestedPath.startsWith(resolvedDash)) {
17675
+ const staticRes = serveStaticFile(requestedPath);
17676
+ if (staticRes)
17677
+ return staticRes;
17678
+ }
17679
+ }
17680
+ const indexRes = serveStaticFile(join8(dashDir, "index.html"));
17681
+ if (indexRes)
17682
+ return indexRes;
17379
17683
  }
17380
- });
17381
- }
17382
- const matched = matchRoute(req.method, pathname);
17383
- if (!matched) {
17384
- if (pathname.startsWith("/api/")) {
17385
17684
  return errorResponse("Not found", 404);
17386
17685
  }
17387
- const dashDir = resolveDashboardDir();
17388
- if (existsSync7(dashDir) && (req.method === "GET" || req.method === "HEAD")) {
17389
- if (pathname !== "/") {
17390
- const resolvedDash = resolve3(dashDir) + sep;
17391
- const requestedPath = resolve3(join8(dashDir, pathname));
17392
- if (requestedPath.startsWith(resolvedDash)) {
17393
- const staticRes = serveStaticFile(requestedPath);
17394
- if (staticRes)
17395
- return staticRes;
17396
- }
17397
- }
17398
- const indexRes = serveStaticFile(join8(dashDir, "index.html"));
17399
- if (indexRes)
17400
- return indexRes;
17686
+ try {
17687
+ return await matched.handler(req, url, matched.params);
17688
+ } catch (e) {
17689
+ console.error(`[mementos-serve] ${req.method} ${pathname}:`, e);
17690
+ return errorResponse("Internal server error", 500);
17401
17691
  }
17402
- return errorResponse("Not found", 404);
17403
- }
17404
- try {
17405
- return await matched.handler(req, url, matched.params);
17406
- } catch (e) {
17407
- console.error(`[mementos-serve] ${req.method} ${pathname}:`, e);
17408
- const message = e instanceof Error ? e.message : "Internal server error";
17409
- return errorResponse(message, 500);
17410
17692
  }
17693
+ });
17694
+ console.log(`Mementos server listening on http://${hostname2}:${port}`);
17695
+ } catch (e) {
17696
+ const err = e;
17697
+ if (err.code === "EADDRINUSE" && attempt < maxRetries) {
17698
+ const nextPort = port + attempt + 1;
17699
+ console.log(`Port ${port} in use, trying ${nextPort}`);
17700
+ startServer(nextPort, attempt + 1);
17701
+ } else {
17702
+ throw e;
17411
17703
  }
17412
- });
17413
- console.log(`Mementos server listening on http://${hostname}:${port}`);
17704
+ }
17414
17705
  }
17415
17706
  async function main() {
17416
17707
  if (hasFlag("--help", "-h")) {