@hasna/mementos 0.14.68 → 0.14.70

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +155 -73
  2. package/dist/cli/__fixtures__/clean-fallback-stub-server.d.ts +2 -0
  3. package/dist/cli/__fixtures__/clean-fallback-stub-server.d.ts.map +1 -0
  4. package/dist/cli/commands/io-clean.d.ts.map +1 -1
  5. package/dist/cli/commands/memory-cmd-crud.d.ts.map +1 -1
  6. package/dist/cli/commands/storage.d.ts.map +1 -1
  7. package/dist/cli/commands/system-mcp.d.ts.map +1 -1
  8. package/dist/cli/commands/system-profile.d.ts.map +1 -1
  9. package/dist/cli/global-options.d.ts +27 -0
  10. package/dist/cli/global-options.d.ts.map +1 -0
  11. package/dist/cli/index.js +699 -301
  12. package/dist/cli/register-all.d.ts +17 -0
  13. package/dist/cli/register-all.d.ts.map +1 -0
  14. package/dist/cli/startup-side-effects.d.ts +9 -0
  15. package/dist/cli/startup-side-effects.d.ts.map +1 -0
  16. package/dist/db/__fixtures__/fail-closed-stub-server.d.ts +2 -0
  17. package/dist/db/__fixtures__/fail-closed-stub-server.d.ts.map +1 -0
  18. package/dist/db/agents.d.ts.map +1 -1
  19. package/dist/db/api-mode.d.ts +70 -2
  20. package/dist/db/api-mode.d.ts.map +1 -1
  21. package/dist/db/database.d.ts +16 -0
  22. package/dist/db/database.d.ts.map +1 -1
  23. package/dist/db/locks.d.ts.map +1 -1
  24. package/dist/db/memories.d.ts +24 -6
  25. package/dist/db/memories.d.ts.map +1 -1
  26. package/dist/db/migrations.d.ts +1 -0
  27. package/dist/db/migrations.d.ts.map +1 -1
  28. package/dist/db/pg-migrations.d.ts.map +1 -1
  29. package/dist/db/session-jobs.d.ts.map +1 -1
  30. package/dist/db/store-backend.d.ts +33 -0
  31. package/dist/db/store-backend.d.ts.map +1 -0
  32. package/dist/index.js +196 -62
  33. package/dist/lib/auto-memory-queue.d.ts +2 -0
  34. package/dist/lib/auto-memory-queue.d.ts.map +1 -1
  35. package/dist/lib/auto-memory.d.ts +1 -0
  36. package/dist/lib/auto-memory.d.ts.map +1 -1
  37. package/dist/lib/built-in-hooks.d.ts.map +1 -1
  38. package/dist/lib/enum-validation.d.ts +20 -0
  39. package/dist/lib/enum-validation.d.ts.map +1 -0
  40. package/dist/lib/gdpr.d.ts.map +1 -1
  41. package/dist/mcp/index.d.ts.map +1 -1
  42. package/dist/mcp/index.js +301 -78
  43. package/dist/mcp/tools/system-tools-memory-admin.d.ts.map +1 -1
  44. package/dist/server/helpers.d.ts +11 -0
  45. package/dist/server/helpers.d.ts.map +1 -1
  46. package/dist/server/index.d.ts.map +1 -1
  47. package/dist/server/index.js +304 -72
  48. package/dist/storage.d.ts +11 -2
  49. package/dist/storage.d.ts.map +1 -1
  50. package/dist/storage.js +42 -11
  51. package/dist/test-support/preload-local-store.d.ts +2 -0
  52. package/dist/test-support/preload-local-store.d.ts.map +1 -0
  53. package/dist/test-support/store-isolation.d.ts +83 -0
  54. package/dist/test-support/store-isolation.d.ts.map +1 -0
  55. package/dist/types/index.d.ts +8 -4
  56. package/dist/types/index.d.ts.map +1 -1
  57. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -2099,11 +2099,32 @@ var require_commander = __commonJS((exports) => {
2099
2099
  exports.InvalidOptionArgumentError = InvalidArgumentError;
2100
2100
  });
2101
2101
 
2102
+ // src/generated/storage-kit/mode.ts
2103
+ function normalizeStorageMode(value) {
2104
+ const normalized = value.trim().toLowerCase().replace(/-/g, "_");
2105
+ if (normalized === "local")
2106
+ return { mode: "local", deprecatedAlias: null };
2107
+ if (normalized === "cloud")
2108
+ return { mode: "cloud", deprecatedAlias: null };
2109
+ if (DEPRECATED_STORAGE_MODE_ALIASES.includes(normalized)) {
2110
+ return { mode: "cloud", deprecatedAlias: normalized };
2111
+ }
2112
+ throw new Error(`Unknown storage mode: ${value}. Use local or cloud.`);
2113
+ }
2114
+ var DEPRECATED_STORAGE_MODE_ALIASES;
2115
+ var init_mode = __esm(() => {
2116
+ DEPRECATED_STORAGE_MODE_ALIASES = [
2117
+ "remote",
2118
+ "hybrid",
2119
+ "self_hosted"
2120
+ ];
2121
+ });
2122
+
2102
2123
  // src/storage.ts
2103
2124
  import { Database } from "bun:sqlite";
2104
- import { existsSync as existsSync2, mkdirSync, readFileSync, writeFileSync } from "fs";
2105
- import { homedir as homedir2 } from "os";
2106
- import { join as join2 } from "path";
2125
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
2126
+ import { homedir } from "os";
2127
+ import { join } from "path";
2107
2128
  import { fileURLToPath } from "url";
2108
2129
  import { Worker } from "worker_threads";
2109
2130
  import pg from "pg";
@@ -2357,21 +2378,23 @@ function warnDeprecatedStorageMode(alias) {
2357
2378
  warnedDeprecatedModes.add(alias);
2358
2379
  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" });
2359
2380
  }
