@hasna/mementos 0.14.69 → 0.14.71

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mcp/index.js CHANGED
@@ -145,6 +145,27 @@ var init_types = __esm(() => {
145
145
  };
146
146
  });
147
147
 
148
+ // src/generated/storage-kit/mode.ts
149
+ function normalizeStorageMode(value) {
150
+ const normalized = value.trim().toLowerCase().replace(/-/g, "_");
151
+ if (normalized === "local")
152
+ return { mode: "local", deprecatedAlias: null };
153
+ if (normalized === "cloud")
154
+ return { mode: "cloud", deprecatedAlias: null };
155
+ if (DEPRECATED_STORAGE_MODE_ALIASES.includes(normalized)) {
156
+ return { mode: "cloud", deprecatedAlias: normalized };
157
+ }
158
+ throw new Error(`Unknown storage mode: ${value}. Use local or cloud.`);
159
+ }
160
+ var DEPRECATED_STORAGE_MODE_ALIASES;
161
+ var init_mode = __esm(() => {
162
+ DEPRECATED_STORAGE_MODE_ALIASES = [
163
+ "remote",
164
+ "hybrid",
165
+ "self_hosted"
166
+ ];
167
+ });
168
+
148
169
  // src/storage.ts
149
170
  import { Database } from "bun:sqlite";
150
171
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
@@ -403,18 +424,20 @@ function warnDeprecatedStorageMode(alias) {
403
424
  warnedDeprecatedModes.add(alias);
404
425
  process.emitWarning(`${MEMENTOS_STORAGE_ENV.mode}="${alias}" is deprecated; use "cloud". ` + `"${alias}" now maps to pure-remote cloud storage. The local<->remote ` + `sync path is deprecated and is not the fleet cutover path.`, { type: "DeprecationWarning", code: "MEMENTOS_STORAGE_MODE_ALIAS" });
405
426
  }
406
- function normalizeStorageMode(value) {
407
- if (!value)
427
+ function normalizeStorageMode2(value, source) {
428
+ if (!value || !value.trim())
408
429
  return null;
409
- const normalized = value.trim().toLowerCase();
410
- if (normalized === "local" || normalized === "cloud") {
411
- return normalized;
430
+ let normalized;
431
+ try {
432
+ normalized = normalizeStorageMode(value);
433
+ } catch (error) {
434
+ const detail = error instanceof Error ? error.message : String(error);
435
+ throw new Error(`mementos: ${source}=${value} is not a valid mode. ${detail}`);
412
436
  }
413
- if (normalized === "remote" || normalized === "hybrid") {
414
- warnDeprecatedStorageMode(normalized);
415
- return "cloud";
437
+ if (normalized.deprecatedAlias) {
438
+ warnDeprecatedStorageMode(normalized.deprecatedAlias);
416
439
  }
417
- return null;
440
+ return normalized.mode;
418
441
  }
419
442
  function readConfigFile() {
420
443
  if (!existsSync(STORAGE_CONFIG_PATH)) {
@@ -444,7 +467,7 @@ function getStorageDatabaseUrl() {
444
467
  }
445
468
  function getStorageModeOverride() {
446
469
  for (const env of MODE_ENV_NAMES) {
447
- const value = normalizeStorageMode(readEnv(env.name) ?? undefined);
470
+ const value = normalizeStorageMode2(readEnv(env.name) ?? undefined, env.name);
448
471
  if (value)
449
472
  return value;
450
473
  }
@@ -454,7 +477,7 @@ function getStorageConfig() {
454
477
  const fileConfig = readConfigFile();
455
478
  const modeOverride = getStorageModeOverride();
456
479
  const envConnectionString = getConfiguredConnectionString();
457
- const fileMode = normalizeStorageMode(fileConfig.mode);
480
+ const fileMode = normalizeStorageMode2(fileConfig.mode, `${STORAGE_CONFIG_PATH} "mode"`);
458
481
  const merged = {
459
482
  ...DEFAULT_STORAGE_CONFIG,
460
483
  ...fileConfig,
@@ -865,6 +888,7 @@ CREATE TABLE IF NOT EXISTS _sync_meta (
865
888
  direction TEXT DEFAULT 'push'
866
889
  )`;
867
890
  var init_storage = __esm(() => {
891
+ init_mode();
868
892
  PgSyncPool = class PgSyncPool {
869
893
  worker;
870
894
  status;
@@ -1216,7 +1240,24 @@ var init_api_mode = __esm(() => {
1216
1240
  });
1217
1241
 
1218
1242
  // src/db/migrations.ts
1219
- var MIGRATIONS;
1243
+ var MEMORY_VERSION_SNAPSHOT_TRIGGER = `
1244
+ CREATE TRIGGER IF NOT EXISTS memories_version_snapshot
1245
+ BEFORE UPDATE ON memories
1246
+ WHEN NEW.version > OLD.version
1247
+ BEGIN
1248
+ INSERT OR IGNORE INTO memory_versions (
1249
+ id, memory_id, version, value, importance, scope, category, tags,
1250
+ summary, pinned, status, when_to_use, created_at
1251
+ ) VALUES (
1252
+ lower(hex(randomblob(4))) || '-' || lower(hex(randomblob(2))) || '-' ||
1253
+ lower(hex(randomblob(2))) || '-' || lower(hex(randomblob(2))) || '-' ||
1254
+ lower(hex(randomblob(6))),
1255
+ OLD.id, OLD.version, OLD.value, OLD.importance, OLD.scope, OLD.category,
1256
+ OLD.tags, OLD.summary, OLD.pinned, OLD.status, OLD.when_to_use,
1257
+ OLD.updated_at
1258
+ );
1259
+ END;
1260
+ `, MIGRATIONS;
1220
1261
  var init_migrations = __esm(() => {
1221
1262
  MIGRATIONS = [
1222
1263
  `
@@ -2114,6 +2155,10 @@ CREATE INDEX IF NOT EXISTS idx_memory_reflection_lessons_memory ON memory_reflec
2114
2155
  CREATE INDEX IF NOT EXISTS idx_memory_reflection_lessons_kind ON memory_reflection_lessons(kind);
2115
2156
 
2116
2157
  INSERT OR IGNORE INTO _migrations (id) VALUES (35);
2158
+ `,
2159
+ `
2160
+ ${MEMORY_VERSION_SNAPSHOT_TRIGGER}
2161
+ INSERT OR IGNORE INTO _migrations (id) VALUES (36);
2117
2162
  `
2118
2163
  ];
2119
2164
  });
@@ -2128,6 +2173,7 @@ __export(exports_database, {
2128
2173
  now: () => now,
2129
2174
  getDbPath: () => getDbPath,
2130
2175
  getDatabase: () => getDatabase,
2176
+ escapeLikePrefix: () => escapeLikePrefix,
2131
2177
  closeDatabase: () => closeDatabase
2132
2178
  });
2133
2179
  import { existsSync as existsSync2, mkdirSync as mkdirSync2, cpSync } from "fs";
@@ -2302,15 +2348,20 @@ function uuid() {
2302
2348
  function shortUuid() {
2303
2349
  return crypto.randomUUID().slice(0, 8);
2304
2350
  }
2351
+ function escapeLikePrefix(s) {
2352
+ return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
2353
+ }
2305
2354
  function resolvePartialId(db, table, partialId) {
2306
2355
  if (!ALLOWED_TABLES.has(table)) {
2307
2356
  throw new Error(`Invalid table name: ${table}`);
2308
2357
  }
2358
+ if (partialId === "")
2359
+ return null;
2309
2360
  if (partialId.length >= 36) {
2310
2361
  const row = db.query(`SELECT id FROM ${table} WHERE id = ?`).get(partialId);
2311
2362
  return row?.id ?? null;
2312
2363
  }
2313
- const rows = db.query(`SELECT id FROM ${table} WHERE id LIKE ?`).all(`${partialId}%`);
2364
+ const rows = db.query(`SELECT id FROM ${table} WHERE id LIKE ? ESCAPE '\\'`).all(`${escapeLikePrefix(partialId)}%`);
2314
2365
  if (rows.length === 1) {
2315
2366
  return rows[0].id;
2316
2367
  }
@@ -3372,33 +3423,19 @@ function updateMemory(id, input, db) {
3372
3423
  const { status, data } = apiJson("PATCH", `/memories/${encodeURIComponent(id)}`, input, { allow404: true });
3373
3424
  if (status === 404)
3374
3425
  throw new MemoryNotFoundError(id);
3426
+ if (data && typeof input.version === "number" && typeof data.version === "number" && data.version <= input.version) {
3427
+ throw new Error(`Update did not persist for memory ${id}: the server returned success but the record is unchanged ` + `(version still ${data.version}). Your data was NOT written. ` + `The server is likely running a build predating the partial-id fix \u2014 pass the full 36-character id as a workaround.`);
3428
+ }
3375
3429
  return data;
3376
3430
  }
3377
3431
  const d = db || getDatabase();
3378
3432
  const existing = getMemory(id, d);
3379
3433
  if (!existing)
3380
3434
  throw new MemoryNotFoundError(id);
3435
+ const memoryId = existing.id;
3381
3436
  if (existing.version !== input.version) {
3382
3437
  throw new VersionConflictError(id, input.version, existing.version);
3383
3438
  }
3384
- try {
3385
- d.run(`INSERT OR IGNORE INTO memory_versions (id, memory_id, version, value, importance, scope, category, tags, summary, pinned, status, when_to_use, created_at)
3386
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
3387
- uuid(),
3388
- existing.id,
3389
- existing.version,
3390
- existing.value,
3391
- existing.importance,
3392
- existing.scope,
3393
- existing.category,
3394
- JSON.stringify(existing.tags),
3395
- existing.summary,
3396
- existing.pinned ? 1 : 0,
3397
- existing.status,
3398
- existing.when_to_use || null,
3399
- existing.updated_at
3400
- ]);
3401
- } catch {}
3402
3439
  const sets = ["version = version + 1", "updated_at = ?"];
3403
3440
  const params = [now()];
3404
3441
  if (input.value !== undefined) {
@@ -3448,15 +3485,18 @@ function updateMemory(id, input, db) {
3448
3485
  if (input.tags !== undefined) {
3449
3486
  sets.push("tags = ?");
3450
3487
  params.push(JSON.stringify(input.tags));
3451
- d.run("DELETE FROM memory_tags WHERE memory_id = ?", [id]);
3488
+ d.run("DELETE FROM memory_tags WHERE memory_id = ?", [memoryId]);
3452
3489
  const insertTag = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
3453
3490
  for (const tag of input.tags) {
3454
- insertTag.run(id, tag);
3491
+ insertTag.run(memoryId, tag);
3455
3492
  }
3456
3493
  }
3457
- params.push(id);
3458
- d.run(`UPDATE memories SET ${sets.join(", ")} WHERE id = ?`, params);
3459
- const updated = getMemory(id, d);
3494
+ params.push(memoryId);
3495
+ const result = d.run(`UPDATE memories SET ${sets.join(", ")} WHERE id = ?`, params);
3496
+ if (result.changes === 0) {
3497
+ throw new Error(`Update affected no rows for memory ${memoryId}: the record was read but not written. ` + `This is a bug in @hasna/mementos, not a bad argument \u2014 please report it.`);
3498
+ }
3499
+ const updated = getMemory(memoryId, d);
3460
3500
  if (input.value !== undefined) {
3461
3501
  try {
3462
3502
  const oldLinks = getEntityMemoryLinks(undefined, updated.id, d);
@@ -3481,10 +3521,11 @@ function deleteMemory(id, db) {
3481
3521
  return status !== 404;
3482
3522
  }
3483
3523
  const d = db || getDatabase();
3484
- const result = d.run("DELETE FROM memories WHERE id = ?", [id]);
3524
+ const memoryId = resolvePartialId(d, "memories", id) ?? id;
3525
+ const result = d.run("DELETE FROM memories WHERE id = ?", [memoryId]);
3485
3526
  if (result.changes > 0) {
3486
3527
  hookRegistry.runHooks("PostMemoryDelete", {
3487
- memoryId: id,
3528
+ memoryId,
3488
3529
  timestamp: Date.now()
3489
3530
  });
3490
3531
  }
@@ -3498,11 +3539,12 @@ function bulkDeleteMemories(ids, db) {
3498
3539
  return data?.deleted ?? 0;
3499
3540
  }
3500
3541
  const d = db || getDatabase();
3501
- const placeholders = ids.map(() => "?").join(",");
3502
- const countRow = d.query(`SELECT COUNT(*) as c FROM memories WHERE id IN (${placeholders})`).get(...ids);
3542
+ const resolvedIds = ids.map((id) => resolvePartialId(d, "memories", id) ?? id);
3543
+ const placeholders = resolvedIds.map(() => "?").join(",");
3544
+ const countRow = d.query(`SELECT COUNT(*) as c FROM memories WHERE id IN (${placeholders})`).get(...resolvedIds);
3503
3545
  const count = countRow.c;
3504
3546
  if (count > 0) {
3505
- d.run(`DELETE FROM memories WHERE id IN (${placeholders})`, ids);
3547
+ d.run(`DELETE FROM memories WHERE id IN (${placeholders})`, resolvedIds);
3506
3548
  }
3507
3549
  return count;
3508
3550
  }
@@ -5704,6 +5746,32 @@ class AutoMemoryQueue {
5704
5746
  getStats() {
5705
5747
  return { ...this.stats, pending: this.queue.length };
5706
5748
  }
5749
+ async waitForIdleForTests(timeoutMs = 3000) {
5750
+ const start = Date.now();
5751
+ while (Date.now() - start < timeoutMs) {
5752
+ if (this.queue.length === 0 && this.activeCount === 0)
5753
+ return;
5754
+ await new Promise((r) => setTimeout(r, 20));
5755
+ }
5756
+ throw new Error("autoMemoryQueue did not become idle before test reset");
5757
+ }
5758
+ resetForTests(handler) {
5759
+ if (this.activeCount !== 0) {
5760
+ throw new Error("Cannot reset autoMemoryQueue while jobs are processing");
5761
+ }
5762
+ this.queue = [];
5763
+ this.running = false;
5764
+ this.stats = {
5765
+ pending: 0,
5766
+ processing: 0,
5767
+ processed: 0,
5768
+ failed: 0,
5769
+ dropped: 0
5770
+ };
5771
+ if (handler !== undefined) {
5772
+ this.handler = handler;
5773
+ }
5774
+ }
5707
5775
  startLoop() {
5708
5776
  this.running = true;
5709
5777
  this.loop();
@@ -5749,6 +5817,7 @@ var init_auto_memory_queue = __esm(() => {
5749
5817
  // src/lib/auto-memory.ts
5750
5818
  var exports_auto_memory = {};
5751
5819
  __export(exports_auto_memory, {
5820
+ resetAutoMemoryForTests: () => resetAutoMemoryForTests,
5752
5821
  processConversationTurn: () => processConversationTurn,
5753
5822
  getAutoMemoryStats: () => getAutoMemoryStats,
5754
5823
  configureAutoMemory: () => configureAutoMemory
@@ -5909,6 +5978,12 @@ function getAutoMemoryStats() {
5909
5978
  function configureAutoMemory(config) {
5910
5979
  providerRegistry.configure(config);
5911
5980
  }
5981
+ async function resetAutoMemoryForTests() {
5982
+ if (autoMemoryQueue.getStats().processing > 0) {
5983
+ await autoMemoryQueue.waitForIdleForTests();
5984
+ }
5985
+ autoMemoryQueue.resetForTests(processJob);
5986
+ }
5912
5987
  var DEDUP_SIMILARITY_THRESHOLD = 0.85;
5913
5988
  var init_auto_memory = __esm(() => {
5914
5989
  init_memories();
@@ -6597,6 +6672,8 @@ var init_built_in_hooks = __esm(() => {
6597
6672
  priority: 100,
6598
6673
  description: "Trigger async LLM entity extraction when a memory is saved",
6599
6674
  handler: async (ctx) => {
6675
+ if (process.env["NODE_ENV"] === "test")
6676
+ return;
6600
6677
  if (ctx.wasUpdated)
6601
6678
  return;
6602
6679
  const processConversationTurn2 = await getAutoMemory();
@@ -11971,11 +12048,14 @@ __export(exports_gdpr, {
11971
12048
  });
11972
12049
  function gdprErase(identifier, options = {}, db) {
11973
12050
  const d = db || getDatabase();
12051
+ if (identifier.trim() === "") {
12052
+ throw new Error("GDPR erase requires a non-empty identifier: an empty or whitespace-only " + "identifier matches every memory and would redact the entire store");
12053
+ }
11974
12054
  const timestamp = now();
11975
12055
  const conditions = [
11976
- "(key LIKE ? OR value LIKE ? OR summary LIKE ? OR tags LIKE ? OR metadata LIKE ?)"
12056
+ "(key LIKE ? ESCAPE '\\' OR value LIKE ? ESCAPE '\\' OR summary LIKE ? ESCAPE '\\' OR tags LIKE ? ESCAPE '\\' OR metadata LIKE ? ESCAPE '\\')"
11977
12057
  ];
11978
- const searchParam = `%${identifier}%`;
12058
+ const searchParam = `%${escapeLikePrefix(identifier)}%`;
11979
12059
  const params = [searchParam, searchParam, searchParam, searchParam, searchParam];
11980
12060
  if (options.project_id) {
11981
12061
  conditions.push("project_id = ?");
@@ -11994,18 +12074,23 @@ function gdprErase(identifier, options = {}, db) {
11994
12074
  timestamp
11995
12075
  };
11996
12076
  }
11997
- const memoryIds = [];
11998
- for (const row of rows) {
11999
- d.run(`UPDATE memories SET
12077
+ const memoryIds = d.transaction(() => {
12078
+ const ids = [];
12079
+ for (const row of rows) {
12080
+ const redactedKey = `[REDACTED]:${crypto.randomUUID()}`;
12081
+ d.run(`UPDATE memories SET
12082
+ key = ?,
12000
12083
  value = '[REDACTED]',
12001
12084
  summary = NULL,
12002
12085
  tags = '[]',
12003
12086
  metadata = '{}',
12004
12087
  updated_at = ?
12005
- WHERE id = ?`, [timestamp, row.id]);
12006
- d.run("DELETE FROM memory_tags WHERE memory_id = ?", [row.id]);
12007
- memoryIds.push(row.id);
12008
- }
12088
+ WHERE id = ?`, [redactedKey, timestamp, row.id]);
12089
+ d.run("DELETE FROM memory_tags WHERE memory_id = ?", [row.id]);
12090
+ ids.push(row.id);
12091
+ }
12092
+ return ids;
12093
+ });
12009
12094
  return {
12010
12095
  erased_count: memoryIds.length,
12011
12096
  memory_ids: memoryIds,
@@ -12897,6 +12982,31 @@ var init_pg_migrations = __esm(() => {
12897
12982
  );
12898
12983
  CREATE INDEX IF NOT EXISTS idx_task_comments_task ON task_comments(task_id);
12899
12984
  CREATE INDEX IF NOT EXISTS idx_task_comments_agent ON task_comments(agent_id);
12985
+ `,
12986
+ `
12987
+ CREATE OR REPLACE FUNCTION snapshot_memory_version() RETURNS trigger AS $$
12988
+ BEGIN
12989
+ INSERT INTO memory_versions (
12990
+ id, memory_id, version, value, importance, scope, category, tags,
12991
+ summary, pinned, status, when_to_use, created_at
12992
+ ) VALUES (
12993
+ gen_random_uuid()::text,
12994
+ OLD.id, OLD.version, OLD.value, OLD.importance, OLD.scope, OLD.category,
12995
+ OLD.tags, OLD.summary, OLD.pinned, OLD.status, OLD.when_to_use,
12996
+ OLD.updated_at
12997
+ ) ON CONFLICT DO NOTHING;
12998
+ RETURN NEW;
12999
+ END;
13000
+ $$ LANGUAGE plpgsql;
13001
+
13002
+ DROP TRIGGER IF EXISTS memories_version_snapshot ON memories;
13003
+ CREATE TRIGGER memories_version_snapshot
13004
+ BEFORE UPDATE ON memories
13005
+ FOR EACH ROW
13006
+ WHEN (NEW.version > OLD.version)
13007
+ EXECUTE FUNCTION snapshot_memory_version();
13008
+
13009
+ INSERT INTO _migrations (id) VALUES (36) ON CONFLICT DO NOTHING;
12900
13010
  `
12901
13011
  ];
12902
13012
  });
@@ -55710,7 +55820,7 @@ function getAgent(idOrName, db) {
55710
55820
  row = d.query("SELECT * FROM agents WHERE LOWER(name) = ?").get(idOrName.trim().toLowerCase());
55711
55821
  if (row)
55712
55822
  return parseAgentRow(row);
55713
- const rows = d.query("SELECT * FROM agents WHERE id LIKE ?").all(`${idOrName}%`);
55823
+ const rows = d.query("SELECT * FROM agents WHERE id LIKE ? ESCAPE '\\'").all(`${escapeLikePrefix(idOrName)}%`);
55714
55824
  if (rows.length === 1)
55715
55825
  return parseAgentRow(rows[0]);
55716
55826
  return null;
@@ -63762,7 +63872,7 @@ ${lines.join(`
63762
63872
  }
63763
63873
  });
63764
63874
  server.tool("memory_gdpr_erase", "GDPR right to be forgotten: erase all memories containing a PII identifier. Replaces content with [REDACTED], preserves anonymized audit trail. IRREVERSIBLE.", {
63765
- identifier: z.string().describe("PII to search for and erase (name, email, etc.)"),
63875
+ identifier: z.string().min(1).describe("PII to search for and erase (name, email, etc.)"),
63766
63876
  project_id: z.string().optional(),
63767
63877
  dry_run: z.boolean().optional().describe("Preview what would be erased without actually erasing (default: false)")
63768
63878
  }, async (args) => {
@@ -65427,11 +65537,12 @@ function hasFlag(...flags) {
65427
65537
  function printHelp() {
65428
65538
  process.stdout.write(`Usage: mementos-mcp [options]
65429
65539
 
65430
- Mementos MCP server (stdio transport by default)
65540
+ Mementos MCP server (Streamable HTTP transport by default)
65431
65541
 
65432
65542
  Options:
65433
- --http Serve MCP over Streamable HTTP (127.0.0.1)
65434
- --port <number> HTTP port (default: 8824, env: MCP_HTTP_PORT)
65543
+ --http Serve MCP over Streamable HTTP (default, 127.0.0.1)
65544
+ --stdio Serve MCP over stdio (env: MCP_STDIO=1)
65545
+ --port <number> HTTP port (default: 8867, env: MCP_HTTP_PORT)
65435
65546
  -h, --help Show help
65436
65547
  -V, --version Show version
65437
65548
  `);
@@ -1 +1 @@
1
- {"version":3,"file":"system-tools-memory-admin.d.ts","sourceRoot":"","sources":["../../../src/mcp/tools/system-tools-memory-admin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAA6B,MAAM,0BAA0B,CAAC;AAI1F,wBAAgB,8BAA8B,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,YAAY,EAAE,SAAS,EAAE,WAAW,EAAE,SAAS,EAAE,iBAAiB,EAAE,EAAE,cAAc,GAAG,IAAI,CAmTtJ"}
1
+ {"version":3,"file":"system-tools-memory-admin.d.ts","sourceRoot":"","sources":["../../../src/mcp/tools/system-tools-memory-admin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAA6B,MAAM,0BAA0B,CAAC;AAI1F,wBAAgB,8BAA8B,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,YAAY,EAAE,SAAS,EAAE,WAAW,EAAE,SAAS,EAAE,iBAAiB,EAAE,EAAE,cAAc,GAAG,IAAI,CAwTtJ"}