2360
- function normalizeStorageMode(value) {
2361
- if (!value)
2381
+ function normalizeStorageMode2(value, source) {
2382
+ if (!value || !value.trim())
2362
2383
  return null;
2363
- const normalized = value.trim().toLowerCase();
2364
- if (normalized === "local" || normalized === "cloud") {
2365
- return normalized;
2384
+ let normalized;
2385
+ try {
2386
+ normalized = normalizeStorageMode(value);
2387
+ } catch (error) {
2388
+ const detail = error instanceof Error ? error.message : String(error);
2389
+ throw new Error(`mementos: ${source}=${value} is not a valid mode. ${detail}`);
2366
2390
  }
2367
- if (normalized === "remote" || normalized === "hybrid") {
2368
- warnDeprecatedStorageMode(normalized);
2369
- return "cloud";
2391
+ if (normalized.deprecatedAlias) {
2392
+ warnDeprecatedStorageMode(normalized.deprecatedAlias);
2370
2393
  }
2371
- return null;
2394
+ return normalized.mode;
2372
2395
  }
2373
2396
  function readConfigFile() {
2374
- if (!existsSync2(STORAGE_CONFIG_PATH)) {
2397
+ if (!existsSync(STORAGE_CONFIG_PATH)) {
2375
2398
  return {};
2376
2399
  }
2377
2400
  try {
@@ -2398,7 +2421,7 @@ function getStorageDatabaseUrl() {
2398
2421
  }
2399
2422
  function getStorageModeOverride() {
2400
2423
  for (const env of MODE_ENV_NAMES) {
2401
- const value = normalizeStorageMode(readEnv(env.name) ?? undefined);
2424
+ const value = normalizeStorageMode2(readEnv(env.name) ?? undefined, env.name);
2402
2425
  if (value)
2403
2426
  return value;
2404
2427
  }
@@ -2408,7 +2431,7 @@ function getStorageConfig() {
2408
2431
  const fileConfig = readConfigFile();
2409
2432
  const modeOverride = getStorageModeOverride();
2410
2433
  const envConnectionString = getConfiguredConnectionString();
2411
- const fileMode = normalizeStorageMode(fileConfig.mode);
2434
+ const fileMode = normalizeStorageMode2(fileConfig.mode, `${STORAGE_CONFIG_PATH} "mode"`);
2412
2435
  const merged = {
2413
2436
  ...DEFAULT_STORAGE_CONFIG,
2414
2437
  ...fileConfig,
@@ -2819,6 +2842,7 @@ CREATE TABLE IF NOT EXISTS _sync_meta (
2819
2842
  direction TEXT DEFAULT 'push'
2820
2843
  )`;
2821
2844
  var init_storage = __esm(() => {
2845
+ init_mode();
2822
2846
  PgSyncPool = class PgSyncPool {
2823
2847
  worker;
2824
2848
  status;
@@ -2831,12 +2855,12 @@ var init_storage = __esm(() => {
2831
2855
  const ext = import.meta.url.endsWith(".ts") ? ".ts" : ".js";
2832
2856
  const here = fileURLToPath(new URL(".", import.meta.url));
2833
2857
  const candidates = [
2834
- join2(here, `pg-sync-worker${ext}`),
2835
- join2(here, "..", `pg-sync-worker${ext}`),
2836
- join2(here, "..", "..", `pg-sync-worker${ext}`)
2858
+ join(here, `pg-sync-worker${ext}`),
2859
+ join(here, "..", `pg-sync-worker${ext}`),
2860
+ join(here, "..", "..", `pg-sync-worker${ext}`)
2837
2861
  ];
2838
2862
  for (const candidate of candidates) {
2839
- if (existsSync2(candidate))
2863
+ if (existsSync(candidate))
2840
2864
  return candidate;
2841
2865
  }
2842
2866
  return candidates[0];
@@ -2911,7 +2935,7 @@ var init_storage = __esm(() => {
2911
2935
  databaseUrl: "MEMENTOS_DATABASE_URL",
2912
2936
  mode: "MEMENTOS_STORAGE_MODE"
2913
2937
  };
2914
- LOCAL_DATA_DIR = join2(homedir2(), ".hasna", "mementos");
2938
+ LOCAL_DATA_DIR = join(homedir(), ".hasna", "mementos");
2915
2939
  DEFAULT_STORAGE_CONFIG = {
2916
2940
  rds: {
2917
2941
  host: "",
@@ -2927,8 +2951,8 @@ var init_storage = __esm(() => {
2927
2951
  schedule_minutes: 0
2928
2952
  }
2929
2953
  };
2930
- STORAGE_CONFIG_DIR = join2(LOCAL_DATA_DIR, "storage");
2931
- STORAGE_CONFIG_PATH = join2(STORAGE_CONFIG_DIR, "config.json");
2954
+ STORAGE_CONFIG_DIR = join(LOCAL_DATA_DIR, "storage");
2955
+ STORAGE_CONFIG_PATH = join(STORAGE_CONFIG_DIR, "config.json");
2932
2956
  DATABASE_ENV_NAMES = [
2933
2957
  { name: MEMENTOS_STORAGE_ENV.databaseUrl, deprecated: false },
2934
2958
  { name: MEMENTOS_STORAGE_FALLBACK_ENV.databaseUrl, deprecated: false }
@@ -2958,10 +2982,10 @@ var init_storage = __esm(() => {
2958
2982
 
2959
2983
  // src/db/api-mode.ts
2960
2984
  import { tmpdir } from "os";
2961
- import { join as join3 } from "path";
2985
+ import { join as join2 } from "path";
2962
2986
  import { writeFileSync as writeFileSync2, unlinkSync } from "fs";
2963
- import { randomUUID as randomUUID3 } from "crypto";
2964
- function firstEnv(...keys) {
2987
+ import { randomUUID } from "crypto";
2988
+ function firstEnv(keys) {
2965
2989
  for (const k of keys) {
2966
2990
  const v = process.env[k]?.trim();
2967
2991
  if (v)
@@ -2969,8 +2993,45 @@ function firstEnv(...keys) {
2969
2993
  }
2970
2994
  return;
2971
2995
  }
2996
+ function firstEnvKey(keys) {
2997
+ for (const k of keys) {
2998
+ if (process.env[k]?.trim())
2999
+ return k;
3000
+ }
3001
+ return null;
3002
+ }
2972
3003
  function hasDatabaseUrl() {
2973
- return Boolean(firstEnv("HASNA_MEMENTOS_DATABASE_URL", "MEMENTOS_DATABASE_URL"));
3004
+ return Boolean(firstEnv(DATABASE_URL_ENV_KEYS));
3005
+ }
3006
+ function getApiModeEnvSources() {
3007
+ return {
3008
+ urlKey: firstEnvKey(API_URL_ENV_KEYS),
3009
+ keyKey: firstEnvKey(API_KEY_ENV_KEYS),
3010
+ databaseUrlKey: firstEnvKey(DATABASE_URL_ENV_KEYS)
3011
+ };
3012
+ }
3013
+ function isLoopbackHost(rawHost) {
3014
+ const host = rawHost.replace(/^\[/, "").replace(/\]$/, "").toLowerCase();
3015
+ return host === "localhost" || host === "::1" || /^127\./.test(host);
3016
+ }
3017
+ function assertRequestAllowedUnderTest(baseUrl) {
3018
+ if (process.env["NODE_ENV"] !== "test")
3019
+ return;
3020
+ if (process.env[ALLOW_REMOTE_API_IN_TESTS_ENV]?.trim())
3021
+ return;
3022
+ let host;
3023
+ try {
3024
+ host = new URL(baseUrl).hostname;
3025
+ } catch {
3026
+ host = "";
3027
+ }
3028
+ if (host && isLoopbackHost(host))
3029
+ return;
3030
+ throw new Error("api-mode: REFUSING to make a cloud request from a test process \u2014 this would write to or read " + "from the SHARED PRODUCTION memory store, where test fixtures are indistinguishable from real " + `memories.
3031
+ ` + ` host : ${host || "(unparseable base URL)"}
3032
+ ` + ` how this happens: a selector set at module scope (after the bun test preload ran), or \`bun test\` ` + `invoked from a directory with no bunfig.toml so the preload never loaded.
3033
+ ` + " fix : build the child/process env via src/test-support/store-isolation.ts, or point the " + `suite at a loopback stub.
3034
+ ` + ` override : set ${ALLOW_REMOTE_API_IN_TESTS_ENV}=1 only for a test that must reach a remote endpoint.`);
2974
3035
  }
2975
3036
  function normalizeBase(raw) {
2976
3037
  let base = raw.trim().replace(/\/+$/, "");
@@ -2978,9 +3039,24 @@ function normalizeBase(raw) {
2978
3039
  return base;
2979
3040
  return `${base}/v1`;
2980
3041
  }
3042
+ function assertUnambiguousStoreEnv() {
3043
+ if (firstEnvKey(DB_PATH_ENV_KEYS))
3044
+ return;
3045
+ if (hasDatabaseUrl())
3046
+ return;
3047
+ const urlKey = firstEnvKey(API_URL_ENV_KEYS);
3048
+ const keyKey = firstEnvKey(API_KEY_ENV_KEYS);
3049
+ if (urlKey && !keyKey) {
3050
+ throw new MementosStoreConfigError(`${urlKey} points at the cloud memory store but ${API_KEY_ENV_KEYS[0]} is not set. ` + `Refusing to serve the on-box SQLite store in its place, because it holds a different ` + `dataset. Set ${API_KEY_ENV_KEYS[0]} to reach the cloud store. If you meant to use the ` + `on-box SQLite store, unset ${urlKey} or set ${DB_PATH_ENV_KEYS[0]} explicitly.`);
3051
+ }
3052
+ if (keyKey && !urlKey) {
3053
+ throw new MementosStoreConfigError(`${keyKey} is set but ${API_URL_ENV_KEYS[0]} is not, so the cloud memory store cannot be ` + `reached. Refusing to serve the on-box SQLite store in its place, because it holds a ` + `different dataset. Set ${API_URL_ENV_KEYS[0]} to reach the cloud store. If you meant to ` + `use the on-box SQLite store, unset ${keyKey} or set ${DB_PATH_ENV_KEYS[0]} explicitly.`);
3054
+ }
3055
+ }
2981
3056
  function getApiConfig() {
2982
- const rawBase = firstEnv("HASNA_MEMENTOS_API_URL", "MEMENTOS_API_URL");
2983
- const apiKey = firstEnv("HASNA_MEMENTOS_API_KEY", "MEMENTOS_API_KEY");
3057
+ assertUnambiguousStoreEnv();
3058
+ const rawBase = firstEnv(API_URL_ENV_KEYS);
3059
+ const apiKey = firstEnv(API_KEY_ENV_KEYS);
2984
3060
  if (!rawBase || !apiKey)
2985
3061
  return null;
2986
3062
  return { baseUrl: normalizeBase(rawBase), apiKey };
@@ -2994,6 +3070,7 @@ function apiRequestRaw(method, path, body) {
2994
3070
  const cfg = getApiConfig();
2995
3071
  if (!cfg)
2996
3072
  throw new Error("api-mode: not configured (HASNA_MEMENTOS_API_URL / HASNA_MEMENTOS_API_KEY)");
3073
+ assertRequestAllowedUnderTest(cfg.baseUrl);
2997
3074
  const url = `${cfg.baseUrl}${path.startsWith("/") ? path : `/${path}`}`;
2998
3075
  const hasBody = body !== undefined && body !== null;
2999
3076
  const timeout = process.env["HASNA_MEMENTOS_API_TIMEOUT"] || DEFAULT_TIMEOUT_S;
@@ -3019,7 +3096,7 @@ x-api-key: ${cfg.apiKey}
3019
3096
  ];
3020
3097
  let bodyFile;
3021
3098
  if (hasBody) {
3022
- bodyFile = join3(tmpdir(), `mem-req-${process.pid}-${randomUUID3()}.json`);
3099
+ bodyFile = join2(tmpdir(), `mem-req-${process.pid}-${randomUUID()}.json`);
3023
3100
  writeFileSync2(bodyFile, JSON.stringify(body), { mode: 384 });
3024
3101
  args.push("--data-binary", `@${bodyFile}`);
3025
3102
  }
@@ -3060,13 +3137,13 @@ x-api-key: ${cfg.apiKey}
3060
3137
  }
3061
3138
  return { status, body: respBody };
3062
3139
  }
3063
- function apiJson(method, path, body) {
3140
+ function apiJson(method, path, body, options) {
3064
3141
  const raw = apiRequestRaw(method, path, body);
3065
3142
  if (raw.status >= 200 && raw.status < 300) {
3066
3143
  const data = raw.body.trim() ? JSON.parse(raw.body) : undefined;
3067
3144
  return { status: raw.status, data };
3068
3145
  }
3069
- if (raw.status === 404) {
3146
+ if (raw.status === 404 && options?.allow404) {
3070
3147
  return { status: 404, data: undefined };
3071
3148
  }
3072
3149
  let msg = `mementos cloud ${method} ${path} \u2192 ${raw.status}`;
@@ -3098,8 +3175,19 @@ function toQuery(params) {
3098
3175
  const s = sp.toString();
3099
3176
  return s ? `?${s}` : "";
3100
3177
  }
3101
- var ApiRequestError, DEFAULT_TIMEOUT_S = "45";
3178
+ var API_URL_ENV_KEYS, API_KEY_ENV_KEYS, DATABASE_URL_ENV_KEYS, ALLOW_REMOTE_API_IN_TESTS_ENV = "MEMENTOS_ALLOW_REMOTE_API_IN_TESTS", DB_PATH_ENV_KEYS, MementosStoreConfigError, ApiRequestError, DEFAULT_TIMEOUT_S = "45";
3102
3179
  var init_api_mode = __esm(() => {
3180
+ API_URL_ENV_KEYS = ["HASNA_MEMENTOS_API_URL", "MEMENTOS_API_URL"];
3181
+ API_KEY_ENV_KEYS = ["HASNA_MEMENTOS_API_KEY", "MEMENTOS_API_KEY"];
3182
+ DATABASE_URL_ENV_KEYS = ["HASNA_MEMENTOS_DATABASE_URL", "MEMENTOS_DATABASE_URL"];
3183
+ DB_PATH_ENV_KEYS = ["HASNA_MEMENTOS_DB_PATH", "MEMENTOS_DB_PATH"];
3184
+ MementosStoreConfigError = class MementosStoreConfigError extends Error {
3185
+ code = "MEMENTOS_STORE_CONFIG";
3186
+ constructor(message) {
3187
+ super(message);
3188
+ this.name = "MementosStoreConfigError";
3189
+ }
3190
+ };
3103
3191
  ApiRequestError = class ApiRequestError extends Error {
3104
3192
  status;
3105
3193
  body;
@@ -3113,7 +3201,24 @@ var init_api_mode = __esm(() => {
3113
3201
  });
3114
3202
 
3115
3203
  // src/db/migrations.ts
3116
- var MIGRATIONS;
3204
+ var MEMORY_VERSION_SNAPSHOT_TRIGGER = `
3205
+ CREATE TRIGGER IF NOT EXISTS memories_version_snapshot
3206
+ BEFORE UPDATE ON memories
3207
+ WHEN NEW.version > OLD.version
3208
+ BEGIN
3209
+ INSERT OR IGNORE INTO memory_versions (
3210
+ id, memory_id, version, value, importance, scope, category, tags,
3211
+ summary, pinned, status, when_to_use, created_at
3212
+ ) VALUES (
3213
+ lower(hex(randomblob(4))) || '-' || lower(hex(randomblob(2))) || '-' ||
3214
+ lower(hex(randomblob(2))) || '-' || lower(hex(randomblob(2))) || '-' ||
3215
+ lower(hex(randomblob(6))),
3216
+ OLD.id, OLD.version, OLD.value, OLD.importance, OLD.scope, OLD.category,
3217
+ OLD.tags, OLD.summary, OLD.pinned, OLD.status, OLD.when_to_use,
3218
+ OLD.updated_at
3219
+ );
3220
+ END;
3221
+ `, MIGRATIONS;
3117
3222
  var init_migrations = __esm(() => {
3118
3223
  MIGRATIONS = [
3119
3224
  `
@@ -4011,6 +4116,10 @@ CREATE INDEX IF NOT EXISTS idx_memory_reflection_lessons_memory ON memory_reflec
4011
4116
  CREATE INDEX IF NOT EXISTS idx_memory_reflection_lessons_kind ON memory_reflection_lessons(kind);
4012
4117
 
4013
4118
  INSERT OR IGNORE INTO _migrations (id) VALUES (35);
4119
+ `,
4120
+ `
4121
+ ${MEMORY_VERSION_SNAPSHOT_TRIGGER}
4122
+ INSERT OR IGNORE INTO _migrations (id) VALUES (36);
4014
4123
  `
4015
4124
  ];
4016
4125
  });
@@ -4022,13 +4131,14 @@ __export(exports_database, {
4022
4131
  shortUuid: () => shortUuid,
4023
4132
  resolvePartialId: () => resolvePartialId,
4024
4133
  resetDatabase: () => resetDatabase,
4025
- now: () => now2,
4134
+ now: () => now,
4026
4135
  getDbPath: () => getDbPath,
4027
4136
  getDatabase: () => getDatabase,
4137
+ escapeLikePrefix: () => escapeLikePrefix,
4028
4138
  closeDatabase: () => closeDatabase
4029
4139
  });
4030
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, cpSync } from "fs";
4031
- import { dirname, join as join4, resolve } from "path";
4140
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, cpSync } from "fs";
4141
+ import { dirname, join as join3, resolve } from "path";
4032
4142
  function isInMemoryDb(path) {
4033
4143
  return path === ":memory:" || path.startsWith("file::memory:");
4034
4144
  }
@@ -4037,8 +4147,8 @@ function findNearestMementosDb(startDir) {
4037
4147
  const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
4038
4148
  const legacyHomeDb = resolve(home, ".mementos", "mementos.db");
4039
4149
  while (true) {
4040
- const candidate = join4(dir, ".mementos", "mementos.db");
4041
- if (existsSync3(candidate) && resolve(candidate) !== legacyHomeDb)
4150
+ const candidate = join3(dir, ".mementos", "mementos.db");
4151
+ if (existsSync2(candidate) && resolve(candidate) !== legacyHomeDb)
4042
4152
  return candidate;
4043
4153
  const parent = dirname(dir);
4044
4154
  if (parent === dir)
@@ -4050,7 +4160,7 @@ function findNearestMementosDb(startDir) {
4050
4160
  function findGitRoot(startDir) {
4051
4161
  let dir = resolve(startDir);
4052
4162
  while (true) {
4053
- if (existsSync3(join4(dir, ".git")))
4163
+ if (existsSync2(join3(dir, ".git")))
4054
4164
  return dir;
4055
4165
  const parent = dirname(dir);
4056
4166
  if (parent === dir)
@@ -4061,10 +4171,10 @@ function findGitRoot(startDir) {
4061
4171
  }
4062
4172
  function migrateGlobalDir() {
4063
4173
  const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
4064
- const newDir = join4(home, ".hasna", "mementos");
4065
- const oldDir = join4(home, ".mementos");
4066
- if (!existsSync3(newDir) && existsSync3(oldDir)) {
4067
- mkdirSync2(join4(home, ".hasna"), { recursive: true });
4174
+ const newDir = join3(home, ".hasna", "mementos");
4175
+ const oldDir = join3(home, ".mementos");
4176
+ if (!existsSync2(newDir) && existsSync2(oldDir)) {
4177
+ mkdirSync2(join3(home, ".hasna"), { recursive: true });
4068
4178
  cpSync(oldDir, newDir, { recursive: true });
4069
4179
  }
4070
4180
  }
@@ -4080,18 +4190,18 @@ function getDbPath() {
4080
4190
  if (process.env["MEMENTOS_DB_SCOPE"] === "project") {
4081
4191
  const gitRoot = findGitRoot(cwd);
4082
4192
  if (gitRoot) {
4083
- return join4(gitRoot, ".mementos", "mementos.db");
4193
+ return join3(gitRoot, ".mementos", "mementos.db");
4084
4194
  }
4085
4195
  }
4086
4196
  migrateGlobalDir();
4087
4197
  const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
4088
- return join4(home, ".hasna", "mementos", "mementos.db");
4198
+ return join3(home, ".hasna", "mementos", "mementos.db");
4089
4199
  }
4090
4200
  function ensureDir(filePath) {
4091
4201
  if (isInMemoryDb(filePath))
4092
4202
  return;
4093
4203
  const dir = dirname(resolve(filePath));
4094
- if (!existsSync3(dir)) {
4204
+ if (!existsSync2(dir)) {
4095
4205
  mkdirSync2(dir, { recursive: true });
4096
4206
  }
4097
4207
  }
@@ -4190,7 +4300,7 @@ function resetDatabase() {
4190
4300
  _db = null;
4191
4301
  _pg = null;
4192
4302
  }
4193
- function now2() {
4303
+ function now() {
4194
4304
  return new Date().toISOString();
4195
4305
  }
4196
4306
  function uuid() {
@@ -4199,15 +4309,20 @@ function uuid() {
4199
4309
  function shortUuid() {
4200
4310
  return crypto.randomUUID().slice(0, 8);
4201
4311
  }
4312
+ function escapeLikePrefix(s) {
4313
+ return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
4314
+ }
4202
4315
  function resolvePartialId(db, table, partialId) {
4203
4316
  if (!ALLOWED_TABLES.has(table)) {
4204
4317
  throw new Error(`Invalid table name: ${table}`);
4205
4318
  }
4319
+ if (partialId === "")
4320
+ return null;
4206
4321
  if (partialId.length >= 36) {
4207
4322
  const row = db.query(`SELECT id FROM ${table} WHERE id = ?`).get(partialId);
4208
4323
  return row?.id ?? null;
4209
4324
  }
4210
- const rows = db.query(`SELECT id FROM ${table} WHERE id LIKE ?`).all(`${partialId}%`);
4325
+ const rows = db.query(`SELECT id FROM ${table} WHERE id LIKE ? ESCAPE '\\'`).all(`${escapeLikePrefix(partialId)}%`);
4211
4326
  if (rows.length === 1) {
4212
4327
  return rows[0].id;
4213
4328
  }
@@ -4236,8 +4351,19 @@ var init_database = __esm(() => {
4236
4351
  });
4237
4352
 
4238
4353
  // src/types/index.ts
4239
- var AgentConflictError, EntityNotFoundError, MemoryNotFoundError, VersionConflictError, MemoryConflictError;
4354
+ var MEMORY_SCOPES, MEMORY_CATEGORIES, MEMORY_SOURCES, MEMORY_STATUSES, AgentConflictError, EntityNotFoundError, MemoryNotFoundError, VersionConflictError, MemoryConflictError;
4240
4355
  var init_types = __esm(() => {
4356
+ MEMORY_SCOPES = ["global", "shared", "private", "working"];
4357
+ MEMORY_CATEGORIES = [
4358
+ "preference",
4359
+ "fact",
4360
+ "knowledge",
4361
+ "history",
4362
+ "procedural",
4363
+ "resource"
4364
+ ];
4365
+ MEMORY_SOURCES = ["user", "agent", "system", "auto", "imported"];
4366
+ MEMORY_STATUSES = ["active", "archived", "expired"];
4241
4367
  AgentConflictError = class AgentConflictError extends Error {
4242
4368
  conflict = true;
4243
4369
  existing_id;
@@ -4394,6 +4520,41 @@ var init_redact = __esm(() => {
4394
4520
  ];
4395
4521
  });
4396
4522
 
4523
+ // src/lib/enum-validation.ts
4524
+ function formatEnumViolation(v) {
4525
+ return `Invalid ${v.field}: "${v.value}". Allowed values: ${v.allowed.join(", ")}.`;
4526
+ }
4527
+ function validateEnumField(field, value) {
4528
+ const allowed = ENUM_FIELDS[field];
4529
+ if (!allowed)
4530
+ return null;
4531
+ if (value === undefined || value === null || value === "")
4532
+ return null;
4533
+ if (typeof value === "string" && allowed.includes(value))
4534
+ return null;
4535
+ return { field, value: String(value), allowed };
4536
+ }
4537
+ function validateMemoryEnums(input) {
4538
+ for (const field of Object.keys(ENUM_FIELDS)) {
4539
+ if (!(field in input))
4540
+ continue;
4541
+ const violation = validateEnumField(field, input[field]);
4542
+ if (violation)
4543
+ return violation;
4544
+ }
4545
+ return null;
4546
+ }
4547
+ var ENUM_FIELDS;
4548
+ var init_enum_validation = __esm(() => {
4549
+ init_types();
4550
+ ENUM_FIELDS = {
4551
+ category: MEMORY_CATEGORIES,
4552
+ scope: MEMORY_SCOPES,
4553
+ source: MEMORY_SOURCES,
4554
+ status: MEMORY_STATUSES
4555
+ };
4556
+ });
4557
+
4397
4558
  // src/lib/hooks.ts
4398
4559
  var exports_hooks = {};
4399
4560
  __export(exports_hooks, {
@@ -4565,7 +4726,7 @@ function linkEntityToMemory(entityId, memoryId, role = "context", db) {
4565
4726
  return data;
4566
4727
  }
4567
4728
  const d = db || getDatabase();
4568
- const timestamp = now2();
4729
+ const timestamp = now();
4569
4730
  d.run(`INSERT OR IGNORE INTO entity_memories (entity_id, memory_id, role, created_at)
4570
4731
  VALUES (?, ?, ?, ?)`, [entityId, memoryId, role, timestamp]);
4571
4732
  const row = d.query("SELECT * FROM entity_memories WHERE entity_id = ? AND memory_id = ?").get(entityId, memoryId);
@@ -4691,11 +4852,14 @@ function parseMemoryRow(row) {
4691
4852
  }
4692
4853
  function createMemory(input, dedupeMode = "merge", db) {
4693
4854
  if (!db && isApiMode()) {
4694
- const { data } = apiJson("POST", "/memories", { ...input, dedupe: dedupeMode });
4855
+ const { status, data } = apiJson("POST", "/memories", { ...input, dedupe: dedupeMode });
4856
+ if (!data || !data.id) {
4857
+ throw new ApiRequestError(`mementos cloud POST /memories \u2192 ${status} but no memory was returned; the write did not persist (key: ${input.key})`, status, "");
4858
+ }
4695
4859
  return data;
4696
4860
  }
4697
4861
  const d = db || getDatabase();
4698
- const timestamp = now2();
4862
+ const timestamp = now();
4699
4863
  if (input.project_id) {
4700
4864
  const resolved = resolvePartialId(d, "projects", input.project_id);
4701
4865
  if (resolved) {
@@ -4830,19 +4994,28 @@ function bulkUpsertMemories(memories, db) {
4830
4994
  const d = db || getDatabase();
4831
4995
  let inserted = 0;
4832
4996
  let skipped = 0;
4997
+ let rejected = 0;
4833
4998
  const errors = [];
4834
- const insert = d.prepare(`INSERT OR IGNORE INTO memories (id, key, value, category, scope, summary, tags, importance, source, status, pinned, agent_id, project_id, session_id, machine_id, namespace, created_by_agent, when_to_use, sequence_group, sequence_order, metadata, access_count, version, expires_at, valid_from, valid_until, ingested_at, created_at, updated_at)
4835
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
4999
+ const insert = d.prepare(`INSERT INTO memories (id, key, value, category, scope, summary, tags, importance, source, status, pinned, agent_id, project_id, session_id, machine_id, namespace, created_by_agent, when_to_use, sequence_group, sequence_order, metadata, access_count, version, expires_at, valid_from, valid_until, ingested_at, created_at, updated_at)
5000
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
5001
+ ON CONFLICT DO NOTHING`);
4836
5002
  const insertTag = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
4837
5003
  for (const mem of memories) {
4838
5004
  const key = mem["key"];
4839
5005
  const id = mem["id"] || uuid();
4840
5006
  try {
4841
5007
  if (!key) {
4842
- errors.push(`skipped row without key (id=${id})`);
5008
+ rejected++;
5009
+ errors.push(`rejected row without key (id=${id})`);
5010
+ continue;
5011
+ }
5012
+ const violation = validateMemoryEnums(mem);
5013
+ if (violation) {
5014
+ rejected++;
5015
+ errors.push(`Rejected "${key}": ${formatEnumViolation(violation)}`);
4843
5016
  continue;
4844
5017
  }
4845
- const timestamp = now2();
5018
+ const timestamp = now();
4846
5019
  let tags = [];
4847
5020
  const rawTags = mem["tags"];
4848
5021
  if (Array.isArray(rawTags)) {
@@ -4888,13 +5061,14 @@ function bulkUpsertMemories(memories, db) {
4888
5061
  skipped++;
4889
5062
  }
4890
5063
  } catch (e) {
5064
+ rejected++;
4891
5065
  errors.push(`Failed "${String(key)}": ${e instanceof Error ? e.message : String(e)}`);
4892
5066
  }
4893
5067
  }
4894
- return { inserted, skipped, errors, total: memories.length };
5068
+ return { inserted, skipped, rejected, errors, total: memories.length };
4895
5069
  }
4896
5070
  function ensureMemoryReferences(d, input) {
4897
- const t = now2();
5071
+ const t = now();
4898
5072
  const tryRun = (sql, params) => {
4899
5073
  try {
4900
5074
  d.run(sql, params);
@@ -4919,7 +5093,7 @@ function listMemoriesByKey(key, db) {
4919
5093
  }
4920
5094
  function getMemory(id, db) {
4921
5095
  if (!db && isApiMode()) {
4922
- const { status, data } = apiJson("GET", `/memories/${encodeURIComponent(id)}`);
5096
+ const { status, data } = apiJson("GET", `/memories/${encodeURIComponent(id)}`, undefined, { allow404: true });
4923
5097
  return status === 404 ? null : data ?? null;
4924
5098
  }
4925
5099
  const d = db || getDatabase();
@@ -5252,38 +5426,24 @@ function getMemoryEmbeddings(ids, db) {
5252
5426
  }
5253
5427
  function updateMemory(id, input, db) {
5254
5428
  if (!db && isApiMode()) {
5255
- const { status, data } = apiJson("PATCH", `/memories/${encodeURIComponent(id)}`, input);
5429
+ const { status, data } = apiJson("PATCH", `/memories/${encodeURIComponent(id)}`, input, { allow404: true });
5256
5430
  if (status === 404)
5257
5431
  throw new MemoryNotFoundError(id);
5432
+ if (data && typeof input.version === "number" && typeof data.version === "number" && data.version <= input.version) {
5433
+ 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.`);
5434
+ }
5258
5435
  return data;
5259
5436
  }
5260
5437
  const d = db || getDatabase();
5261
5438
  const existing = getMemory(id, d);
5262
5439
  if (!existing)
5263
5440
  throw new MemoryNotFoundError(id);
5441
+ const memoryId = existing.id;
5264
5442
  if (existing.version !== input.version) {
5265
5443
  throw new VersionConflictError(id, input.version, existing.version);
5266
5444
  }
5267
- try {
5268
- 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)
5269
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
5270
- uuid(),
5271
- existing.id,
5272
- existing.version,
5273
- existing.value,
5274
- existing.importance,
5275
- existing.scope,
5276
- existing.category,
5277
- JSON.stringify(existing.tags),
5278
- existing.summary,
5279
- existing.pinned ? 1 : 0,
5280
- existing.status,
5281
- existing.when_to_use || null,
5282
- existing.updated_at
5283
- ]);
5284
- } catch {}
5285
5445
  const sets = ["version = version + 1", "updated_at = ?"];
5286
- const params = [now2()];
5446
+ const params = [now()];
5287
5447
  if (input.value !== undefined) {
5288
5448
  sets.push("value = ?");
5289
5449
  params.push(redactSecrets(input.value));
@@ -5331,15 +5491,18 @@ function updateMemory(id, input, db) {
5331
5491
  if (input.tags !== undefined) {
5332
5492
  sets.push("tags = ?");
5333
5493
  params.push(JSON.stringify(input.tags));
5334
- d.run("DELETE FROM memory_tags WHERE memory_id = ?", [id]);
5494
+ d.run("DELETE FROM memory_tags WHERE memory_id = ?", [memoryId]);
5335
5495
  const insertTag = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
5336
5496
  for (const tag of input.tags) {
5337
- insertTag.run(id, tag);
5497
+ insertTag.run(memoryId, tag);
5338
5498
  }
5339
5499
  }
5340
- params.push(id);
5341
- d.run(`UPDATE memories SET ${sets.join(", ")} WHERE id = ?`, params);
5342
- const updated = getMemory(id, d);
5500
+ params.push(memoryId);
5501
+ const result = d.run(`UPDATE memories SET ${sets.join(", ")} WHERE id = ?`, params);
5502
+ if (result.changes === 0) {
5503
+ 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.`);
5504
+ }
5505
+ const updated = getMemory(memoryId, d);
5343
5506
  if (input.value !== undefined) {
5344
5507
  try {
5345
5508
  const oldLinks = getEntityMemoryLinks(undefined, updated.id, d);
@@ -5360,14 +5523,15 @@ function updateMemory(id, input, db) {
5360
5523
  }
5361
5524
  function deleteMemory(id, db) {
5362
5525
  if (!db && isApiMode()) {
5363
- const { status } = apiJson("DELETE", `/memories/${encodeURIComponent(id)}`);
5526
+ const { status } = apiJson("DELETE", `/memories/${encodeURIComponent(id)}`, undefined, { allow404: true });
5364
5527
  return status !== 404;
5365
5528
  }
5366
5529
  const d = db || getDatabase();
5367
- const result = d.run("DELETE FROM memories WHERE id = ?", [id]);
5530
+ const memoryId = resolvePartialId(d, "memories", id) ?? id;
5531
+ const result = d.run("DELETE FROM memories WHERE id = ?", [memoryId]);
5368
5532
  if (result.changes > 0) {
5369
5533
  hookRegistry.runHooks("PostMemoryDelete", {
5370
- memoryId: id,
5534
+ memoryId,
5371
5535
  timestamp: Date.now()
5372
5536
  });
5373
5537
  }
@@ -5381,11 +5545,12 @@ function bulkDeleteMemories(ids, db) {
5381
5545
  return data?.deleted ?? 0;
5382
5546
  }
5383
5547
  const d = db || getDatabase();
5384
- const placeholders = ids.map(() => "?").join(",");
5385
- const countRow = d.query(`SELECT COUNT(*) as c FROM memories WHERE id IN (${placeholders})`).get(...ids);
5548
+ const resolvedIds = ids.map((id) => resolvePartialId(d, "memories", id) ?? id);
5549
+ const placeholders = resolvedIds.map(() => "?").join(",");
5550
+ const countRow = d.query(`SELECT COUNT(*) as c FROM memories WHERE id IN (${placeholders})`).get(...resolvedIds);
5386
5551
  const count = countRow.c;
5387
5552
  if (count > 0) {
5388
- d.run(`DELETE FROM memories WHERE id IN (${placeholders})`, ids);
5553
+ d.run(`DELETE FROM memories WHERE id IN (${placeholders})`, resolvedIds);
5389
5554
  }
5390
5555
  return count;
5391
5556
  }
@@ -5393,14 +5558,14 @@ function touchMemory(id, db) {
5393
5558
  if (!db && isApiMode())
5394
5559
  return;
5395
5560
  const d = db || getDatabase();
5396
- d.run("UPDATE memories SET access_count = access_count + 1, accessed_at = ? WHERE id = ?", [now2(), id]);
5561
+ d.run("UPDATE memories SET access_count = access_count + 1, accessed_at = ? WHERE id = ?", [now(), id]);
5397
5562
  }
5398
5563
  function incrementRecallCount(id, db) {
5399
5564
  if (!db && isApiMode())
5400
5565
  return;
5401
5566
  const d = db || getDatabase();
5402
5567
  try {
5403
- d.run("UPDATE memories SET recall_count = recall_count + 1, access_count = access_count + 1, accessed_at = ? WHERE id = ?", [now2(), id]);
5568
+ d.run("UPDATE memories SET recall_count = recall_count + 1, access_count = access_count + 1, accessed_at = ? WHERE id = ?", [now(), id]);
5404
5569
  const row = d.query("SELECT recall_count, importance FROM memories WHERE id = ?").get(id);
5405
5570
  if (!row)
5406
5571
  return;
@@ -5417,7 +5582,7 @@ function cleanExpiredMemories(db) {
5417
5582
  return data?.cleaned ?? 0;
5418
5583
  }
5419
5584
  const d = db || getDatabase();
5420
- const timestamp = now2();
5585
+ const timestamp = now();
5421
5586
  const countRow = d.query("SELECT COUNT(*) as c FROM memories WHERE expires_at IS NOT NULL AND expires_at < ?").get(timestamp);
5422
5587
  const count = countRow.c;
5423
5588
  if (count > 0) {
@@ -5512,6 +5677,7 @@ var init_memories = __esm(() => {
5512
5677
  init_types();
5513
5678
  init_database();
5514
5679
  init_redact();
5680
+ init_enum_validation();
5515
5681
  init_hooks();
5516
5682
  init_poisoning();
5517
5683
  init_entity_memories();
@@ -5547,7 +5713,7 @@ function registerProject(name, path, description, memoryPrefix, db) {
5547
5713
  return data;
5548
5714
  }
5549
5715
  const d = db || getDatabase();
5550
- const timestamp = now2();
5716
+ const timestamp = now();
5551
5717
  const existing = d.query("SELECT * FROM projects WHERE path = ?").get(path);
5552
5718
  if (existing) {
5553
5719
  const existingId = existing["id"];
@@ -5563,7 +5729,7 @@ function registerProject(name, path, description, memoryPrefix, db) {
5563
5729
  }
5564
5730
  function getProject(idOrPath, db) {
5565
5731
  if (!db && isApiMode()) {
5566
- const { status, data } = apiJson("GET", `/projects/${encodeURIComponent(idOrPath)}`);
5732
+ const { status, data } = apiJson("GET", `/projects/${encodeURIComponent(idOrPath)}`, undefined, { allow404: true });
5567
5733
  if (status === 404 || !data)
5568
5734
  return null;
5569
5735
  return data;
@@ -5613,7 +5779,7 @@ function createEntity(input, db) {
5613
5779
  return data;
5614
5780
  }
5615
5781
  const d = db || getDatabase();
5616
- const timestamp = now2();
5782
+ const timestamp = now();
5617
5783
  const metadataJson = JSON.stringify(input.metadata || {});
5618
5784
  const existing = d.query(`SELECT * FROM entities
5619
5785
  WHERE name = ? AND type = ? AND COALESCE(project_id, '') = ?`).get(input.name, input.type, input.project_id || "");
@@ -5656,7 +5822,7 @@ function createEntity(input, db) {
5656
5822
  }
5657
5823
  function getEntity(id, db) {
5658
5824
  if (!db && isApiMode()) {
5659
- const { status, data } = apiJson("GET", `/entities/${encodeURIComponent(id)}`);
5825
+ const { status, data } = apiJson("GET", `/entities/${encodeURIComponent(id)}`, undefined, { allow404: true });
5660
5826
  if (status === 404 || !data)
5661
5827
  throw new EntityNotFoundError(id);
5662
5828
  return data;
@@ -5739,7 +5905,7 @@ function listEntities(filter = {}, db) {
5739
5905
  }
5740
5906
  function deleteEntity(id, db) {
5741
5907
  if (!db && isApiMode()) {
5742
- const { status } = apiJson("DELETE", `/entities/${encodeURIComponent(id)}`);
5908
+ const { status } = apiJson("DELETE", `/entities/${encodeURIComponent(id)}`, undefined, { allow404: true });
5743
5909
  if (status === 404)
5744
5910
  throw new EntityNotFoundError(id);
5745
5911
  return;
@@ -5789,7 +5955,7 @@ function mergeEntities(sourceId, targetId, db) {
5789
5955
  d.run(`UPDATE entity_memories SET entity_id = ? WHERE entity_id = ?`, [tgt, src]);
5790
5956
  d.run("DELETE FROM entity_memories WHERE entity_id = ?", [src]);
5791
5957
  d.run("DELETE FROM entities WHERE id = ?", [src]);
5792
- d.run("UPDATE entities SET updated_at = ? WHERE id = ?", [now2(), tgt]);
5958
+ d.run("UPDATE entities SET updated_at = ? WHERE id = ?", [now(), tgt]);
5793
5959
  return getEntity(tgt, d);
5794
5960
  }
5795
5961
  var init_entities = __esm(() => {
@@ -6454,7 +6620,7 @@ function registerAgent(name, sessionId, description, role, projectId, db) {
6454
6620
  return data;
6455
6621
  }
6456
6622
  const d = db || getDatabase();
6457
- const timestamp = now2();
6623
+ const timestamp = now();
6458
6624
  const normalizedName = name.trim().toLowerCase();
6459
6625
  if (projectId) {
6460
6626
  const resolvedProjectId = resolvePartialId(d, "projects", projectId);
@@ -6503,7 +6669,7 @@ function registerAgent(name, sessionId, description, role, projectId, db) {
6503
6669
  }
6504
6670
  function getAgent(idOrName, db) {
6505
6671
  if (!db && isApiMode()) {
6506
- const { status, data } = apiJson("GET", `/agents/${encodeURIComponent(idOrName)}`);
6672
+ const { status, data } = apiJson("GET", `/agents/${encodeURIComponent(idOrName)}`, undefined, { allow404: true });
6507
6673
  if (status === 404 || !data)
6508
6674
  return null;
6509
6675
  return data;
@@ -6515,7 +6681,7 @@ function getAgent(idOrName, db) {
6515
6681
  row = d.query("SELECT * FROM agents WHERE LOWER(name) = ?").get(idOrName.trim().toLowerCase());
6516
6682
  if (row)
6517
6683
  return parseAgentRow(row);
6518
- const rows = d.query("SELECT * FROM agents WHERE id LIKE ?").all(`${idOrName}%`);
6684
+ const rows = d.query("SELECT * FROM agents WHERE id LIKE ? ESCAPE '\\'").all(`${escapeLikePrefix(idOrName)}%`);
6519
6685
  if (rows.length === 1)
6520
6686
  return parseAgentRow(rows[0]);
6521
6687
  return null;
@@ -6541,7 +6707,7 @@ function touchAgent(idOrName, db) {
6541
6707
  const agent = getAgent(idOrName, d);
6542
6708
  if (!agent)
6543
6709
  return;
6544
- d.run("UPDATE agents SET last_seen_at = ? WHERE id = ?", [now2(), agent.id]);
6710
+ d.run("UPDATE agents SET last_seen_at = ? WHERE id = ?", [now(), agent.id]);
6545
6711
  }
6546
6712
  function listAgentsByProject(projectId, db) {
6547
6713
  if (!db && isApiMode()) {
@@ -6556,7 +6722,7 @@ function listAgentsByProject(projectId, db) {
6556
6722
  }
6557
6723
  function updateAgent(id, updates, db) {
6558
6724
  if (!db && isApiMode()) {
6559
- const { status, data } = apiJson("PATCH", `/agents/${encodeURIComponent(id)}`, updates);
6725
+ const { status, data } = apiJson("PATCH", `/agents/${encodeURIComponent(id)}`, updates, { allow404: true });
6560
6726
  if (status === 404 || !data)
6561
6727
  return null;
6562
6728
  return data;
@@ -6565,7 +6731,7 @@ function updateAgent(id, updates, db) {
6565
6731
  const agent = getAgent(id, d);
6566
6732
  if (!agent)
6567
6733
  return null;
6568
- const timestamp = now2();
6734
+ const timestamp = now();
6569
6735
  if (updates.name) {
6570
6736
  const normalizedNewName = updates.name.trim().toLowerCase();
6571
6737
  if (normalizedNewName !== agent.name) {
@@ -7402,7 +7568,7 @@ function createRelation(input, db) {
7402
7568
  }
7403
7569
  const d = db || getDatabase();
7404
7570
  const id = shortUuid();
7405
- const timestamp = now2();
7571
+ const timestamp = now();
7406
7572
  const weight = input.weight ?? 1;
7407
7573
  const metadata = JSON.stringify(input.metadata ?? {});
7408
7574
  d.run(`INSERT INTO relations (id, source_entity_id, target_entity_id, relation_type, weight, metadata, created_at)
@@ -7455,7 +7621,7 @@ function listRelations(filter, db) {
7455
7621
  }
7456
7622
  function deleteRelation(id, db) {
7457
7623
  if (!db && isApiMode()) {
7458
- const { status } = apiJson("DELETE", `/relations/${encodeURIComponent(id)}`);
7624
+ const { status } = apiJson("DELETE", `/relations/${encodeURIComponent(id)}`, undefined, { allow404: true });
7459
7625
  if (status === 404)
7460
7626
  throw new Error(`Relation not found: ${id}`);
7461
7627
  return;
@@ -8109,6 +8275,32 @@ class AutoMemoryQueue {
8109
8275
  getStats() {
8110
8276
  return { ...this.stats, pending: this.queue.length };
8111
8277
  }
8278
+ async waitForIdleForTests(timeoutMs = 3000) {
8279
+ const start = Date.now();
8280
+ while (Date.now() - start < timeoutMs) {
8281
+ if (this.queue.length === 0 && this.activeCount === 0)
8282
+ return;
8283
+ await new Promise((r) => setTimeout(r, 20));
8284
+ }
8285
+ throw new Error("autoMemoryQueue did not become idle before test reset");
8286
+ }
8287
+ resetForTests(handler) {
8288
+ if (this.activeCount !== 0) {
8289
+ throw new Error("Cannot reset autoMemoryQueue while jobs are processing");
8290
+ }
8291
+ this.queue = [];
8292
+ this.running = false;
8293
+ this.stats = {
8294
+ pending: 0,
8295
+ processing: 0,
8296
+ processed: 0,
8297
+ failed: 0,
8298
+ dropped: 0
8299
+ };
8300
+ if (handler !== undefined) {
8301
+ this.handler = handler;
8302
+ }
8303
+ }
8112
8304
  startLoop() {
8113
8305
  this.running = true;
8114
8306
  this.loop();
@@ -8154,6 +8346,7 @@ var init_auto_memory_queue = __esm(() => {
8154
8346
  // src/lib/auto-memory.ts
8155
8347
  var exports_auto_memory = {};
8156
8348
  __export(exports_auto_memory, {
8349
+ resetAutoMemoryForTests: () => resetAutoMemoryForTests,
8157
8350
  processConversationTurn: () => processConversationTurn,
8158
8351
  getAutoMemoryStats: () => getAutoMemoryStats,
8159
8352
  configureAutoMemory: () => configureAutoMemory
@@ -8314,6 +8507,12 @@ function getAutoMemoryStats() {
8314
8507
  function configureAutoMemory(config) {
8315
8508
  providerRegistry.configure(config);
8316
8509
  }
8510
+ async function resetAutoMemoryForTests() {
8511
+ if (autoMemoryQueue.getStats().processing > 0) {
8512
+ await autoMemoryQueue.waitForIdleForTests();
8513
+ }
8514
+ autoMemoryQueue.resetForTests(processJob);
8515
+ }
8317
8516
  var DEDUP_SIMILARITY_THRESHOLD = 0.85;
8318
8517
  var init_auto_memory = __esm(() => {
8319
8518
  init_memories();
@@ -8367,7 +8566,7 @@ function createWebhookHook(input, db) {
8367
8566
  }
8368
8567
  const d = db || getDatabase();
8369
8568
  const id = shortUuid();
8370
- const timestamp = now2();
8569
+ const timestamp = now();
8371
8570
  d.run(`INSERT INTO webhook_hooks
8372
8571
  (id, type, handler_url, priority, blocking, agent_id, project_id, description, enabled, created_at, invocation_count, failure_count)
8373
8572
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1, ?, 0, 0)`, [
@@ -8385,7 +8584,7 @@ function createWebhookHook(input, db) {
8385
8584
  }
8386
8585
  function getWebhookHook(id, db) {
8387
8586
  if (!db && isApiMode()) {
8388
- const { status, data } = apiJson("GET", `/webhooks/${encodeURIComponent(id)}`);
8587
+ const { status, data } = apiJson("GET", `/webhooks/${encodeURIComponent(id)}`, undefined, { allow404: true });
8389
8588
  if (status === 404 || !data)
8390
8589
  return null;
8391
8590
  return data;
@@ -8421,7 +8620,7 @@ function updateWebhookHook(id, updates, db) {
8421
8620
  enabled: updates.enabled,
8422
8621
  priority: updates.priority,
8423
8622
  description: updates.description
8424
- });
8623
+ }, { allow404: true });
8425
8624
  if (status === 404 || !data)
8426
8625
  return null;
8427
8626
  return data;
@@ -8452,7 +8651,7 @@ function updateWebhookHook(id, updates, db) {
8452
8651
  }
8453
8652
  function deleteWebhookHook(id, db) {
8454
8653
  if (!db && isApiMode()) {
8455
- const { status } = apiJson("DELETE", `/webhooks/${encodeURIComponent(id)}`);
8654
+ const { status } = apiJson("DELETE", `/webhooks/${encodeURIComponent(id)}`, undefined, { allow404: true });
8456
8655
  return status === 204 || status === 200;
8457
8656
  }
8458
8657
  const d = db || getDatabase();
@@ -8550,7 +8749,7 @@ function parseEventRow(row) {
8550
8749
  function createSynthesisRun(input, db) {
8551
8750
  const d = db || getDatabase();
8552
8751
  const id = shortUuid();
8553
- const timestamp = now2();
8752
+ const timestamp = now();
8554
8753
  d.run(`INSERT INTO synthesis_runs (id, triggered_by, project_id, agent_id, corpus_size, proposals_generated, proposals_accepted, proposals_rejected, status, started_at)
8555
8754
  VALUES (?, ?, ?, ?, ?, 0, 0, 0, 'pending', ?)`, [
8556
8755
  id,
@@ -8651,7 +8850,7 @@ function updateSynthesisRun(id, updates, db) {
8651
8850
  function createProposal(input, db) {
8652
8851
  const d = db || getDatabase();
8653
8852
  const id = shortUuid();
8654
- const timestamp = now2();
8853
+ const timestamp = now();
8655
8854
  d.run(`INSERT INTO synthesis_proposals (id, run_id, proposal_type, memory_ids, target_memory_id, proposed_changes, reasoning, confidence, status, created_at)
8656
8855
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', ?)`, [
8657
8856
  id,
@@ -8710,7 +8909,7 @@ function updateProposal(id, updates, db) {
8710
8909
  function createMetric(input, db) {
8711
8910
  const d = db || getDatabase();
8712
8911
  const id = shortUuid();
8713
- const timestamp = now2();
8912
+ const timestamp = now();
8714
8913
  d.run(`INSERT INTO synthesis_metrics (id, run_id, metric_type, value, baseline, created_at)
8715
8914
  VALUES (?, ?, ?, ?, ?, ?)`, [id, input.run_id, input.metric_type, input.value, input.baseline ?? null, timestamp]);
8716
8915
  return { id, run_id: input.run_id, metric_type: input.metric_type, value: input.value, baseline: input.baseline ?? null, created_at: timestamp };
@@ -8724,7 +8923,7 @@ function recordSynthesisEvent(input, db) {
8724
8923
  try {
8725
8924
  const d = db || getDatabase();
8726
8925
  const id = shortUuid();
8727
- const timestamp = now2();
8926
+ const timestamp = now();
8728
8927
  d.run(`INSERT INTO synthesis_events (id, event_type, memory_id, agent_id, project_id, session_id, query, importance_at_time, metadata, created_at)
8729
8928
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
8730
8929
  id,
@@ -8978,7 +9177,7 @@ __export(exports_contradiction, {
8978
9177
  });
8979
9178
  function invalidateFact(oldMemoryId, newMemoryId, db) {
8980
9179
  const d = db || getDatabase();
8981
- const timestamp = now2();
9180
+ const timestamp = now();
8982
9181
  d.run("UPDATE memories SET valid_until = ?, updated_at = ? WHERE id = ?", [timestamp, timestamp, oldMemoryId]);
8983
9182
  if (newMemoryId) {
8984
9183
  const row = d.query("SELECT metadata FROM memories WHERE id = ?").get(newMemoryId);
@@ -9151,6 +9350,8 @@ var init_built_in_hooks = __esm(() => {
9151
9350
  priority: 100,
9152
9351
  description: "Trigger async LLM entity extraction when a memory is saved",
9153
9352
  handler: async (ctx) => {
9353
+ if (process.env["NODE_ENV"] === "test")
9354
+ return;
9154
9355
  if (ctx.wasUpdated)
9155
9356
  return;
9156
9357
  const processConversationTurn2 = await getAutoMemory();
@@ -9456,7 +9657,7 @@ async function buildCorpus(options) {
9456
9657
  duplicateCandidates,
9457
9658
  lowImportanceHighRecall,
9458
9659
  highImportanceLowRecall,
9459
- generatedAt: now2()
9660
+ generatedAt: now()
9460
9661
  };
9461
9662
  }
9462
9663
  var init_corpus_builder = __esm(() => {
@@ -9789,7 +9990,7 @@ async function executeProposals(runId, proposals, db) {
9789
9990
  const rollback = executeProposal(proposal, d);
9790
9991
  updateProposal(proposal.id, {
9791
9992
  status: "accepted",
9792
- executed_at: now2(),
9993
+ executed_at: now(),
9793
9994
  rollback_data: rollback
9794
9995
  }, d);
9795
9996
  rollbackData[proposal.id] = rollback;
@@ -9838,7 +10039,7 @@ function executeArchive(proposal, d) {
9838
10039
  if (!mem)
9839
10040
  continue;
9840
10041
  rollback[memId] = mem.status;
9841
- d.run("UPDATE memories SET status = 'archived', updated_at = ? WHERE id = ?", [now2(), memId]);
10042
+ d.run("UPDATE memories SET status = 'archived', updated_at = ? WHERE id = ?", [now(), memId]);
9842
10043
  }
9843
10044
  return { old_status: rollback };
9844
10045
  }
@@ -9853,7 +10054,7 @@ function executePromote(proposal, d) {
9853
10054
  if (!mem)
9854
10055
  continue;
9855
10056
  rollback[memId] = mem.importance;
9856
- d.run("UPDATE memories SET importance = ?, updated_at = ? WHERE id = ?", [Math.max(1, Math.min(10, Math.round(newImportance))), now2(), memId]);
10057
+ d.run("UPDATE memories SET importance = ?, updated_at = ? WHERE id = ?", [Math.max(1, Math.min(10, Math.round(newImportance))), now(), memId]);
9857
10058
  }
9858
10059
  return { old_importance: rollback };
9859
10060
  }
@@ -9870,7 +10071,7 @@ function executeUpdateValue(proposal, d) {
9870
10071
  if (!mem)
9871
10072
  throw new Error(`Memory ${memId} not found`);
9872
10073
  rollback[memId] = { value: mem.value, version: mem.version };
9873
- d.run("UPDATE memories SET value = ?, version = version + 1, updated_at = ? WHERE id = ?", [newValue, now2(), memId]);
10074
+ d.run("UPDATE memories SET value = ?, version = version + 1, updated_at = ? WHERE id = ?", [newValue, now(), memId]);
9874
10075
  return { old_state: rollback };
9875
10076
  }
9876
10077
  function executeAddTag(proposal, d) {
@@ -9885,7 +10086,7 @@ function executeAddTag(proposal, d) {
9885
10086
  continue;
9886
10087
  rollback[memId] = [...mem.tags];
9887
10088
  const newTags = Array.from(new Set([...mem.tags, ...tagsToAdd]));
9888
- d.run("UPDATE memories SET tags = ?, updated_at = ? WHERE id = ?", [JSON.stringify(newTags), now2(), memId]);
10089
+ d.run("UPDATE memories SET tags = ?, updated_at = ? WHERE id = ?", [JSON.stringify(newTags), now(), memId]);
9889
10090
  const insertTag = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
9890
10091
  for (const tag of tagsToAdd) {
9891
10092
  insertTag.run(memId, tag);
@@ -9918,11 +10119,11 @@ function executeMerge(proposal, d) {
9918
10119
  const mergedValue = proposal.proposed_changes["merged_value"] ?? [target.value, ...sourceValues].join(`
9919
10120
  ---
9920
10121
  `);
9921
- d.run("UPDATE memories SET value = ?, version = version + 1, updated_at = ? WHERE id = ?", [mergedValue, now2(), targetId]);
10122
+ d.run("UPDATE memories SET value = ?, version = version + 1, updated_at = ? WHERE id = ?", [mergedValue, now(), targetId]);
9922
10123
  for (const memId of proposal.memory_ids) {
9923
10124
  if (memId === targetId)
9924
10125
  continue;
9925
- d.run("UPDATE memories SET status = 'archived', updated_at = ? WHERE id = ?", [now2(), memId]);
10126
+ d.run("UPDATE memories SET status = 'archived', updated_at = ? WHERE id = ?", [now(), memId]);
9926
10127
  }
9927
10128
  return rollback;
9928
10129
  }
@@ -9938,7 +10139,7 @@ function executeRemoveDuplicate(proposal, d) {
9938
10139
  if (mem.id === keepId)
9939
10140
  continue;
9940
10141
  rollback[mem.id] = mem.status;
9941
- d.run("UPDATE memories SET status = 'archived', updated_at = ? WHERE id = ?", [now2(), mem.id]);
10142
+ d.run("UPDATE memories SET status = 'archived', updated_at = ? WHERE id = ?", [now(), mem.id]);
9942
10143
  }
9943
10144
  return { old_status: rollback, kept_id: keepId };
9944
10145
  }
@@ -9970,7 +10171,7 @@ function rollbackProposal(proposal, d) {
9970
10171
  if (!oldStatus)
9971
10172
  break;
9972
10173
  for (const [memId, status] of Object.entries(oldStatus)) {
9973
- d.run("UPDATE memories SET status = ?, updated_at = ? WHERE id = ?", [status, now2(), memId]);
10174
+ d.run("UPDATE memories SET status = ?, updated_at = ? WHERE id = ?", [status, now(), memId]);
9974
10175
  }
9975
10176
  break;
9976
10177
  }
@@ -9979,7 +10180,7 @@ function rollbackProposal(proposal, d) {
9979
10180
  if (!oldImportance)
9980
10181
  break;
9981
10182
  for (const [memId, importance] of Object.entries(oldImportance)) {
9982
- d.run("UPDATE memories SET importance = ?, updated_at = ? WHERE id = ?", [importance, now2(), memId]);
10183
+ d.run("UPDATE memories SET importance = ?, updated_at = ? WHERE id = ?", [importance, now(), memId]);
9983
10184
  }
9984
10185
  break;
9985
10186
  }
@@ -9988,7 +10189,7 @@ function rollbackProposal(proposal, d) {
9988
10189
  if (!oldState)
9989
10190
  break;
9990
10191
  for (const [memId, state] of Object.entries(oldState)) {
9991
- d.run("UPDATE memories SET value = ?, version = ?, updated_at = ? WHERE id = ?", [state.value, state.version, now2(), memId]);
10192
+ d.run("UPDATE memories SET value = ?, version = ?, updated_at = ? WHERE id = ?", [state.value, state.version, now(), memId]);
9992
10193
  }
9993
10194
  break;
9994
10195
  }
@@ -9997,7 +10198,7 @@ function rollbackProposal(proposal, d) {
9997
10198
  if (!oldTags)
9998
10199
  break;
9999
10200
  for (const [memId, tags] of Object.entries(oldTags)) {
10000
- d.run("UPDATE memories SET tags = ?, updated_at = ? WHERE id = ?", [JSON.stringify(tags), now2(), memId]);
10201
+ d.run("UPDATE memories SET tags = ?, updated_at = ? WHERE id = ?", [JSON.stringify(tags), now(), memId]);
10001
10202
  d.run("DELETE FROM memory_tags WHERE memory_id = ?", [memId]);
10002
10203
  const insertTag = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
10003
10204
  for (const tag of tags) {
@@ -10012,11 +10213,11 @@ function rollbackProposal(proposal, d) {
10012
10213
  const archivedMemories = rb["archived_memories"];
10013
10214
  const targetId = proposal.target_memory_id;
10014
10215
  if (targetId && targetOldValue !== undefined && targetOldVersion !== undefined) {
10015
- d.run("UPDATE memories SET value = ?, version = ?, updated_at = ? WHERE id = ?", [targetOldValue, targetOldVersion, now2(), targetId]);
10216
+ d.run("UPDATE memories SET value = ?, version = ?, updated_at = ? WHERE id = ?", [targetOldValue, targetOldVersion, now(), targetId]);
10016
10217
  }
10017
10218
  if (archivedMemories) {
10018
10219
  for (const [memId, status] of Object.entries(archivedMemories)) {
10019
- d.run("UPDATE memories SET status = ?, updated_at = ? WHERE id = ?", [status, now2(), memId]);
10220
+ d.run("UPDATE memories SET status = ?, updated_at = ? WHERE id = ?", [status, now(), memId]);
10020
10221
  }
10021
10222
  }
10022
10223
  break;
@@ -10150,12 +10351,12 @@ async function runSynthesis(options = {}) {
10150
10351
  proposals_accepted: execResult.executed,
10151
10352
  proposals_rejected: validation.rejectedProposals.length + execResult.failed,
10152
10353
  status: "completed",
10153
- completed_at: now2()
10354
+ completed_at: now()
10154
10355
  }, d);
10155
10356
  } else {
10156
10357
  updateSynthesisRun(run.id, {
10157
10358
  status: "completed",
10158
- completed_at: now2()
10359
+ completed_at: now()
10159
10360
  }, d);
10160
10361
  }
10161
10362
  let effectivenessReport = null;
@@ -10177,7 +10378,7 @@ async function runSynthesis(options = {}) {
10177
10378
  updateSynthesisRun(run.id, {
10178
10379
  status: "failed",
10179
10380
  error: err instanceof Error ? err.message : String(err),
10180
- completed_at: now2()
10381
+ completed_at: now()
10181
10382
  }, d);
10182
10383
  const failedRun = listSynthesisRuns({ project_id: projectId, limit: 1 }, d)[0] ?? run;
10183
10384
  return {
@@ -10193,7 +10394,7 @@ async function rollbackSynthesis(runId, db) {
10193
10394
  const d = db || getDatabase();
10194
10395
  const result = await rollbackRun(runId, d);
10195
10396
  if (result.errors.length === 0) {
10196
- updateSynthesisRun(runId, { status: "rolled_back", completed_at: now2() }, d);
10397
+ updateSynthesisRun(runId, { status: "rolled_back", completed_at: now() }, d);
10197
10398
  }
10198
10399
  return result;
10199
10400
  }
@@ -10259,7 +10460,7 @@ function parseJobRow(row) {
10259
10460
  function createSessionJob(input, db) {
10260
10461
  const d = db || getDatabase();
10261
10462
  const id = uuid();
10262
- const timestamp = now2();
10463
+ const timestamp = now();
10263
10464
  const source = input.source ?? "manual";
10264
10465
  const metadata = JSON.stringify(input.metadata ?? {});
10265
10466
  d.run(`INSERT INTO session_memory_jobs
@@ -10278,7 +10479,7 @@ function createSessionJob(input, db) {
10278
10479
  }
10279
10480
  function getSessionJob(id, db) {
10280
10481
  if (!db && isApiMode()) {
10281
- const { status, data } = apiJson("GET", `/sessions/jobs/${encodeURIComponent(id)}`);
10482
+ const { status, data } = apiJson("GET", `/sessions/jobs/${encodeURIComponent(id)}`, undefined, { allow404: true });
10282
10483
  if (status === 404 || !data)
10283
10484
  return null;
10284
10485
  return data;
@@ -10408,7 +10609,7 @@ function saveToolEvent(input, db) {
10408
10609
  }
10409
10610
  const d = db || getDatabase();
10410
10611
  const id = uuid();
10411
- const timestamp = now2();
10612
+ const timestamp = now();
10412
10613
  const metadataJson = JSON.stringify(input.metadata || {});
10413
10614
  d.run(`INSERT INTO tool_events (id, tool_name, action, success, error_type, error_message, tokens_used, latency_ms, context, lesson, when_to_use, agent_id, project_id, session_id, metadata, created_at)
10414
10615
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
@@ -11974,6 +12175,31 @@ var init_pg_migrations = __esm(() => {
11974
12175
  );
11975
12176
  CREATE INDEX IF NOT EXISTS idx_task_comments_task ON task_comments(task_id);
11976
12177
  CREATE INDEX IF NOT EXISTS idx_task_comments_agent ON task_comments(agent_id);
12178
+ `,
12179
+ `
12180
+ CREATE OR REPLACE FUNCTION snapshot_memory_version() RETURNS trigger AS $$
12181
+ BEGIN
12182
+ INSERT INTO memory_versions (
12183
+ id, memory_id, version, value, importance, scope, category, tags,
12184
+ summary, pinned, status, when_to_use, created_at
12185
+ ) VALUES (
12186
+ gen_random_uuid()::text,
12187
+ OLD.id, OLD.version, OLD.value, OLD.importance, OLD.scope, OLD.category,
12188
+ OLD.tags, OLD.summary, OLD.pinned, OLD.status, OLD.when_to_use,
12189
+ OLD.updated_at
12190
+ ) ON CONFLICT DO NOTHING;
12191
+ RETURN NEW;
12192
+ END;
12193
+ $$ LANGUAGE plpgsql;
12194
+
12195
+ DROP TRIGGER IF EXISTS memories_version_snapshot ON memories;
12196
+ CREATE TRIGGER memories_version_snapshot
12197
+ BEFORE UPDATE ON memories
12198
+ FOR EACH ROW
12199
+ WHEN (NEW.version > OLD.version)
12200
+ EXECUTE FUNCTION snapshot_memory_version();
12201
+
12202
+ INSERT INTO _migrations (id) VALUES (36) ON CONFLICT DO NOTHING;
11977
12203
  `
11978
12204
  ];
11979
12205
  });
@@ -58701,15 +58927,108 @@ var {
58701
58927
  Help
58702
58928
  } = import__.default;
58703
58929
 
58930
+ // src/cli/index.tsx
58931
+ init_database();
58932
+ import { readFileSync as readFileSync8 } from "fs";
58933
+ import { dirname as dirname7, join as join12 } from "path";
58934
+ import { fileURLToPath as fileURLToPath4 } from "url";
58935
+
58936
+ // src/db/machines.ts
58937
+ init_database();
58938
+ import { hostname, platform } from "os";
58939
+ function parseMachine(row) {
58940
+ if (!row)
58941
+ return null;
58942
+ return {
58943
+ ...row,
58944
+ is_primary: Boolean(row.is_primary)
58945
+ };
58946
+ }
58947
+ function normalizeHostname(host) {
58948
+ return host.replace(/\.(local|lan|home|internal)$/i, "");
58949
+ }
58950
+ function registerMachine(name, db = getDatabase()) {
58951
+ const rawHost = hostname();
58952
+ const host = normalizeHostname(rawHost);
58953
+ const plat = platform();
58954
+ const machineName = name?.trim() || host;
58955
+ const existing = parseMachine(db.query("SELECT * FROM machines WHERE hostname = ?").get(host));
58956
+ if (existing) {
58957
+ db.run("UPDATE machines SET last_seen_at = ? WHERE id = ?", [now(), existing.id]);
58958
+ return parseMachine(db.query("SELECT * FROM machines WHERE id = ?").get(existing.id));
58959
+ }
58960
+ let finalName = machineName;
58961
+ let suffix = 2;
58962
+ while (db.query("SELECT id FROM machines WHERE name = ?").get(finalName)) {
58963
+ finalName = `${machineName}-${suffix++}`;
58964
+ }
58965
+ const id = uuid();
58966
+ db.run("INSERT INTO machines (id, name, hostname, platform) VALUES (?, ?, ?, ?)", [id, finalName, host, plat]);
58967
+ return parseMachine(db.query("SELECT * FROM machines WHERE id = ?").get(id));
58968
+ }
58969
+ function getPrimaryMachine(db = getDatabase()) {
58970
+ return parseMachine(db.query("SELECT * FROM machines WHERE is_primary = 1 LIMIT 1").get());
58971
+ }
58972
+ function getPrimaryMachineCandidate(db = getDatabase()) {
58973
+ if (getPrimaryMachine(db))
58974
+ return null;
58975
+ return parseMachine(db.query("SELECT * FROM machines ORDER BY created_at ASC, id ASC LIMIT 1").get());
58976
+ }
58977
+ function getPrimaryMachineStartupWarning(db = getDatabase()) {
58978
+ if (getPrimaryMachine(db))
58979
+ return null;
58980
+ const candidate = getPrimaryMachineCandidate(db);
58981
+ if (!candidate) {
58982
+ return "No primary machine configured. Fallback sync target is unset because no machines are registered yet.";
58983
+ }
58984
+ 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.`;
58985
+ }
58986
+ function touchMachine(id, db = getDatabase()) {
58987
+ db.run("UPDATE machines SET last_seen_at = ? WHERE id = ?", [now(), id]);
58988
+ }
58989
+ function getCurrentMachineId(db = getDatabase()) {
58990
+ const host = normalizeHostname(hostname());
58991
+ const m = db.query("SELECT id FROM machines WHERE hostname = ?").get(host);
58992
+ if (m) {
58993
+ touchMachine(m.id, db);
58994
+ return m.id;
58995
+ }
58996
+ return registerMachine(undefined, db).id;
58997
+ }
58998
+
58999
+ // src/cli/startup-side-effects.ts
59000
+ var NO_STARTUP_DB_ACCESS = new WeakSet;
59001
+ function withoutStartupDbAccess(command) {
59002
+ NO_STARTUP_DB_ACCESS.add(command);
59003
+ return command;
59004
+ }
59005
+ function skipsStartupDbAccess(command) {
59006
+ return command !== undefined && NO_STARTUP_DB_ACCESS.has(command);
59007
+ }
59008
+
59009
+ // src/cli/global-options.ts
59010
+ var GLOBAL_OPTIONS = [
59011
+ ["-p, --project <path>", "Project path for scoping"],
59012
+ ["-j, --json", "Output as JSON"],
59013
+ ["-f, --format <fmt>", "Output format: compact, json, csv, yaml"],
59014
+ ["-a, --agent <name>", "Agent name or ID"],
59015
+ ["-s, --session <id>", "Session ID"]
59016
+ ];
59017
+ function applyGlobalOptions(program2) {
59018
+ for (const [flags, description] of GLOBAL_OPTIONS)
59019
+ program2.option(flags, description);
59020
+ return program2;
59021
+ }
59022
+
58704
59023
  // node_modules/@hasna/events/dist/commander.js
58705
59024
  import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
58706
- import { existsSync } from "fs";
58707
- import { homedir } from "os";
58708
- import { join } from "path";
59025
+ import { existsSync as existsSync3 } from "fs";
59026
+ import { homedir as homedir2 } from "os";
59027
+ import { join as join4 } from "path";
58709
59028
  import { createHmac, timingSafeEqual } from "crypto";
58710
- import { randomUUID } from "crypto";
58711
- import { spawn } from "child_process";
58712
59029
  import { randomUUID as randomUUID2 } from "crypto";
59030
+ import { spawn } from "child_process";
59031
+ import { randomUUID as randomUUID22 } from "crypto";
58713
59032
  function getPathValue(input, path) {
58714
59033
  return path.split(".").reduce((value, part) => {
58715
59034
  if (value && typeof value === "object" && part in value) {
@@ -58754,7 +59073,7 @@ function channelMatchesEvent(channel, event) {
58754
59073
  var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR";
58755
59074
  var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
58756
59075
  function getEventsDataDir(override) {
58757
- return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join(homedir(), ".hasna", "events");
59076
+ return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join4(homedir2(), ".hasna", "events");
58758
59077
  }
58759
59078
 
58760
59079
  class JsonEventsStore {
@@ -58764,9 +59083,9 @@ class JsonEventsStore {
58764
59083
  deliveriesPath;
58765
59084
  constructor(dataDir = getEventsDataDir()) {
58766
59085
  this.dataDir = dataDir;
58767
- this.channelsPath = join(dataDir, "channels.json");
58768
- this.eventsPath = join(dataDir, "events.json");
58769
- this.deliveriesPath = join(dataDir, "deliveries.json");
59086
+ this.channelsPath = join4(dataDir, "channels.json");
59087
+ this.eventsPath = join4(dataDir, "events.json");
59088
+ this.deliveriesPath = join4(dataDir, "deliveries.json");
58770
59089
  }
58771
59090
  async init() {
58772
59091
  await mkdir(this.dataDir, { recursive: true, mode: 448 });
@@ -58838,7 +59157,7 @@ class JsonEventsStore {
58838
59157
  };
58839
59158
  }
58840
59159
  async ensureArrayFile(path) {
58841
- if (!existsSync(path)) {
59160
+ if (!existsSync3(path)) {
58842
59161
  await writeFile(path, `[]
58843
59162
  `, { encoding: "utf-8", mode: 384 });
58844
59163
  }
@@ -58876,7 +59195,7 @@ function signPayload(secret, timestamp, body) {
58876
59195
  const digest = createHmac("sha256", secret).update(buildSignatureBase(timestamp, body)).digest("hex");
58877
59196
  return `sha256=${digest}`;
58878
59197
  }
58879
- function now() {
59198
+ function now2() {
58880
59199
  return new Date().toISOString();
58881
59200
  }
58882
59201
  function truncate(value, max = 4096) {
@@ -58903,7 +59222,7 @@ function buildWebhookRequest(event, channel) {
58903
59222
  async function dispatchWebhook(event, channel, options = {}) {
58904
59223
  if (!channel.webhook)
58905
59224
  throw new Error(`Channel ${channel.id} has no webhook config`);
58906
- const startedAt = now();
59225
+ const startedAt = now2();
58907
59226
  const { body, headers } = buildWebhookRequest(event, channel);
58908
59227
  const controller = new AbortController;
58909
59228
  const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
@@ -58919,7 +59238,7 @@ async function dispatchWebhook(event, channel, options = {}) {
58919
59238
  attempt: 1,
58920
59239
  status: response.ok ? "success" : "failed",
58921
59240
  startedAt,
58922
- completedAt: now(),
59241
+ completedAt: now2(),
58923
59242
  responseStatus: response.status,
58924
59243
  responseBody,
58925
59244
  error: response.ok ? undefined : `Webhook returned HTTP ${response.status}`
@@ -58929,7 +59248,7 @@ async function dispatchWebhook(event, channel, options = {}) {
58929
59248
  attempt: 1,
58930
59249
  status: "failed",
58931
59250
  startedAt,
58932
- completedAt: now(),
59251
+ completedAt: now2(),
58933
59252
  error: error instanceof Error ? error.message : String(error)
58934
59253
  };
58935
59254
  } finally {
@@ -58939,7 +59258,7 @@ async function dispatchWebhook(event, channel, options = {}) {
58939
59258
  async function dispatchCommand(event, channel) {
58940
59259
  if (!channel.command)
58941
59260
  throw new Error(`Channel ${channel.id} has no command config`);
58942
- const startedAt = now();
59261
+ const startedAt = now2();
58943
59262
  const eventJson = JSON.stringify(event);
58944
59263
  const env = {
58945
59264
  ...process.env,
@@ -58955,7 +59274,7 @@ async function dispatchCommand(event, channel) {
58955
59274
  HASNA_EVENT_SCHEMA_VERSION: event.schemaVersion,
58956
59275
  HASNA_EVENT_JSON: eventJson
58957
59276
  };
58958
- return new Promise((resolve) => {
59277
+ return new Promise((resolve2) => {
58959
59278
  const child = spawn(channel.command.command, channel.command.args ?? [], {
58960
59279
  cwd: channel.command.cwd,
58961
59280
  env,
@@ -58973,11 +59292,11 @@ async function dispatchCommand(event, channel) {
58973
59292
  });
58974
59293
  child.on("error", (error) => {
58975
59294
  clearTimeout(timeout);
58976
- resolve({
59295
+ resolve2({
58977
59296
  attempt: 1,
58978
59297
  status: "failed",
58979
59298
  startedAt,
58980
- completedAt: now(),
59299
+ completedAt: now2(),
58981
59300
  stdout: truncate(stdout),
58982
59301
  stderr: truncate(stderr),
58983
59302
  error: error.message
@@ -58986,11 +59305,11 @@ async function dispatchCommand(event, channel) {
58986
59305
  child.on("close", (code, signal) => {
58987
59306
  clearTimeout(timeout);
58988
59307
  const success = code === 0;
58989
- resolve({
59308
+ resolve2({
58990
59309
  attempt: 1,
58991
59310
  status: success ? "success" : "failed",
58992
59311
  startedAt,
58993
- completedAt: now(),
59312
+ completedAt: now2(),
58994
59313
  stdout: truncate(stdout),
58995
59314
  stderr: truncate(stderr),
58996
59315
  error: success ? undefined : `Command exited with ${signal ? `signal ${signal}` : `code ${code}`}`
@@ -59006,27 +59325,27 @@ async function dispatchChannel(event, channel, options = {}) {
59006
59325
  return {
59007
59326
  attempt: 1,
59008
59327
  status: "skipped",
59009
- startedAt: now(),
59010
- completedAt: now(),
59328
+ startedAt: now2(),
59329
+ completedAt: now2(),
59011
59330
  error: `Unsupported transport: ${channel.transport}`
59012
59331
  };
59013
59332
  }
59014
59333
  function createDeliveryResult(event, channel, attempts) {
59015
59334
  const status = attempts.some((attempt) => attempt.status === "success") ? "success" : attempts.every((attempt) => attempt.status === "skipped") ? "skipped" : "failed";
59016
59335
  return {
59017
- id: randomUUID(),
59336
+ id: randomUUID2(),
59018
59337
  eventId: event.id,
59019
59338
  channelId: channel.id,
59020
59339
  transport: channel.transport,
59021
59340
  status,
59022
59341
  attempts,
59023
- createdAt: attempts[0]?.startedAt ?? now(),
59024
- completedAt: attempts.at(-1)?.completedAt ?? now()
59342
+ createdAt: attempts[0]?.startedAt ?? now2(),
59343
+ completedAt: attempts.at(-1)?.completedAt ?? now2()
59025
59344
  };
59026
59345
  }
59027
59346
  function createEvent(input) {
59028
59347
  return {
59029
- id: input.id ?? randomUUID2(),
59348
+ id: input.id ?? randomUUID22(),
59030
59349
  source: input.source,
59031
59350
  type: input.type,
59032
59351
  time: normalizeTime(input.time),
@@ -59385,75 +59704,6 @@ function collectValues(value, previous) {
59385
59704
  return previous;
59386
59705
  }
59387
59706
 
59388
- // src/cli/index.tsx
59389
- init_database();
59390
- import { readFileSync as readFileSync8 } from "fs";
59391
- import { dirname as dirname7, join as join12 } from "path";
59392
- import { fileURLToPath as fileURLToPath4 } from "url";
59393
-
59394
- // src/db/machines.ts
59395
- init_database();
59396
- import { hostname, platform } from "os";
59397
- function parseMachine(row) {
59398
- if (!row)
59399
- return null;
59400
- return {
59401
- ...row,
59402
- is_primary: Boolean(row.is_primary)
59403
- };
59404
- }
59405
- function normalizeHostname(host) {
59406
- return host.replace(/\.(local|lan|home|internal)$/i, "");
59407
- }
59408
- function registerMachine(name, db = getDatabase()) {
59409
- const rawHost = hostname();
59410
- const host = normalizeHostname(rawHost);
59411
- const plat = platform();
59412
- const machineName = name?.trim() || host;
59413
- const existing = parseMachine(db.query("SELECT * FROM machines WHERE hostname = ?").get(host));
59414
- if (existing) {
59415
- db.run("UPDATE machines SET last_seen_at = ? WHERE id = ?", [now2(), existing.id]);
59416
- return parseMachine(db.query("SELECT * FROM machines WHERE id = ?").get(existing.id));
59417
- }
59418
- let finalName = machineName;
59419
- let suffix = 2;
59420
- while (db.query("SELECT id FROM machines WHERE name = ?").get(finalName)) {
59421
- finalName = `${machineName}-${suffix++}`;
59422
- }
59423
- const id = uuid();
59424
- db.run("INSERT INTO machines (id, name, hostname, platform) VALUES (?, ?, ?, ?)", [id, finalName, host, plat]);
59425
- return parseMachine(db.query("SELECT * FROM machines WHERE id = ?").get(id));
59426
- }
59427
- function getPrimaryMachine(db = getDatabase()) {
59428
- return parseMachine(db.query("SELECT * FROM machines WHERE is_primary = 1 LIMIT 1").get());
59429
- }
59430
- function getPrimaryMachineCandidate(db = getDatabase()) {
59431
- if (getPrimaryMachine(db))
59432
- return null;
59433
- return parseMachine(db.query("SELECT * FROM machines ORDER BY created_at ASC, id ASC LIMIT 1").get());
59434
- }
59435
- function getPrimaryMachineStartupWarning(db = getDatabase()) {
59436
- if (getPrimaryMachine(db))
59437
- return null;
59438
- const candidate = getPrimaryMachineCandidate(db);
59439
- if (!candidate) {
59440
- return "No primary machine configured. Fallback sync target is unset because no machines are registered yet.";
59441
- }
59442
- 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.`;
59443
- }
59444
- function touchMachine(id, db = getDatabase()) {
59445
- db.run("UPDATE machines SET last_seen_at = ? WHERE id = ?", [now2(), id]);
59446
- }
59447
- function getCurrentMachineId(db = getDatabase()) {
59448
- const host = normalizeHostname(hostname());
59449
- const m = db.query("SELECT id FROM machines WHERE hostname = ?").get(host);
59450
- if (m) {
59451
- touchMachine(m.id, db);
59452
- return m.id;
59453
- }
59454
- return registerMachine(undefined, db).id;
59455
- }
59456
-
59457
59707
  // src/cli/commands/memory.ts
59458
59708
  init_helpers();
59459
59709
 
@@ -59510,10 +59760,12 @@ var FORMAT_UNITS = [
59510
59760
  ];
59511
59761
 
59512
59762
  // src/cli/commands/memory-cmd-crud.ts
59763
+ init_enum_validation();
59764
+ init_types();
59513
59765
  init_helpers();
59514
59766
  function registerCrudCommands(program2) {
59515
59767
  const handleError = makeHandleError(program2);
59516
- program2.command("save <key> <value>").description("Save a memory (create or upsert)").option("-c, --category <cat>", "Category: preference, fact, knowledge, history").option("-s, --scope <scope>", "Scope: global, shared, private").option("--importance <n>", "Importance 1-10", parseInt).option("--tags <tags>", "Comma-separated tags").option("--summary <text>", "Brief summary").option("--ttl <duration>", "Time-to-live: 30s, 5m, 2h, 1d, 1w, or milliseconds").option("--source <src>", "Source: user, agent, system, auto, imported").option("--template <name>", "Apply a template: correction, preference, decision, learning").action((key, value, opts) => {
59768
+ program2.command("save <key> <value>").description("Save a memory (create or upsert)").option("-c, --category <cat>", `Category: ${MEMORY_CATEGORIES.join(", ")}`).option("--scope <scope>", `Scope: ${MEMORY_SCOPES.join(", ")}`).option("--importance <n>", "Importance 1-10", parseInt).option("--tags <tags>", "Comma-separated tags").option("--summary <text>", "Brief summary").option("--ttl <duration>", "Time-to-live: 30s, 5m, 2h, 1d, 1w, or milliseconds").option("--source <src>", "Source: user, agent, system, auto, imported").option("--template <name>", "Apply a template: correction, preference, decision, learning").option("--dedupe <mode>", "Conflict handling: merge (default, upsert the matching row), create (fork a new row under the same key), error").action((key, value, opts) => {
59517
59769
  try {
59518
59770
  const globalOpts = program2.opts();
59519
59771
  const templates = {
@@ -59551,6 +59803,20 @@ function registerCrudCommands(program2) {
59551
59803
  }
59552
59804
  templateDefaults = tpl;
59553
59805
  }
59806
+ for (const [flag, value2] of [
59807
+ ["category", opts.category],
59808
+ ["scope", opts.scope],
59809
+ ["source", opts.source]
59810
+ ]) {
59811
+ const violation = validateEnumField(flag, value2);
59812
+ if (!violation)
59813
+ continue;
59814
+ let msg = formatEnumViolation(violation);
59815
+ if (flag === "category" && templates[violation.value]) {
59816
+ msg += ` Did you mean --template ${violation.value}?`;
59817
+ }
59818
+ throw new Error(msg);
59819
+ }
59554
59820
  const explicitTags = opts.tags ? opts.tags.split(",").map((t) => t.trim()) : undefined;
59555
59821
  const mergedTags = explicitTags ? explicitTags : templateDefaults?.tags && templateDefaults.tags.length > 0 ? templateDefaults.tags : undefined;
59556
59822
  let resolvedAgentId;
@@ -59576,17 +59842,40 @@ function registerCrudCommands(program2) {
59576
59842
  if (project)
59577
59843
  input.project_id = project.id;
59578
59844
  }
59579
- const memory = createMemory(input);
59845
+ const bucket = (m) => [m.scope ?? "private", m.agent_id ?? "", m.project_id ?? "", m.session_id ?? ""].join("\x1F");
59846
+ const targetBucket = bucket({
59847
+ scope: input.scope ?? "private",
59848
+ agent_id: input.agent_id,
59849
+ project_id: input.project_id,
59850
+ session_id: input.session_id
59851
+ });
59852
+ const dedupe = opts.dedupe;
59853
+ const forkRequested = dedupe === "create" || dedupe === "version-fork";
59854
+ let willUpdateExisting = false;
59855
+ if (!forkRequested) {
59856
+ const sameKey = getMemoriesByKey(key);
59857
+ const match = sameKey.find((m) => bucket(m) === targetBucket);
59858
+ willUpdateExisting = Boolean(match);
59859
+ if (!match && sameKey.length > 0) {
59860
+ const rows = sameKey.map((m) => ` ${m.id.slice(0, 8)} scope=${m.scope} project=${m.project_id ?? "none"} session=${m.session_id ?? "none"} agent=${m.agent_id ?? "none"}`).join(`
59861
+ `);
59862
+ throw new Error(`Refusing to fork key "${key}": ${sameKey.length} active memor${sameKey.length === 1 ? "y" : "ies"} ` + `already ${sameKey.length === 1 ? "uses" : "use"} it, ` + `but none matches the scope/project/session this save targets ` + `(scope=${input.scope ?? "private"}, project=${input.project_id ?? "none"}, session=${input.session_id ?? "none"}).
59863
+ ` + `${rows}
59864
+ ` + `Saving would create a second active row under the same key. Either target the existing row ` + `(match its scope/project/session flags, or use \`mementos update <id>\`), or pass \`--dedupe create\` ` + `to fork deliberately.`);
59865
+ }
59866
+ }
59867
+ const memory = forkRequested ? createMemory(input, dedupe) : createMemory(input);
59868
+ const outcome = willUpdateExisting ? "Updated" : "Created";
59580
59869
  if (globalOpts.json) {
59581
- outputJson(memory);
59870
+ outputJson({ ...memory, outcome: outcome.toLowerCase() });
59582
59871
  } else {
59583
- console.log(chalk2.green(`Saved: ${memory.key} (${memory.id.slice(0, 8)})`));
59872
+ console.log(chalk2.green(`${outcome}: ${memory.key} (${memory.id.slice(0, 8)})`));
59584
59873
  }
59585
59874
  } catch (e) {
59586
59875
  handleError(e);
59587
59876
  }
59588
59877
  });
59589
- program2.command("update <id>").description("Update a memory by ID").option("--value <text>", "New value").option("--importance <n>", "New importance 1-10", parseInt).option("--tags <tags>", "New comma-separated tags").option("--summary <text>", "New summary").option("--pin", "Pin the memory").option("--unpin", "Unpin the memory").option("-c, --category <cat>", "New category").option("-s, --scope <scope>", "New scope").option("--status <status>", "New status: active, archived, expired").action((id, opts) => {
59878
+ program2.command("update <id>").description("Update a memory by ID").option("--value <text>", "New value").option("--importance <n>", "New importance 1-10", parseInt).option("--tags <tags>", "New comma-separated tags").option("--summary <text>", "New summary").option("--pin", "Pin the memory").option("--unpin", "Unpin the memory").option("-c, --category <cat>", "New category").option("--scope <scope>", "New scope").option("--status <status>", "New status: active, archived, expired").action((id, opts) => {
59590
59879
  try {
59591
59880
  const globalOpts = program2.opts();
59592
59881
  const resolvedId = resolveMemoryId(id);
@@ -59620,17 +59909,31 @@ function registerCrudCommands(program2) {
59620
59909
  updateInput.scope = opts.scope;
59621
59910
  if (opts.status !== undefined)
59622
59911
  updateInput.status = opts.status;
59912
+ for (const [flag, value] of [
59913
+ ["category", opts.category],
59914
+ ["scope", opts.scope],
59915
+ ["status", opts.status]
59916
+ ]) {
59917
+ const violation = validateEnumField(flag, value);
59918
+ if (violation)
59919
+ throw new Error(formatEnumViolation(violation));
59920
+ }
59921
+ const changedFields = Object.keys(updateInput).filter((k) => k !== "version");
59922
+ if (changedFields.length === 0) {
59923
+ throw new Error(`Nothing to update: no fields were given for ${existing.key} (${existing.id.slice(0, 8)}). ` + `Pass at least one of --value, --scope, --category, --status, --importance, --tags, --summary, --pin/--unpin. ` + `Note that -s is the global --session, not --scope.`);
59924
+ }
59623
59925
  const updated = updateMemory(resolvedId, updateInput);
59624
59926
  if (globalOpts.json) {
59625
- outputJson(updated);
59927
+ outputJson({ ...updated, updated_fields: changedFields });
59626
59928
  } else {
59627
- console.log(chalk2.green(`Updated: ${updated.key} (${updated.id.slice(0, 8)})`));
59929
+ const n = changedFields.length;
59930
+ console.log(chalk2.green(`Updated ${n} field${n === 1 ? "" : "s"}: ${updated.key} (${updated.id.slice(0, 8)})`) + chalk2.dim(` [${changedFields.join(", ")}]`));
59628
59931
  }
59629
59932
  } catch (e) {
59630
59933
  handleError(e);
59631
59934
  }
59632
59935
  });
59633
- program2.command("forget <keyOrId>").description("Delete a memory by key or ID").option("-s, --scope <scope>", "Filter by scope (global, shared, private)").option("-a, --agent <agent>", "Filter by agent ID").option("-p, --project <project>", "Filter by project ID").option("--all", "Delete ALL matching memories (no disambiguation needed)").action((keyOrId, opts) => {
59936
+ program2.command("forget <keyOrId>").description("Delete a memory by key or ID").option("--scope <scope>", "Filter by scope (global, shared, private)").option("--agent <agent>", "Filter by agent ID").option("--project <project>", "Filter by project ID").option("--all", "Delete ALL matching memories (no disambiguation needed)").action((keyOrId, opts) => {
59634
59937
  try {
59635
59938
  const globalOpts = program2.opts();
59636
59939
  const idMatch = isApiMode() ? null : resolvePartialId(getDatabase(), "memories", keyOrId);
@@ -59730,7 +60033,7 @@ function registerViewCommands(program2) {
59730
60033
  handleError(e);
59731
60034
  }
59732
60035
  });
59733
- program2.command("pin <keyOrId>").description("Pin a memory by key or partial ID").option("-s, --scope <scope>", "Scope filter for key lookup").option("--agent <name>", "Agent filter for key lookup").option("--project <path>", "Project filter for key lookup").action((keyOrId, opts) => {
60036
+ program2.command("pin <keyOrId>").description("Pin a memory by key or partial ID").option("--scope <scope>", "Scope filter for key lookup").option("--agent <name>", "Agent filter for key lookup").option("--project <path>", "Project filter for key lookup").action((keyOrId, opts) => {
59734
60037
  try {
59735
60038
  const globalOpts = program2.opts();
59736
60039
  const memory = resolveKeyOrId(keyOrId, opts, globalOpts);
@@ -59755,7 +60058,7 @@ function registerViewCommands(program2) {
59755
60058
  handleError(e);
59756
60059
  }
59757
60060
  });
59758
- program2.command("unpin <keyOrId>").description("Unpin a memory by key or partial ID").option("-s, --scope <scope>", "Scope filter for key lookup").option("--agent <name>", "Agent filter for key lookup").option("--project <path>", "Project filter for key lookup").action((keyOrId, opts) => {
60061
+ program2.command("unpin <keyOrId>").description("Unpin a memory by key or partial ID").option("--scope <scope>", "Scope filter for key lookup").option("--agent <name>", "Agent filter for key lookup").option("--project <path>", "Project filter for key lookup").action((keyOrId, opts) => {
59759
60062
  try {
59760
60063
  const globalOpts = program2.opts();
59761
60064
  const memory = resolveKeyOrId(keyOrId, opts, globalOpts);
@@ -59780,7 +60083,7 @@ function registerViewCommands(program2) {
59780
60083
  handleError(e);
59781
60084
  }
59782
60085
  });
59783
- program2.command("archive <keyOrId>").description("Archive a memory by key or ID (hides from lists, keeps history)").option("-s, --scope <scope>", "Scope filter for key lookup").action((keyOrId, opts) => {
60086
+ program2.command("archive <keyOrId>").description("Archive a memory by key or ID (hides from lists, keeps history)").option("--scope <scope>", "Scope filter for key lookup").action((keyOrId, opts) => {
59784
60087
  try {
59785
60088
  const globalOpts = program2.opts();
59786
60089
  const memory = resolveKeyOrId(keyOrId, opts, globalOpts);
@@ -59799,7 +60102,7 @@ function registerViewCommands(program2) {
59799
60102
  process.exit(1);
59800
60103
  }
59801
60104
  });
59802
- program2.command("versions <keyOrId>").description("Show version history for a memory").option("-s, --scope <scope>", "Scope filter for key lookup").action((keyOrId, opts) => {
60105
+ program2.command("versions <keyOrId>").description("Show version history for a memory").option("--scope <scope>", "Scope filter for key lookup").action((keyOrId, opts) => {
59803
60106
  try {
59804
60107
  const globalOpts = program2.opts();
59805
60108
  const memory = resolveKeyOrId(keyOrId, opts, globalOpts);
@@ -59841,7 +60144,7 @@ import chalk4 from "chalk";
59841
60144
  import { resolve as resolve4 } from "path";
59842
60145
  function registerTailCommand(program2) {
59843
60146
  const handleError = makeHandleError(program2);
59844
- program2.command("tail").description("Watch for new/updated memories in real-time (like tail -f)").option("-s, --scope <scope>", "Scope filter: global, shared, private").option("-c, --category <cat>", "Category filter: preference, fact, knowledge, history").option("--agent <name>", "Agent filter").option("--project <path>", "Project filter").option("--interval <ms>", "Poll interval in milliseconds (default: 2000)", parseInt).option("--notify", "Send macOS notifications for each change").action((opts) => {
60147
+ program2.command("tail").description("Watch for new/updated memories in real-time (like tail -f)").option("--scope <scope>", "Scope filter: global, shared, private, working").option("-c, --category <cat>", "Category filter: preference, fact, knowledge, history, procedural, resource").option("--agent <name>", "Agent filter").option("--project <path>", "Project filter").option("--interval <ms>", "Poll interval in milliseconds (default: 2000)", parseInt).option("--notify", "Send macOS notifications for each change").action((opts) => {
59845
60148
  try {
59846
60149
  const globalOpts = program2.opts();
59847
60150
  const jsonMode = !!globalOpts.json;
@@ -59955,7 +60258,7 @@ import chalk6 from "chalk";
59955
60258
  import { resolve as resolve5 } from "path";
59956
60259
  function registerSearchCommand(program2) {
59957
60260
  const handleError = makeHandleError(program2);
59958
- program2.command("search <query>").description("Full-text search across memories").option("-s, --scope <scope>", "Scope filter").option("-c, --category <cat>", "Category filter").option("--tags <tags>", "Comma-separated tags filter").option("--project <path>", "Project filter (path or name)").option("--agent <name>", "Agent filter").option("--session <id>", "Session ID filter").option("--limit <n>", "Max results", parseInt).option("--offset <n>", "Offset for pagination", parseInt).option("--cursor <n>", "Cursor offset for the next page", parseInt).option("--format <fmt>", "Output format: compact (default), json, csv, yaml").option("--verbose", "Show match highlights and wider snippets").option("--history", "Show recent search queries instead of searching").option("--popular", "Show most popular search queries").action((query, opts) => {
60261
+ program2.command("search <query>").description("Full-text search across memories").option("--scope <scope>", "Scope filter").option("-c, --category <cat>", "Category filter").option("--tags <tags>", "Comma-separated tags filter").option("--project <path>", "Project filter (path or name)").option("--agent <name>", "Agent filter").option("--session <id>", "Session ID filter").option("--limit <n>", "Max results", parseInt).option("--offset <n>", "Offset for pagination", parseInt).option("--cursor <n>", "Cursor offset for the next page", parseInt).option("--format <fmt>", "Output format: compact (default), json, csv, yaml").option("--verbose", "Show match highlights and wider snippets").option("--history", "Show recent search queries instead of searching").option("--popular", "Show most popular search queries").action((query, opts) => {
59959
60262
  try {
59960
60263
  const fmt = getOutputFormat(program2, opts.format);
59961
60264
  const isStructured = fmt === "json" || fmt === "csv" || fmt === "yaml";
@@ -60216,7 +60519,7 @@ init_memories();
60216
60519
  init_helpers();
60217
60520
  import chalk10 from "chalk";
60218
60521
  function registerRemoveCommand(program2) {
60219
- program2.command("remove <nameOrId>").description("Remove/delete a memory by name or ID (alias for memory forget)").option("--agent <id>", "Agent ID").option("-s, --scope <scope>", "Filter by scope (when looking up by key)").action((nameOrId, opts) => {
60522
+ program2.command("remove <nameOrId>").description("Remove/delete a memory by name or ID (alias for memory forget)").option("--agent <id>", "Agent ID").option("--scope <scope>", "Filter by scope (when looking up by key)").action((nameOrId, opts) => {
60220
60523
  const globalOpts = program2.opts();
60221
60524
  const agentId = opts.agent || globalOpts.agent;
60222
60525
  let id = isApiMode() ? null : resolvePartialId(getDatabase(), "memories", nameOrId);
@@ -60273,7 +60576,7 @@ import chalk11 from "chalk";
60273
60576
  import { resolve as resolve6 } from "path";
60274
60577
  function registerRecallCommand(program2) {
60275
60578
  const handleError = makeHandleError(program2);
60276
- program2.command("recall <key>").description("Recall a memory by key").option("-s, --scope <scope>", "Scope filter").option("--agent <name>", "Agent filter").option("--project <path>", "Project filter").action((key, opts) => {
60579
+ program2.command("recall <key>").description("Recall a memory by key").option("--scope <scope>", "Scope filter").option("--agent <name>", "Agent filter").option("--project <path>", "Project filter").action((key, opts) => {
60277
60580
  try {
60278
60581
  const globalOpts = program2.opts();
60279
60582
  const agentId = opts.agent || globalOpts.agent;
@@ -60331,7 +60634,7 @@ import chalk12 from "chalk";
60331
60634
  import { resolve as resolve7 } from "path";
60332
60635
  function registerListCommand(program2) {
60333
60636
  const handleError = makeHandleError(program2);
60334
- program2.command("list").description("List memories with optional filters").option("-s, --scope <scope>", "Scope filter").option("-c, --category <cat>", "Category filter").option("--tags <tags>", "Comma-separated tags filter").option("--importance-min <n>", "Minimum importance", parseInt).option("--pinned", "Show only pinned").option("--agent <name>", "Agent filter").option("--project <path>", "Project filter").option("--session <id>", "Session ID filter").option("--limit <n>", "Max results", parseInt).option("--offset <n>", "Offset for pagination", parseInt).option("--cursor <n>", "Cursor offset for the next page", parseInt).option("--status <status>", "Status filter: active, archived, expired").option("--format <fmt>", "Output format: compact (default), json, csv, yaml").option("--verbose", "Show wider memory snippets in human output").action((opts) => {
60637
+ program2.command("list").description("List memories with optional filters").option("--scope <scope>", "Scope filter").option("-c, --category <cat>", "Category filter").option("--tags <tags>", "Comma-separated tags filter").option("--importance-min <n>", "Minimum importance", parseInt).option("--pinned", "Show only pinned").option("--agent <name>", "Agent filter").option("--project <path>", "Project filter").option("--session <id>", "Session ID filter").option("--limit <n>", "Max results", parseInt).option("--offset <n>", "Offset for pagination", parseInt).option("--cursor <n>", "Cursor offset for the next page", parseInt).option("--status <status>", "Status filter: active, archived, expired").option("--format <fmt>", "Output format: compact (default), json, csv, yaml").option("--verbose", "Show wider memory snippets in human output").action((opts) => {
60335
60638
  try {
60336
60639
  const globalOpts = program2.opts();
60337
60640
  const fmt = getOutputFormat(program2, opts.format);
@@ -60980,7 +61283,7 @@ init_helpers();
60980
61283
  import { resolve as resolve11 } from "path";
60981
61284
  function registerExportCommand(program2) {
60982
61285
  const handleError = makeHandleError(program2);
60983
- program2.command("export").description("Export memories as JSON").option("-s, --scope <scope>", "Scope filter").option("-c, --category <cat>", "Category filter").option("--agent <name>", "Agent filter").option("--project <path>", "Project filter").action((opts) => {
61286
+ program2.command("export").description("Export memories as JSON").option("--scope <scope>", "Scope filter").option("-c, --category <cat>", "Category filter").option("--agent <name>", "Agent filter").option("--project <path>", "Project filter").action((opts) => {
60984
61287
  try {
60985
61288
  const globalOpts = program2.opts();
60986
61289
  const agentId = opts.agent || globalOpts.agent;
@@ -61296,7 +61599,7 @@ function enforceQuotas(config, db) {
61296
61599
  }
61297
61600
  function archiveStale(staleDays, db) {
61298
61601
  const d = db || getDatabase();
61299
- const timestamp = now2();
61602
+ const timestamp = now();
61300
61603
  const cutoff = new Date(Date.now() - staleDays * 24 * 60 * 60 * 1000).toISOString();
61301
61604
  const archiveWhere = `status = 'active' AND pinned = 0 AND COALESCE(accessed_at, created_at) < ?`;
61302
61605
  const count = d.query(`SELECT COUNT(*) as c FROM memories WHERE ${archiveWhere}`).get(cutoff).c;
@@ -61307,7 +61610,7 @@ function archiveStale(staleDays, db) {
61307
61610
  }
61308
61611
  function archiveUnused(days, db) {
61309
61612
  const d = db || getDatabase();
61310
- const timestamp = now2();
61613
+ const timestamp = now();
61311
61614
  const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
61312
61615
  const unusedWhere = `status = 'active' AND pinned = 0 AND access_count = 0 AND created_at < ?`;
61313
61616
  const count = d.query(`SELECT COUNT(*) as c FROM memories WHERE ${unusedWhere}`).get(cutoff).c;
@@ -61318,7 +61621,7 @@ function archiveUnused(days, db) {
61318
61621
  }
61319
61622
  function deprioritizeStale(days, db) {
61320
61623
  const d = db || getDatabase();
61321
- const timestamp = now2();
61624
+ const timestamp = now();
61322
61625
  const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
61323
61626
  const deprioWhere = `status = 'active' AND pinned = 0 AND importance > 1 AND COALESCE(accessed_at, updated_at) < ?`;
61324
61627
  const count = d.query(`SELECT COUNT(*) as c FROM memories WHERE ${deprioWhere}`).get(cutoff).c;
@@ -61346,7 +61649,9 @@ init_api_mode();
61346
61649
  init_helpers();
61347
61650
  function runCleanupViaApi() {
61348
61651
  const empty = { expired: 0, evicted: 0, archived: 0, unused_archived: 0, deprioritized: 0 };
61349
- const { status, data } = apiJson("POST", "/maintenance/cleanup");
61652
+ const { status, data } = apiJson("POST", "/maintenance/cleanup", undefined, {
61653
+ allow404: true
61654
+ });
61350
61655
  if (status !== 404 && data)
61351
61656
  return { ...empty, ...data };
61352
61657
  const legacy = apiJson("POST", "/memories/clean");
@@ -61637,7 +61942,7 @@ function getFocus(agentId) {
61637
61942
  init_helpers();
61638
61943
  function registerAgentCommands(program2) {
61639
61944
  const handleError = makeHandleError(program2);
61640
- program2.command("register-agent <name>").alias("init-agent").description("Register an agent (returns ID)").option("-d, --description <text>", "Agent description").option("-r, --role <role>", "Agent role").option("-p, --project <id>", "Lock agent to a project (sets active_project_id)").action((name, opts) => {
61945
+ program2.command("register-agent <name>").alias("init-agent").description("Register an agent (returns ID)").option("-d, --description <text>", "Agent description").option("-r, --role <role>", "Agent role").option("--project <id>", "Lock agent to a project (sets active_project_id)").action((name, opts) => {
61641
61946
  try {
61642
61947
  const globalOpts = program2.opts();
61643
61948
  const agent = registerAgent(name, undefined, opts.description, opts.role, opts.project);
@@ -62810,12 +63115,12 @@ function registerDoctorCommand(program2) {
62810
63115
  const activeProfile = getActiveProfile();
62811
63116
  const profiles = listProfiles();
62812
63117
  if (activeProfile) {
62813
- checks.push({ name: "Active profile", status: "ok", detail: `${activeProfile} (${profiles.length} total)` });
63118
+ checks.push({ name: "Profile metadata", status: "ok", detail: `${activeProfile} active (${profiles.length} total); verify runtime DB with storage mode` });
62814
63119
  } else {
62815
- checks.push({ name: "Active profile", status: "ok", detail: `default (~/.hasna/mementos/mementos.db) \u2014 ${profiles.length} profile(s) available` });
63120
+ checks.push({ name: "Profile metadata", status: "ok", detail: `none active \u2014 ${profiles.length} profile(s) available; verify runtime DB with storage mode` });
62816
63121
  }
62817
63122
  } catch (e) {
62818
- checks.push({ name: "Active profile", status: "warn", detail: e instanceof Error ? e.message : String(e) });
63123
+ checks.push({ name: "Profile metadata", status: "warn", detail: e instanceof Error ? e.message : String(e) });
62819
63124
  }
62820
63125
  try {
62821
63126
  const mementosUrl = process.env["MEMENTOS_URL"] || `http://127.0.0.1:19428`;
@@ -63124,7 +63429,7 @@ function registerConfigCommand(program2) {
63124
63429
  import chalk29 from "chalk";
63125
63430
  init_helpers();
63126
63431
  function registerProfileCommand(program2) {
63127
- const profileCmd = program2.command("profile").description("Manage memory profiles (isolated DBs per context)");
63432
+ const profileCmd = program2.command("profile").description("Manage named profile files and active-profile metadata");
63128
63433
  profileCmd.command("list").description("List all available profiles").action(() => {
63129
63434
  const globalOpts = program2.opts();
63130
63435
  const profiles = listProfiles();
@@ -63144,7 +63449,7 @@ function registerProfileCommand(program2) {
63144
63449
  }
63145
63450
  if (!active) {
63146
63451
  console.log(chalk29.dim(`
63147
- (no active profile \u2014 using default DB)`));
63452
+ (no active-profile metadata set)`));
63148
63453
  }
63149
63454
  });
63150
63455
  profileCmd.command("get").description("Show the currently active profile").action(() => {
@@ -63157,20 +63462,21 @@ function registerProfileCommand(program2) {
63157
63462
  console.log(chalk29.dim("(from MEMENTOS_PROFILE env var)"));
63158
63463
  }
63159
63464
  } else {
63160
- console.log(chalk29.dim("No active profile \u2014 using default DB (~/.hasna/mementos/mementos.db)"));
63465
+ console.log(chalk29.dim("No active-profile metadata set."));
63161
63466
  }
63162
63467
  });
63163
- profileCmd.command("set <name>").description("Switch to a named profile (creates the DB on first use)").action((name) => {
63468
+ profileCmd.command("set <name>").description("Set the active-profile metadata").action((name) => {
63164
63469
  const clean = name.trim().toLowerCase().replace(/[^a-z0-9_-]/g, "-");
63165
63470
  if (!clean) {
63166
63471
  console.error(chalk29.red("Invalid profile name. Use letters, numbers, hyphens, underscores."));
63167
63472
  process.exit(1);
63168
63473
  }
63169
63474
  setActiveProfile(clean);
63170
- console.log(chalk29.green(`\u2713 Switched to profile: ${clean}`));
63171
- console.log(chalk29.dim(` DB: ~/.hasna/mementos/profiles/${clean}.db (created on first use)`));
63475
+ console.log(chalk29.green(`\u2713 Active-profile metadata set: ${clean}`));
63476
+ console.log(chalk29.dim(` Profile file: ~/.hasna/mementos/profiles/${clean}.db`));
63477
+ console.log(chalk29.dim(" Run `mementos storage mode` to verify the live runtime database."));
63172
63478
  });
63173
- profileCmd.command("unset").description("Clear the active profile (revert to default DB)").action(() => {
63479
+ profileCmd.command("unset").description("Clear the active-profile metadata").action(() => {
63174
63480
  const was = getActiveProfile();
63175
63481
  setActiveProfile(null);
63176
63482
  if (was) {
@@ -63178,7 +63484,7 @@ function registerProfileCommand(program2) {
63178
63484
  } else {
63179
63485
  console.log(chalk29.dim("No active profile was set."));
63180
63486
  }
63181
- console.log(chalk29.dim(" Now using default DB: ~/.hasna/mementos/mementos.db"));
63487
+ console.log(chalk29.dim(" Run `mementos storage mode` to verify the live runtime database."));
63182
63488
  });
63183
63489
  profileCmd.command("delete <name>").description("Delete a profile and its DB file (irreversible)").option("-y, --yes", "Skip confirmation prompt").action(async (name, opts) => {
63184
63490
  if (!opts.yes) {
@@ -63971,19 +64277,20 @@ function registerMiscCommands(program2) {
63971
64277
  // src/cli/commands/system-mcp.ts
63972
64278
  import chalk38 from "chalk";
63973
64279
  function registerMcpCommand(program2) {
63974
- program2.command("mcp").description("Install mementos MCP server into Claude Code, Codex, or Gemini").option("--claude", "Install into Claude Code (~/.claude/.mcp.json)").option("--codex", "Install into Codex (~/.codex/config.toml)").option("--gemini", "Install into Gemini (~/.gemini/settings.json)").option("--all", "Install into all supported agents").option("--uninstall", "Remove mementos MCP from config").action((opts) => {
63975
- const { readFileSync: _rfs, writeFileSync: _wfs, existsSync: fileExists } = __require("fs");
64280
+ program2.command("mcp").description("Install mementos MCP server into Claude Code, Codex, Cursor, or Gemini").option("--claude", "Install into Claude Code (~/.claude/.mcp.json)").option("--codex", "Install into Codex (~/.codex/config.toml)").option("--cursor", "Install into Cursor (~/.cursor/mcp.json)").option("--gemini", "Install into Gemini (~/.gemini/settings.json)").option("--all", "Install into all supported agents").option("--uninstall", "Remove mementos MCP from config").action((opts) => {
64281
+ const { readFileSync: _rfs, writeFileSync: _wfs, existsSync: fileExists, mkdirSync: makeDir } = __require("fs");
63976
64282
  const { join: pathJoin } = __require("path");
63977
64283
  const { homedir: getHome } = __require("os");
63978
64284
  const home = getHome();
63979
64285
  const mementosCmd = process.argv[0]?.includes("bun") ? pathJoin(home, ".bun", "bin", "mementos-mcp") : "mementos-mcp";
63980
- const targets = opts.all ? ["claude", "codex", "gemini"] : [
64286
+ const targets = opts.all ? ["claude", "codex", "cursor", "gemini"] : [
63981
64287
  opts.claude ? "claude" : null,
63982
64288
  opts.codex ? "codex" : null,
64289
+ opts.cursor ? "cursor" : null,
63983
64290
  opts.gemini ? "gemini" : null
63984
64291
  ].filter(Boolean);
63985
64292
  if (targets.length === 0) {
63986
- console.log(chalk38.yellow("Specify a target: --claude, --codex, --gemini, or --all"));
64293
+ console.log(chalk38.yellow("Specify a target: --claude, --codex, --cursor, --gemini, or --all"));
63987
64294
  console.log(chalk38.gray("Example: mementos mcp --all"));
63988
64295
  return;
63989
64296
  }
@@ -64029,6 +64336,28 @@ args = []
64029
64336
  console.log(chalk38.yellow(`Codex config not found: ${configPath}`));
64030
64337
  }
64031
64338
  }
64339
+ if (target === "cursor") {
64340
+ const configDir = pathJoin(home, ".cursor");
64341
+ const configPath = pathJoin(configDir, "mcp.json");
64342
+ let config = {};
64343
+ if (fileExists(configPath)) {
64344
+ config = JSON.parse(_rfs(configPath, "utf-8"));
64345
+ } else if (opts.uninstall) {
64346
+ console.log(chalk38.yellow(`mementos was not installed in Cursor: ${configPath}`));
64347
+ continue;
64348
+ }
64349
+ const servers = config["mcpServers"] || {};
64350
+ if (opts.uninstall) {
64351
+ delete servers["mementos"];
64352
+ } else {
64353
+ servers["mementos"] = { command: mementosCmd, args: [] };
64354
+ }
64355
+ config["mcpServers"] = servers;
64356
+ makeDir(configDir, { recursive: true });
64357
+ _wfs(configPath, JSON.stringify(config, null, 2) + `
64358
+ `, "utf-8");
64359
+ console.log(chalk38.green(`${opts.uninstall ? "Removed from" : "Installed into"} Cursor: ${configPath}`));
64360
+ }
64032
64361
  if (target === "gemini") {
64033
64362
  const configPath = pathJoin(home, ".gemini", "settings.json");
64034
64363
  let config = {};
@@ -64060,7 +64389,7 @@ import chalk39 from "chalk";
64060
64389
  import { resolve as resolve19 } from "path";
64061
64390
  function registerWatchCommand(program2) {
64062
64391
  const handleError = makeHandleError(program2);
64063
- program2.command("watch").description("Watch for new and changed memories in real-time").option("-s, --scope <scope>", "Scope filter: global, shared, private").option("-c, --category <cat>", "Category filter: preference, fact, knowledge, history").option("--agent <name>", "Agent filter").option("--project <path>", "Project filter").option("--interval <ms>", "Poll interval in milliseconds", parseInt).action((opts) => {
64392
+ program2.command("watch").description("Watch for new and changed memories in real-time").option("--scope <scope>", "Scope filter: global, shared, private, working").option("-c, --category <cat>", "Category filter: preference, fact, knowledge, history, procedural, resource").option("--agent <name>", "Agent filter").option("--project <path>", "Project filter").option("--interval <ms>", "Poll interval in milliseconds", parseInt).action((opts) => {
64064
64393
  try {
64065
64394
  const globalOpts = program2.opts();
64066
64395
  const agentId = opts.agent || globalOpts.agent;
@@ -64656,6 +64985,35 @@ function getStorageSyncStatus(options = {}) {
64656
64985
  }
64657
64986
  }
64658
64987
 
64988
+ // src/db/store-backend.ts
64989
+ init_database();
64990
+ init_api_mode();
64991
+ init_storage();
64992
+ function resolveStoreBackend() {
64993
+ const apiMode = isApiMode();
64994
+ const apiConfig = getApiConfig();
64995
+ const storageMode = getStorageMode();
64996
+ const sources = getApiModeEnvSources();
64997
+ const backend = apiMode ? "cloud-api" : storageMode === "cloud" ? "cloud-postgres" : "local-sqlite";
64998
+ let selectedBy = "default";
64999
+ if (apiMode) {
65000
+ selectedBy = `${sources.urlKey} + ${sources.keyKey} (presence)`;
65001
+ } else if (backend === "cloud-postgres") {
65002
+ const modeKey = [MEMENTOS_STORAGE_ENV.mode, MEMENTOS_STORAGE_FALLBACK_ENV.mode].find((key) => process.env[key]?.trim());
65003
+ selectedBy = modeKey ?? sources.databaseUrlKey ?? "storage config file";
65004
+ }
65005
+ return {
65006
+ schema: "mementos.store_backend.v1",
65007
+ backend,
65008
+ api_mode: apiMode,
65009
+ storage_mode: storageMode,
65010
+ db_path: getDbPath(),
65011
+ api_endpoint: apiConfig?.baseUrl ?? null,
65012
+ api_key_present: Boolean(apiConfig?.apiKey),
65013
+ selected_by: selectedBy
65014
+ };
65015
+ }
65016
+
64659
65017
  // src/cli/commands/storage.ts
64660
65018
  function parseTables(raw) {
64661
65019
  if (!raw) {
@@ -64680,7 +65038,38 @@ function printSyncResult(result) {
64680
65038
  }
64681
65039
  }
64682
65040
  function installStorageSubcommands(storage, program2) {
64683
- storage.command("status").description("Show local database and remote storage sync status").option("--json", "Output JSON").action((opts) => {
65041
+ withoutStartupDbAccess(storage.command("mode").description("Show which store this process will actually read and write (no DB or network access)").option("--json", "Output JSON").action((opts) => {
65042
+ const useJson = Boolean(opts.json || program2.opts().json);
65043
+ let report;
65044
+ try {
65045
+ report = resolveStoreBackend();
65046
+ } catch (error) {
65047
+ const message = error instanceof Error ? error.message : String(error);
65048
+ if (useJson)
65049
+ outputJson2(true, { ok: false, error: message });
65050
+ else
65051
+ console.error(chalk40.red(message));
65052
+ process.exitCode = 1;
65053
+ return;
65054
+ }
65055
+ if (useJson) {
65056
+ outputJson2(true, report);
65057
+ return;
65058
+ }
65059
+ const label = report.backend === "local-sqlite" ? chalk40.green(report.backend) : chalk40.yellow(report.backend);
65060
+ console.log(`Backend: ${label}`);
65061
+ console.log(`Selected by: ${report.selected_by}`);
65062
+ console.log(`API mode: ${report.api_mode ? "yes" : "no"}`);
65063
+ console.log(`Storage mode: ${report.storage_mode}`);
65064
+ if (report.backend === "local-sqlite") {
65065
+ console.log(`Database: ${report.db_path}`);
65066
+ } else {
65067
+ console.log(`API endpoint: ${report.api_endpoint ?? "(none)"}`);
65068
+ console.log(`API key: ${report.api_key_present ? "configured" : "not configured"}`);
65069
+ console.log(`Local SQLite (not authoritative): ${report.db_path}`);
65070
+ }
65071
+ }));
65072
+ storage.command("status").description("Show local database, legacy sync, and storage runtime status").option("--json", "Output JSON").action((opts) => {
64684
65073
  const useJson = Boolean(opts.json || program2.opts().json);
64685
65074
  const status = getStorageSyncStatus();
64686
65075
  const config = getStorageConfig();
@@ -64837,7 +65226,7 @@ function installStorageSubcommands(storage, program2) {
64837
65226
  process.exitCode = 1;
64838
65227
  }
64839
65228
  });
64840
- storage.command("feedback").description("Save feedback locally").argument("<message>", "Feedback message").option("--email <email>", "Contact email").option("--category <category>", "Feedback category", "general").option("--json", "Output JSON").action((message, opts) => {
65229
+ storage.command("feedback").description("Save feedback to the selected store").argument("<message>", "Feedback message").option("--email <email>", "Contact email").option("--category <category>", "Feedback category", "general").option("--json", "Output JSON").action((message, opts) => {
64841
65230
  const useJson = Boolean(opts.json || program2.opts().json);
64842
65231
  try {
64843
65232
  const { saveFeedback: saveFeedback2 } = (init_feedback(), __toCommonJS(exports_feedback));
@@ -64864,7 +65253,7 @@ function installStorageSubcommands(storage, program2) {
64864
65253
  });
64865
65254
  }
64866
65255
  function registerStorageCommands(program2) {
64867
- const storage = program2.command("storage").description("Manage mementos local/remote storage sync");
65256
+ const storage = program2.command("storage").description("Inspect storage and manage migrations or legacy row sync");
64868
65257
  installStorageSubcommands(storage, program2);
64869
65258
  }
64870
65259
 
@@ -65140,7 +65529,7 @@ function parseMemoryLink(row) {
65140
65529
  function createMemoryLink(input, db) {
65141
65530
  const d = db || getDatabase();
65142
65531
  const id = shortUuid();
65143
- const timestamp = now2();
65532
+ const timestamp = now();
65144
65533
  d.run(`INSERT OR IGNORE INTO memory_links (id, source_memory_id, target_memory_id, relation_type, run_id, metadata, created_at)
65145
65534
  VALUES (?, ?, ?, ?, ?, ?, ?)`, [
65146
65535
  id,
@@ -65226,7 +65615,7 @@ function createRun(options, db) {
65226
65615
  options.projectId ?? null,
65227
65616
  options.agentId ?? null,
65228
65617
  options.dryRun ?? true ? 1 : 0,
65229
- now2()
65618
+ now()
65230
65619
  ]);
65231
65620
  return getRun(id, db);
65232
65621
  }
@@ -65271,7 +65660,7 @@ function persistAction(action, db) {
65271
65660
  action.reason,
65272
65661
  JSON.stringify(action.plannedChanges),
65273
65662
  action.applied ? 1 : 0,
65274
- now2()
65663
+ now()
65275
65664
  ]);
65276
65665
  }
65277
65666
  function markActionApplied(action, db) {
@@ -65646,11 +66035,11 @@ async function runConsolidation(options = {}) {
65646
66035
  actions = applied;
65647
66036
  }
65648
66037
  const summary = buildSummary2(actions);
65649
- run = updateRun(run.id, { status: "completed", summary, completed_at: now2() }, db);
66038
+ run = updateRun(run.id, { status: "completed", summary, completed_at: now() }, db);
65650
66039
  return { run, actions, dryRun, summary };
65651
66040
  } catch (error) {
65652
66041
  const message = error instanceof Error ? error.message : String(error);
65653
- run = updateRun(run.id, { status: "failed", error: message, completed_at: now2() }, db);
66042
+ run = updateRun(run.id, { status: "failed", error: message, completed_at: now() }, db);
65654
66043
  return {
65655
66044
  run,
65656
66045
  actions: [],
@@ -65723,7 +66112,7 @@ function createRun2(options, memoryIds, db) {
65723
66112
  options.provider ?? null,
65724
66113
  options.model ?? null,
65725
66114
  JSON.stringify(memoryIds),
65726
- now2()
66115
+ now()
65727
66116
  ]);
65728
66117
  return getRun2(id, db);
65729
66118
  }
@@ -65766,7 +66155,7 @@ function insertLessonRow(runId, lesson, db) {
65766
66155
  lesson.lesson,
65767
66156
  JSON.stringify(lesson.evidence),
65768
66157
  lesson.importance,
65769
- now2()
66158
+ now()
65770
66159
  ]);
65771
66160
  }
65772
66161
  function listToolEventsForTrajectory(options, db) {
@@ -66059,7 +66448,7 @@ async function reflectOnTrajectory(options) {
66059
66448
  }
66060
66449
  });
66061
66450
  }
66062
- run = updateRun2(run.id, { status: "completed", summary: criticResult.summary, completed_at: now2() }, db);
66451
+ run = updateRun2(run.id, { status: "completed", summary: criticResult.summary, completed_at: now() }, db);
66063
66452
  return {
66064
66453
  run,
66065
66454
  dryRun,
@@ -66073,7 +66462,7 @@ async function reflectOnTrajectory(options) {
66073
66462
  };
66074
66463
  } catch (error40) {
66075
66464
  const message = error40 instanceof Error ? error40.message : String(error40);
66076
- run = updateRun2(run.id, { status: "failed", error: message, completed_at: now2() }, db);
66465
+ run = updateRun2(run.id, { status: "failed", error: message, completed_at: now() }, db);
66077
66466
  return {
66078
66467
  run,
66079
66468
  dryRun,
@@ -66489,6 +66878,26 @@ function makeBrainsCommand() {
66489
66878
  return brains;
66490
66879
  }
66491
66880
 
66881
+ // src/cli/register-all.ts
66882
+ function registerAllCommands(program2) {
66883
+ registerInitCommand(program2);
66884
+ registerMemoryCommands(program2);
66885
+ registerInfoCommands(program2);
66886
+ registerIoCommands(program2);
66887
+ registerAgentCommands(program2);
66888
+ registerProjectCommands(program2);
66889
+ registerProjectPanelCommand(program2);
66890
+ registerEntityCommands(program2);
66891
+ registerRelationCommands(program2);
66892
+ registerGraphCommands(program2);
66893
+ registerSystemCommands(program2);
66894
+ registerStorageCommands(program2);
66895
+ registerConsolidationCommands(program2);
66896
+ program2.addCommand(makeBrainsCommand());
66897
+ registerEventsCommands(program2, { source: "mementos" });
66898
+ return program2;
66899
+ }
66900
+
66492
66901
  // src/cli/index.tsx
66493
66902
  function getPackageVersion2() {
66494
66903
  try {
@@ -66500,9 +66909,12 @@ function getPackageVersion2() {
66500
66909
  }
66501
66910
  }
66502
66911
  var program2 = new Command;
66503
- program2.name("mementos").description("Universal memory system for AI agents").version(getPackageVersion2()).option("-p, --project <path>", "Project path for scoping").option("-j, --json", "Output as JSON").option("-f, --format <fmt>", "Output format: compact, json, csv, yaml").option("-a, --agent <name>", "Agent name or ID").option("-s, --session <id>", "Session ID");
66912
+ program2.name("mementos").description("Universal memory system for AI agents").version(getPackageVersion2());
66913
+ applyGlobalOptions(program2);
66504
66914
  var startupWarningShown = false;
66505
- program2.hook("preAction", () => {
66915
+ program2.hook("preAction", (_thisCommand, actionCommand) => {
66916
+ if (skipsStartupDbAccess(actionCommand))
66917
+ return;
66506
66918
  if (startupWarningShown)
66507
66919
  return;
66508
66920
  startupWarningShown = true;
@@ -66513,19 +66925,5 @@ program2.hook("preAction", () => {
66513
66925
  }
66514
66926
  } catch {}
66515
66927
  });
66516
- registerInitCommand(program2);
66517
- registerMemoryCommands(program2);
66518
- registerInfoCommands(program2);
66519
- registerIoCommands(program2);
66520
- registerAgentCommands(program2);
66521
- registerProjectCommands(program2);
66522
- registerProjectPanelCommand(program2);
66523
- registerEntityCommands(program2);
66524
- registerRelationCommands(program2);
66525
- registerGraphCommands(program2);
66526
- registerSystemCommands(program2);
66527
- registerStorageCommands(program2);
66528
- registerConsolidationCommands(program2);
66529
- program2.addCommand(makeBrainsCommand());
66530
- registerEventsCommands(program2, { source: "mementos" });
66928
+ registerAllCommands(program2);
66531
66929
  program2.parse(process.argv);