@hasna/mementos 0.14.87 → 0.15.2

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 (88) hide show
  1. package/README.md +44 -9
  2. package/bun.lock +4 -62
  3. package/dist/cli/commands/info-history.d.ts.map +1 -1
  4. package/dist/cli/commands/info-stale.d.ts.map +1 -1
  5. package/dist/cli/commands/io-backup.d.ts.map +1 -1
  6. package/dist/cli/commands/io-export.d.ts.map +1 -1
  7. package/dist/cli/commands/io-restore.d.ts.map +1 -1
  8. package/dist/cli/commands/memory-cmd-chain.d.ts.map +1 -1
  9. package/dist/cli/commands/memory-cmd-crud.d.ts.map +1 -1
  10. package/dist/cli/commands/memory-cmd-recall.d.ts.map +1 -1
  11. package/dist/cli/commands/memory-cmd-search.d.ts.map +1 -1
  12. package/dist/cli/commands/memory-cmd-tail.d.ts.map +1 -1
  13. package/dist/cli/commands/memory-cmd-view.d.ts.map +1 -1
  14. package/dist/cli/commands/memory-cmd-when-to-use.d.ts.map +1 -1
  15. package/dist/cli/commands/storage.d.ts.map +1 -1
  16. package/dist/cli/commands/system-doctor.d.ts.map +1 -1
  17. package/dist/cli/commands/system-profile.d.ts.map +1 -1
  18. package/dist/cli/commands/system-status.d.ts +43 -0
  19. package/dist/cli/commands/system-status.d.ts.map +1 -0
  20. package/dist/cli/commands/system.d.ts.map +1 -1
  21. package/dist/cli/helpers.d.ts.map +1 -1
  22. package/dist/cli/index.js +3735 -2021
  23. package/dist/cli/register-all.d.ts +1 -1
  24. package/dist/cli/register-all.d.ts.map +1 -1
  25. package/dist/db/api-mode.d.ts +135 -37
  26. package/dist/db/api-mode.d.ts.map +1 -1
  27. package/dist/db/database.d.ts.map +1 -1
  28. package/dist/db/memories.d.ts.map +1 -1
  29. package/dist/db/pg-migrate.d.ts.map +1 -1
  30. package/dist/db/store-backend.d.ts +8 -2
  31. package/dist/db/store-backend.d.ts.map +1 -1
  32. package/dist/diagnostics/historical-project-registration-receipt.js +152 -11
  33. package/dist/generated/storage-kit/backend.d.ts +12 -2
  34. package/dist/generated/storage-kit/backend.d.ts.map +1 -1
  35. package/dist/generated/storage-kit/index.d.ts +1 -1
  36. package/dist/generated/storage-kit/index.d.ts.map +1 -1
  37. package/dist/generated/storage-kit/migrations.d.ts +21 -0
  38. package/dist/generated/storage-kit/migrations.d.ts.map +1 -1
  39. package/dist/generated/storage-kit/pool.d.ts +2 -5
  40. package/dist/generated/storage-kit/pool.d.ts.map +1 -1
  41. package/dist/generated/storage-kit/query.d.ts +1 -1
  42. package/dist/generated/storage-kit/query.d.ts.map +1 -1
  43. package/dist/index.d.ts +1 -2
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +3057 -1266
  46. package/dist/lib/config.d.ts.map +1 -1
  47. package/dist/lib/env.d.ts +10 -0
  48. package/dist/lib/env.d.ts.map +1 -0
  49. package/dist/lib/gatherer.d.ts.map +1 -1
  50. package/dist/lib/local-opt-in.d.ts +114 -0
  51. package/dist/lib/local-opt-in.d.ts.map +1 -0
  52. package/dist/lib/model-config.d.ts +1 -1
  53. package/dist/lib/paths.d.ts +60 -0
  54. package/dist/lib/paths.d.ts.map +1 -0
  55. package/dist/lib/profile-sync.d.ts.map +1 -1
  56. package/dist/lib/redact.d.ts +70 -10
  57. package/dist/lib/redact.d.ts.map +1 -1
  58. package/dist/lib/reflection.d.ts.map +1 -1
  59. package/dist/lib/search.d.ts.map +1 -1
  60. package/dist/lib/storage-sync.d.ts.map +1 -1
  61. package/dist/lib/sync.d.ts.map +1 -1
  62. package/dist/mcp/index.d.ts.map +1 -1
  63. package/dist/mcp/index.js +3108 -1276
  64. package/dist/project-registration.js +378 -153
  65. package/dist/sdk/index.d.ts +173 -8
  66. package/dist/sdk/index.d.ts.map +1 -1
  67. package/dist/sdk/index.js +953 -12
  68. package/dist/server/auth.d.ts +2 -0
  69. package/dist/server/auth.d.ts.map +1 -1
  70. package/dist/server/helpers.d.ts +48 -3
  71. package/dist/server/helpers.d.ts.map +1 -1
  72. package/dist/server/index.d.ts.map +1 -1
  73. package/dist/server/index.js +3146 -1324
  74. package/dist/server/routes/system-synthesis.d.ts.map +1 -1
  75. package/dist/storage.d.ts +18 -0
  76. package/dist/storage.d.ts.map +1 -1
  77. package/dist/storage.js +149 -83
  78. package/dist/test-support/store-isolation.d.ts +50 -11
  79. package/dist/test-support/store-isolation.d.ts.map +1 -1
  80. package/docker-entrypoint.sh +7 -0
  81. package/hasna.contract.json +17 -5
  82. package/package.json +8 -8
  83. package/postinstall.mjs +85 -0
  84. package/dashboard/bun.lock +0 -519
  85. package/dist/cli/brains.d.ts +0 -3
  86. package/dist/cli/brains.d.ts.map +0 -1
  87. package/dist/lib/retired-storage-mode.d.ts +0 -11
  88. package/dist/lib/retired-storage-mode.d.ts.map +0 -1
@@ -72,16 +72,7 @@ function serverDataBackendEnvKeys(name) {
72
72
  databaseUrlKeys: [`HASNA_${token}_DATABASE_URL`, `${token}_DATABASE_URL`]
73
73
  };
74
74
  }
75
- function legacyModeKeys(name) {
76
- const token = envToken(name);
77
- return [
78
- `HASNA_${token}_STORAGE_MODE`,
79
- `HASNA_${token}_MODE`,
80
- `${token}_STORAGE_MODE`,
81
- `${token}_MODE`
82
- ];
83
- }
84
- function firstEnv(env, keys) {
75
+ function firstDatabaseUrl(env, keys) {
85
76
  for (const key of keys) {
86
77
  const value = ownString(env, key)?.trim();
87
78
  if (value)
@@ -89,23 +80,8 @@ function firstEnv(env, keys) {
89
80
  }
90
81
  return null;
91
82
  }
92
- function firstDefinedEnvKey(env, keys) {
93
- for (const key of keys) {
94
- if (Object.hasOwn(env, key) && env[key] !== undefined)
95
- return key;
96
- }
97
- return null;
98
- }
99
- function assertNoLegacyStorageMode(name, env = process.env) {
100
- const legacyKey = firstDefinedEnvKey(env, legacyModeKeys(name));
101
- if (!legacyKey)
102
- return;
103
- const canonicalDatabaseUrl = serverDataBackendEnvKeys(name).databaseUrlKeys[0];
104
- throw new Error(`${legacyKey} was removed. Delete the storage-mode variable; ` + `set ${canonicalDatabaseUrl} to select the postgresql server backend, ` + `or leave it unset for sqlite.`);
105
- }
106
83
  function resolveServerDataBackend(name, env = process.env) {
107
- assertNoLegacyStorageMode(name, env);
108
- const databaseUrl = firstEnv(env, serverDataBackendEnvKeys(name).databaseUrlKeys);
84
+ const databaseUrl = firstDatabaseUrl(env, serverDataBackendEnvKeys(name).databaseUrlKeys);
109
85
  if (!databaseUrl) {
110
86
  return {
111
87
  backend: "sqlite",
@@ -122,41 +98,125 @@ function resolveServerDataBackend(name, env = process.env) {
122
98
  };
123
99
  }
124
100
  function resolveDatabaseUrl(name, env = process.env) {
125
- assertNoLegacyStorageMode(name, env);
126
- const hit = firstEnv(env, serverDataBackendEnvKeys(name).databaseUrlKeys);
127
- return hit?.value ?? null;
101
+ return firstDatabaseUrl(env, serverDataBackendEnvKeys(name).databaseUrlKeys)?.value ?? null;
128
102
  }
129
103
  var init_backend = () => {};
130
104
 
131
- // src/lib/retired-storage-mode.ts
132
- function firstDefinedEnvKey2(env, keys) {
133
- for (const key of keys) {
134
- if (Object.hasOwn(env, key) && env[key] !== undefined)
135
- return key;
105
+ // src/lib/paths.ts
106
+ import { existsSync } from "fs";
107
+ import { homedir } from "os";
108
+ import { join, resolve } from "path";
109
+ import { homedir as pathsResolverHomedir } from "os";
110
+ import { join as pathsResolverJoin } from "path";
111
+ function pathsResolverAssertApp(app) {
112
+ if (typeof app !== "string" || app.length === 0) {
113
+ throw new TypeError("paths: app must be a non-empty string");
114
+ }
115
+ if (!PATHS_RESOLVER_APP_SLUG_RE.test(app)) {
116
+ throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
117
+ }
118
+ }
119
+ function pathsResolverAssertKind(kind) {
120
+ if (!Object.keys(PATHS_RESOLVER_KIND_ENV).includes(kind)) {
121
+ throw new TypeError(`paths: invalid path kind "${kind}" \u2014 expected one of ${Object.keys(PATHS_RESOLVER_KIND_ENV).join(", ")}`);
122
+ }
123
+ }
124
+ function pathsResolverBaseDir(kind, options) {
125
+ pathsResolverAssertKind(kind);
126
+ const env = options.env ?? process.env;
127
+ const override = env[PATHS_RESOLVER_KIND_ENV[kind]];
128
+ if (typeof override === "string" && override.length > 0)
129
+ return override;
130
+ const home = options.home ?? pathsResolverHomedir();
131
+ const platform = options.platform ?? process.platform;
132
+ if (platform === "darwin") {
133
+ switch (kind) {
134
+ case "data":
135
+ return pathsResolverJoin(home, "Library", "Application Support", "Hasna");
136
+ case "cache":
137
+ return pathsResolverJoin(home, "Library", "Caches", "Hasna");
138
+ case "state":
139
+ return pathsResolverJoin(home, "Library", "Logs", "Hasna");
140
+ }
141
+ }
142
+ switch (kind) {
143
+ case "data":
144
+ return pathsResolverJoin(home, ".local", "share", "hasna");
145
+ case "state":
146
+ return pathsResolverJoin(home, ".local", "state", "hasna");
147
+ case "cache":
148
+ return pathsResolverJoin(home, ".cache", "hasna");
136
149
  }
137
- return null;
138
150
  }
139
- function assertNoLegacyStorageMode2(env = process.env) {
140
- const legacyKey = firstDefinedEnvKey2(env, LEGACY_STORAGE_MODE_KEYS);
141
- if (!legacyKey)
142
- return;
143
- throw new Error(`${legacyKey} was removed. Deployment modes no longer exist: delete the storage-mode variable. ` + `The client uses the local SQLite store, or the HTTP API selected by ` + `HASNA_MEMENTOS_API_URL + HASNA_MEMENTOS_API_KEY. ` + `On the server, set HASNA_MEMENTOS_DATABASE_URL to select the postgresql backend, ` + `or leave it unset for sqlite.`);
144
- }
145
- var LEGACY_STORAGE_MODE_KEYS;
146
- var init_retired_storage_mode = __esm(() => {
147
- LEGACY_STORAGE_MODE_KEYS = [
148
- "HASNA_MEMENTOS_STORAGE_MODE",
149
- "HASNA_MEMENTOS_MODE",
150
- "MEMENTOS_STORAGE_MODE",
151
- "MEMENTOS_MODE"
152
- ];
151
+ function pathsResolverResolve(kind, options) {
152
+ pathsResolverAssertApp(options.app);
153
+ const appSegment = options.internal === true ? pathsResolverJoin("internal", options.app) : options.app;
154
+ return pathsResolverJoin(pathsResolverBaseDir(kind, options), appSegment);
155
+ }
156
+ function dataDir(options) {
157
+ return pathsResolverResolve("data", options);
158
+ }
159
+ function effectiveHome() {
160
+ return process.env["HOME"] || process.env["USERPROFILE"] || homedir() || "/tmp";
161
+ }
162
+ function legacyDataRoot() {
163
+ return join(effectiveHome(), ".hasna", "mementos");
164
+ }
165
+ function resolverDataRoot() {
166
+ return dataDir({
167
+ app: "mementos",
168
+ home: process.env["HOME"] || process.env["USERPROFILE"] || undefined
169
+ });
170
+ }
171
+ function adoptResolverDataRoot(resolved, env = process.env) {
172
+ const dataOverride = env.HASNA_DATA_HOME;
173
+ if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
174
+ return true;
175
+ return existsSync(join(resolved, "mementos.db"));
176
+ }
177
+ function exactDataRoot() {
178
+ for (const key of ["HASNA_MEMENTOS_HOME", "MEMENTOS_HOME"]) {
179
+ const dir = process.env[key]?.trim();
180
+ if (dir)
181
+ return resolve(dir);
182
+ }
183
+ return;
184
+ }
185
+ function getDataRoot() {
186
+ const exact = exactDataRoot();
187
+ if (exact)
188
+ return exact;
189
+ const resolved = resolverDataRoot();
190
+ return adoptResolverDataRoot(resolved) ? resolve(resolved) : resolve(legacyDataRoot());
191
+ }
192
+ var PATHS_RESOLVER_KIND_ENV, PATHS_RESOLVER_APP_SLUG_RE;
193
+ var init_paths = __esm(() => {
194
+ PATHS_RESOLVER_KIND_ENV = {
195
+ data: "HASNA_DATA_HOME",
196
+ state: "HASNA_STATE_HOME",
197
+ cache: "HASNA_CACHE_HOME"
198
+ };
199
+ PATHS_RESOLVER_APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
200
+ });
201
+
202
+ // src/lib/env.ts
203
+ var alias = (canonical, legacy) => process.env[canonical] ?? process.env[legacy], env;
204
+ var init_env = __esm(() => {
205
+ env = {
206
+ apiKey: () => alias("HASNA_MEMENTOS_API_KEY", "MEMENTOS_API_KEY"),
207
+ defaultScope: () => alias("HASNA_MEMENTOS_DEFAULT_SCOPE", "MEMENTOS_DEFAULT_SCOPE"),
208
+ defaultCategory: () => alias("HASNA_MEMENTOS_DEFAULT_CATEGORY", "MEMENTOS_DEFAULT_CATEGORY"),
209
+ defaultImportance: () => alias("HASNA_MEMENTOS_DEFAULT_IMPORTANCE", "MEMENTOS_DEFAULT_IMPORTANCE"),
210
+ pgsyncQueryTimeoutMs: () => alias("HASNA_MEMENTOS_PGSYNC_QUERY_TIMEOUT_MS", "MEMENTOS_PGSYNC_QUERY_TIMEOUT_MS"),
211
+ reflectProvider: () => alias("HASNA_MEMENTOS_REFLECT_PROVIDER", "MEMENTOS_REFLECT_PROVIDER"),
212
+ reflectModel: () => alias("HASNA_MEMENTOS_REFLECT_MODEL", "MEMENTOS_REFLECT_MODEL")
213
+ };
153
214
  });
154
215
 
155
216
  // src/storage.ts
156
217
  import { Database } from "bun:sqlite";
157
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
158
- import { homedir } from "os";
159
- import { join } from "path";
218
+ import { existsSync as existsSync2, mkdirSync, readFileSync, writeFileSync } from "fs";
219
+ import { join as join2 } from "path";
160
220
  import { fileURLToPath } from "url";
161
221
  import { Worker } from "worker_threads";
162
222
  import pg from "pg";
@@ -181,6 +241,7 @@ class SqliteAdapter {
181
241
  db;
182
242
  constructor(path) {
183
243
  this.db = new Database(path, { create: true });
244
+ this.db.exec("PRAGMA busy_timeout = 5000");
184
245
  this.db.exec("PRAGMA journal_mode = WAL");
185
246
  this.db.exec("PRAGMA foreign_keys = ON");
186
247
  }
@@ -415,7 +476,7 @@ function readEnv(name) {
415
476
  return value ? value : null;
416
477
  }
417
478
  function readConfigFile() {
418
- if (!existsSync(STORAGE_CONFIG_PATH)) {
479
+ if (!existsSync2(STORAGE_CONFIG_PATH)) {
419
480
  return {};
420
481
  }
421
482
  try {
@@ -431,9 +492,9 @@ function getConfigPath() {
431
492
  return STORAGE_CONFIG_PATH;
432
493
  }
433
494
  function getStorageDatabaseEnv() {
434
- for (const env of DATABASE_ENV_NAMES) {
435
- if (readEnv(env.name))
436
- return env;
495
+ for (const env2 of DATABASE_ENV_NAMES) {
496
+ if (readEnv(env2.name))
497
+ return env2;
437
498
  }
438
499
  return null;
439
500
  }
@@ -443,20 +504,19 @@ function getStorageEnvName(key) {
443
504
  return readEnv(canonical) || !readEnv(fallback) ? canonical : fallback;
444
505
  }
445
506
  function getStorageDatabaseUrl() {
446
- const env = getStorageDatabaseEnv();
447
- return env ? readEnv(env.name) : null;
507
+ const env2 = getStorageDatabaseEnv();
508
+ return env2 ? readEnv(env2.name) : null;
448
509
  }
449
510
  function getStorageDatabaseEnvName() {
450
511
  return getStorageEnvName("databaseUrl");
451
512
  }
452
- function getStorageBackend(env = process.env) {
453
- return resolveServerDataBackend("mementos", env).backend;
513
+ function getStorageBackend(env2 = process.env) {
514
+ return resolveServerDataBackend("mementos", env2).backend;
454
515
  }
455
- function getStorageBackendDatabaseUrl(env = process.env) {
456
- return resolveDatabaseUrl("mementos", env);
516
+ function getStorageBackendDatabaseUrl(env2 = process.env) {
517
+ return resolveDatabaseUrl("mementos", env2);
457
518
  }
458
519
  function getStorageConfig() {
459
- assertNoLegacyStorageMode2();
460
520
  const fileConfig = readConfigFile();
461
521
  const merged = {
462
522
  ...DEFAULT_STORAGE_CONFIG,
@@ -543,14 +603,14 @@ function storageEnvStatus(key) {
543
603
  };
544
604
  }
545
605
  function postgresBackendStatus(config) {
546
- const env = getStorageDatabaseEnv();
547
- const envUrl = env ? readEnv(env.name) : null;
548
- if (env && envUrl) {
606
+ const env2 = getStorageDatabaseEnv();
607
+ const envUrl = env2 ? readEnv(env2.name) : null;
608
+ if (env2 && envUrl) {
549
609
  const validation = validatePostgresConnectionString(envUrl);
550
610
  return {
551
611
  configured: validation.ok,
552
612
  source: "env",
553
- env_name: env.name,
613
+ env_name: env2.name,
554
614
  redacted_url: validation.redacted_url,
555
615
  missing: [],
556
616
  issues: validation.issues,
@@ -596,7 +656,6 @@ function getSafeStorageConfigSummary(config = getStorageConfig()) {
596
656
  };
597
657
  }
598
658
  function getStorageStatus() {
599
- assertNoLegacyStorageMode2();
600
659
  const config = getStorageConfig();
601
660
  const backend = getStorageBackend();
602
661
  const postgresRequested = backend === "postgresql";
@@ -677,11 +736,7 @@ function saveStorageConfig(config) {
677
736
  function getConfiguredConnectionString() {
678
737
  return getStorageDatabaseUrl() ?? undefined;
679
738
  }
680
- function getStorageConnectionString(dbName = "mementos") {
681
- assertNoLegacyStorageMode2();
682
- if (!isServerContext()) {
683
- throw new Error("Refusing to construct an RDS Postgres DSN outside the mementos-serve server. " + "The raw database DSN is NEVER distributed to client machines. " + "Clients must use the HTTP API: set HASNA_MEMENTOS_API_URL and " + "HASNA_MEMENTOS_API_KEY (and unset HASNA_MEMENTOS_DATABASE_URL).");
684
- }
739
+ function resolveConfiguredConnectionString(dbName) {
685
740
  const envConnectionString = getConfiguredConnectionString();
686
741
  if (envConnectionString) {
687
742
  const validation = validatePostgresConnectionString(envConnectionString);
@@ -700,7 +755,7 @@ function getStorageConnectionString(dbName = "mementos") {
700
755
  missing.push("storage.rds.username");
701
756
  }
702
757
  if (missing.length > 0) {
703
- throw new Error(`Remote storage database is not configured. Missing ${missing.join(", ")}. Set HASNA_MEMENTOS_DATABASE_URL or configure ~/.hasna/mementos/storage/config.json.`);
758
+ throw new Error(`Remote storage database is not configured. Missing ${missing.join(", ")}. Set HASNA_MEMENTOS_DATABASE_URL or configure ${STORAGE_CONFIG_PATH}.`);
704
759
  }
705
760
  const password = process.env[password_env];
706
761
  if (!password) {
@@ -709,6 +764,15 @@ function getStorageConnectionString(dbName = "mementos") {
709
764
  const sslParam = ssl ? "?sslmode=require" : "";
710
765
  return `postgres://${username}:${encodeURIComponent(password)}@${host}:${port}/${dbName}${sslParam}`;
711
766
  }
767
+ function getStorageConnectionString(dbName = "mementos") {
768
+ if (!isServerContext()) {
769
+ throw new Error("Refusing to construct an RDS Postgres DSN outside the mementos-serve server. " + "The raw database DSN is NEVER distributed to client machines. " + "Clients must use the HTTP API: set HASNA_MEMENTOS_API_URL and " + "HASNA_MEMENTOS_API_KEY (and unset HASNA_MEMENTOS_DATABASE_URL).");
770
+ }
771
+ return resolveConfiguredConnectionString(dbName);
772
+ }
773
+ function getStorageConnectionStringForOperator(dbName = "mementos") {
774
+ return resolveConfiguredConnectionString(dbName);
775
+ }
712
776
  function isSyncExcludedTable(table) {
713
777
  return SYNC_EXCLUDED_TABLE_PATTERNS.some((pattern) => pattern.test(table));
714
778
  }
@@ -877,7 +941,8 @@ CREATE TABLE IF NOT EXISTS _sync_meta (
877
941
  )`;
878
942
  var init_storage = __esm(() => {
879
943
  init_backend();
880
- init_retired_storage_mode();
944
+ init_paths();
945
+ init_env();
881
946
  PgSyncPool = class PgSyncPool {
882
947
  worker;
883
948
  status;
@@ -887,7 +952,7 @@ var init_storage = __esm(() => {
887
952
  generation = 0;
888
953
  static DATA_BYTES = 128 * 1024 * 1024;
889
954
  static queryTimeoutMs() {
890
- const raw = process.env["MEMENTOS_PGSYNC_QUERY_TIMEOUT_MS"]?.trim();
955
+ const raw = env.pgsyncQueryTimeoutMs()?.trim();
891
956
  const parsed = raw ? Number(raw) : Number.NaN;
892
957
  return Number.isFinite(parsed) && parsed > 0 ? parsed : 60000;
893
958
  }
@@ -895,12 +960,12 @@ var init_storage = __esm(() => {
895
960
  const ext = import.meta.url.endsWith(".ts") ? ".ts" : ".js";
896
961
  const here = fileURLToPath(new URL(".", import.meta.url));
897
962
  const candidates = [
898
- join(here, `pg-sync-worker${ext}`),
899
- join(here, "..", `pg-sync-worker${ext}`),
900
- join(here, "..", "..", `pg-sync-worker${ext}`)
963
+ join2(here, `pg-sync-worker${ext}`),
964
+ join2(here, "..", `pg-sync-worker${ext}`),
965
+ join2(here, "..", "..", `pg-sync-worker${ext}`)
901
966
  ];
902
967
  for (const candidate of candidates) {
903
- if (existsSync(candidate))
968
+ if (existsSync2(candidate))
904
969
  return candidate;
905
970
  }
906
971
  return candidates[0];
@@ -988,7 +1053,7 @@ var init_storage = __esm(() => {
988
1053
  MEMENTOS_STORAGE_FALLBACK_ENV = {
989
1054
  databaseUrl: "MEMENTOS_DATABASE_URL"
990
1055
  };
991
- LOCAL_DATA_DIR = join(homedir(), ".hasna", "mementos");
1056
+ LOCAL_DATA_DIR = getDataRoot();
992
1057
  DEFAULT_STORAGE_CONFIG = {
993
1058
  rds: {
994
1059
  host: "",
@@ -1003,8 +1068,8 @@ var init_storage = __esm(() => {
1003
1068
  schedule_minutes: 0
1004
1069
  }
1005
1070
  };
1006
- STORAGE_CONFIG_DIR = join(LOCAL_DATA_DIR, "storage");
1007
- STORAGE_CONFIG_PATH = join(STORAGE_CONFIG_DIR, "config.json");
1071
+ STORAGE_CONFIG_DIR = join2(LOCAL_DATA_DIR, "storage");
1072
+ STORAGE_CONFIG_PATH = join2(STORAGE_CONFIG_DIR, "config.json");
1008
1073
  DATABASE_ENV_NAMES = [
1009
1074
  { name: MEMENTOS_STORAGE_ENV.databaseUrl, deprecated: false },
1010
1075
  { name: MEMENTOS_STORAGE_FALLBACK_ENV.databaseUrl, deprecated: false }
@@ -1028,28 +1093,96 @@ var init_storage = __esm(() => {
1028
1093
  ];
1029
1094
  });
1030
1095
 
1096
+ // src/lib/local-opt-in.ts
1097
+ import {
1098
+ clientTransportEnvKeys,
1099
+ credentialOverrideEnvKey,
1100
+ credentialPointerEnvKey,
1101
+ CREDENTIAL_PROFILE_ENV_KEY
1102
+ } from "@hasna/contracts/client";
1103
+ function isMementosLocalOptIn(env2 = process.env) {
1104
+ return MEMENTOS_LOCAL_OPT_IN_ENV_KEYS.some((key) => (env2[key] ?? "").trim() !== "");
1105
+ }
1106
+ function hasExplicitLocalDbPath(env2 = process.env) {
1107
+ return MEMENTOS_DB_PATH_ENV_KEYS.some((key) => (env2[key] ?? "").trim() !== "");
1108
+ }
1109
+ function mementosAuthorityEnvKeys() {
1110
+ const keys = clientTransportEnvKeys("mementos");
1111
+ return [
1112
+ ...keys.apiUrlKeys,
1113
+ ...keys.apiKeyKeys,
1114
+ credentialOverrideEnvKey("mementos"),
1115
+ credentialPointerEnvKey("mementos"),
1116
+ CREDENTIAL_PROFILE_ENV_KEY
1117
+ ];
1118
+ }
1119
+ function hasMementosEnvAuthorityIntent(env2 = process.env) {
1120
+ return mementosAuthorityEnvKeys().some((key) => (env2[key] ?? "").trim() !== "");
1121
+ }
1122
+ function selectsMementosLocalStore(env2 = process.env) {
1123
+ if (hasExplicitLocalDbPath(env2))
1124
+ return true;
1125
+ return !hasMementosEnvAuthorityIntent(env2) && isMementosLocalOptIn(env2);
1126
+ }
1127
+ function mementosResolverEnv(env2) {
1128
+ const blanks = mementosAuthorityEnvKeys().filter((key) => (key in env2) && (env2[key] ?? "").trim() === "");
1129
+ if (blanks.length === 0)
1130
+ return env2;
1131
+ const next = { ...env2 };
1132
+ for (const key of blanks)
1133
+ delete next[key];
1134
+ return next;
1135
+ }
1136
+ function isAmbientMementosEnv(env2) {
1137
+ if (typeof process !== "undefined" && env2 === process.env)
1138
+ return true;
1139
+ return env2[CONTRACTS_AMBIENT_ENVIRONMENT] === true;
1140
+ }
1141
+ function mementosResolverInputs(env2, credentials = {}) {
1142
+ const normalised = mementosResolverEnv(env2);
1143
+ if (normalised === env2)
1144
+ return { env: normalised, credentials };
1145
+ const keychain = { ...credentials.keychain };
1146
+ if (keychain.enabled === undefined && keychain.run === undefined) {
1147
+ keychain.enabled = isAmbientMementosEnv(env2);
1148
+ }
1149
+ return { env: normalised, credentials: { ...credentials, keychain } };
1150
+ }
1151
+ var MEMENTOS_LOCAL_OPT_IN_ENV_KEYS, MEMENTOS_DB_PATH_ENV_KEYS, CONTRACTS_AMBIENT_ENVIRONMENT;
1152
+ var init_local_opt_in = __esm(() => {
1153
+ MEMENTOS_LOCAL_OPT_IN_ENV_KEYS = ["HASNA_MEMENTOS_LOCAL", "MEMENTOS_LOCAL"];
1154
+ MEMENTOS_DB_PATH_ENV_KEYS = ["HASNA_MEMENTOS_DB_PATH", "MEMENTOS_DB_PATH"];
1155
+ CONTRACTS_AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
1156
+ });
1157
+
1031
1158
  // src/db/api-mode.ts
1032
1159
  import { tmpdir } from "os";
1033
- import { join as join2 } from "path";
1160
+ import { join as join3 } from "path";
1034
1161
  import { writeFileSync as writeFileSync2, unlinkSync } from "fs";
1035
1162
  import { randomUUID } from "crypto";
1036
- function firstEnv2(keys) {
1163
+ import {
1164
+ clientTransportEnvKeys as clientTransportEnvKeys2,
1165
+ resolveClientTransport,
1166
+ resolveCredential,
1167
+ toV1BaseUrl,
1168
+ ClientTransportConfigurationError
1169
+ } from "@hasna/contracts/client";
1170
+ function firstEnv(keys, env2 = process.env) {
1037
1171
  for (const k of keys) {
1038
- const v = process.env[k]?.trim();
1172
+ const v = env2[k]?.trim();
1039
1173
  if (v)
1040
1174
  return v;
1041
1175
  }
1042
1176
  return;
1043
1177
  }
1044
- function firstEnvKey(keys) {
1045
- for (const k of keys) {
1046
- if (process.env[k]?.trim())
1047
- return k;
1048
- }
1049
- return null;
1178
+ function hasDatabaseUrl(env2 = process.env) {
1179
+ return Boolean(firstEnv(DATABASE_URL_ENV_KEYS, env2));
1050
1180
  }
1051
- function hasDatabaseUrl() {
1052
- return Boolean(firstEnv2(DATABASE_URL_ENV_KEYS));
1181
+ function assertCleanResolvedBase(baseUrl) {
1182
+ if (/[?#]/.test(baseUrl)) {
1183
+ throw new Error("mementos base URL must not contain userinfo, query, or fragment data");
1184
+ }
1185
+ return baseUrl;
1053
1186
  }
1054
1187
  function isLoopbackHost(rawHost) {
1055
1188
  const host = rawHost.replace(/^\[/, "").replace(/\]$/, "").toLowerCase();
@@ -1070,50 +1203,61 @@ function assertRequestAllowedUnderTest(baseUrl) {
1070
1203
  return;
1071
1204
  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.
1072
1205
  ` + ` host : ${host || "(unparseable base URL)"}
1073
- ` + ` 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.
1206
+ ` + " 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.
1074
1207
  ` + " fix : build the child/process env via src/test-support/store-isolation.ts, or point the " + `suite at a loopback stub.
1075
1208
  ` + ` override : set ${ALLOW_REMOTE_API_IN_TESTS_ENV}=1 only for a test that must reach a remote endpoint.`);
1076
1209
  }
1077
- function normalizeBase(raw) {
1078
- let base = raw.trim().replace(/\/+$/, "");
1079
- if (/\/(v1|api)$/.test(base))
1080
- return base;
1081
- return `${base}/v1`;
1082
- }
1083
- function assertUnambiguousStoreEnv(env = process.env) {
1084
- assertNoLegacyStorageMode2(env);
1085
- if (firstEnvKey(DB_PATH_ENV_KEYS))
1086
- return;
1087
- if (hasDatabaseUrl())
1088
- return;
1089
- const urlKey = firstEnvKey(API_URL_ENV_KEYS);
1090
- const keyKey = firstEnvKey(API_KEY_ENV_KEYS);
1091
- if (urlKey && !keyKey) {
1092
- 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.`);
1093
- }
1094
- if (keyKey && !urlKey) {
1095
- 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.`);
1096
- }
1210
+ function toStoreConfigError(error) {
1211
+ const message = error instanceof Error ? error.message : String(error);
1212
+ return new MementosStoreConfigError(`${message} Local SQLite is opt-in only (${DB_PATH_ENV_KEYS[0]}, or ${MEMENTOS_LOCAL_OPT_IN_ENV_KEYS[0]} = 1) and is disabled by default \u2014 failing closed`);
1097
1213
  }
1098
- function getApiConfig() {
1099
- assertUnambiguousStoreEnv();
1100
- if (firstEnvKey(DB_PATH_ENV_KEYS))
1214
+ function getApiConfig(env2 = process.env, options = {}) {
1215
+ if (hasDatabaseUrl(env2))
1101
1216
  return null;
1102
- const rawBase = firstEnv2(API_URL_ENV_KEYS);
1103
- const apiKey = firstEnv2(API_KEY_ENV_KEYS);
1104
- if (!rawBase || !apiKey)
1217
+ if (selectsMementosLocalStore(env2))
1105
1218
  return null;
1106
- return { baseUrl: normalizeBase(rawBase), apiKey };
1219
+ const inputs = mementosResolverInputs(env2, options.credentials);
1220
+ let credential;
1221
+ try {
1222
+ credential = resolveCredential("mementos", inputs.env, inputs.credentials);
1223
+ } catch (error) {
1224
+ throw toStoreConfigError(error);
1225
+ }
1226
+ let resolution;
1227
+ try {
1228
+ resolution = resolveClientTransport("mementos", inputs.env, {
1229
+ credentials: credential ? { ...inputs.credentials, apiKey: credential.apiKey } : inputs.credentials
1230
+ });
1231
+ } catch (error) {
1232
+ if (error instanceof ClientTransportConfigurationError && credential === null && /is not set and no API key could be resolved/.test(error.message)) {
1233
+ return null;
1234
+ }
1235
+ throw toStoreConfigError(error);
1236
+ }
1237
+ return { baseUrl: assertCleanResolvedBase(resolution.baseUrl), apiKey: credential.apiKey };
1107
1238
  }
1108
1239
  function isApiMode() {
1109
1240
  if (hasDatabaseUrl())
1110
1241
  return false;
1111
1242
  return getApiConfig() !== null;
1112
1243
  }
1244
+ function assertClientStoreConfigured(env2 = process.env, options = {}) {
1245
+ if (selectsMementosLocalStore(env2))
1246
+ return;
1247
+ if (hasDatabaseUrl(env2))
1248
+ return;
1249
+ const config = getApiConfig(env2, options);
1250
+ if (config !== null)
1251
+ return;
1252
+ throw new MementosStoreConfigError(unconfiguredStoreMessage());
1253
+ }
1254
+ function unconfiguredStoreMessage() {
1255
+ return "mementos is not configured to reach any memory store, and will NOT fall back to the " + "on-box SQLite store (~/.hasna/mementos/mementos.db). " + "No credential could be resolved from the Keychain item hasna.credentials.mementos.api-key " + "(macOS only), ~/.hasna/mementos/config/credentials, or " + `${API_KEY_ENV_KEYS[0]}; the authority would be the fleet gateway ${"https://api.hasna.com/mementos"} ` + `(or ${API_URL_ENV_KEYS[0]} if set). ` + `Set ${API_URL_ENV_KEYS[0]} and ${API_KEY_ENV_KEYS[0]} (the resolver also accepts the legacy ` + `${API_URL_ENV_KEYS[1]} / ${API_KEY_ENV_KEYS[1]} aliases for one release) to use the fleet memory ` + `API, or opt into an explicit local SQLite file with ${DB_PATH_ENV_KEYS[0]} / ` + `${DB_PATH_ENV_KEYS[1]} (or ${MEMENTOS_LOCAL_OPT_IN_ENV_KEYS[0]}=1).`;
1256
+ }
1113
1257
  function apiRequestRaw(method, path, body) {
1114
1258
  const cfg = getApiConfig();
1115
1259
  if (!cfg)
1116
- throw new Error("api-mode: not configured (HASNA_MEMENTOS_API_URL / HASNA_MEMENTOS_API_KEY)");
1260
+ throw new Error("api-mode: not configured (no Hasna mementos credential resolved)");
1117
1261
  assertRequestAllowedUnderTest(cfg.baseUrl);
1118
1262
  const url = `${cfg.baseUrl}${path.startsWith("/") ? path : `/${path}`}`;
1119
1263
  const hasBody = body !== undefined && body !== null;
@@ -1140,7 +1284,7 @@ x-api-key: ${cfg.apiKey}
1140
1284
  ];
1141
1285
  let bodyFile;
1142
1286
  if (hasBody) {
1143
- bodyFile = join2(tmpdir(), `mem-req-${process.pid}-${randomUUID()}.json`);
1287
+ bodyFile = join3(tmpdir(), `mem-req-${process.pid}-${randomUUID()}.json`);
1144
1288
  writeFileSync2(bodyFile, JSON.stringify(body), { mode: 384 });
1145
1289
  args.push("--data-binary", `@${bodyFile}`);
1146
1290
  }
@@ -1228,13 +1372,13 @@ function toQuery(params) {
1228
1372
  const s = sp.toString();
1229
1373
  return s ? `?${s}` : "";
1230
1374
  }
1231
- 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";
1375
+ var API_URL_ENV_KEYS, API_KEY_ENV_KEYS, DATABASE_URL_ENV_KEYS, DB_PATH_ENV_KEYS, ALLOW_REMOTE_API_IN_TESTS_ENV = "MEMENTOS_ALLOW_REMOTE_API_IN_TESTS", MementosStoreConfigError, ApiRequestError, DEFAULT_TIMEOUT_S = "45";
1232
1376
  var init_api_mode = __esm(() => {
1233
- init_retired_storage_mode();
1234
- API_URL_ENV_KEYS = ["HASNA_MEMENTOS_API_URL", "MEMENTOS_API_URL"];
1235
- API_KEY_ENV_KEYS = ["HASNA_MEMENTOS_API_KEY", "MEMENTOS_API_KEY"];
1377
+ init_local_opt_in();
1378
+ API_URL_ENV_KEYS = clientTransportEnvKeys2("mementos").apiUrlKeys;
1379
+ API_KEY_ENV_KEYS = clientTransportEnvKeys2("mementos").apiKeyKeys;
1236
1380
  DATABASE_URL_ENV_KEYS = ["HASNA_MEMENTOS_DATABASE_URL", "MEMENTOS_DATABASE_URL"];
1237
- DB_PATH_ENV_KEYS = ["HASNA_MEMENTOS_DB_PATH", "MEMENTOS_DB_PATH"];
1381
+ DB_PATH_ENV_KEYS = MEMENTOS_DB_PATH_ENV_KEYS;
1238
1382
  MementosStoreConfigError = class MementosStoreConfigError extends Error {
1239
1383
  code = "MEMENTOS_STORE_CONFIG";
1240
1384
  constructor(message) {
@@ -2660,18 +2804,18 @@ __export(exports_database, {
2660
2804
  escapeLikePrefix: () => escapeLikePrefix,
2661
2805
  closeDatabase: () => closeDatabase
2662
2806
  });
2663
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, cpSync } from "fs";
2664
- import { dirname, join as join3, resolve } from "path";
2807
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, cpSync } from "fs";
2808
+ import { dirname, join as join4, resolve as resolve2 } from "path";
2665
2809
  function isInMemoryDb(path) {
2666
2810
  return path === ":memory:" || path.startsWith("file::memory:");
2667
2811
  }
2668
2812
  function findNearestMementosDb(startDir) {
2669
- let dir = resolve(startDir);
2813
+ let dir = resolve2(startDir);
2670
2814
  const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
2671
- const legacyHomeDb = resolve(home, ".mementos", "mementos.db");
2815
+ const legacyHomeDb = resolve2(home, ".mementos", "mementos.db");
2672
2816
  while (true) {
2673
- const candidate = join3(dir, ".mementos", "mementos.db");
2674
- if (existsSync2(candidate) && resolve(candidate) !== legacyHomeDb)
2817
+ const candidate = join4(dir, ".mementos", "mementos.db");
2818
+ if (existsSync3(candidate) && resolve2(candidate) !== legacyHomeDb)
2675
2819
  return candidate;
2676
2820
  const parent = dirname(dir);
2677
2821
  if (parent === dir)
@@ -2681,9 +2825,9 @@ function findNearestMementosDb(startDir) {
2681
2825
  return null;
2682
2826
  }
2683
2827
  function findGitRoot(startDir) {
2684
- let dir = resolve(startDir);
2828
+ let dir = resolve2(startDir);
2685
2829
  while (true) {
2686
- if (existsSync2(join3(dir, ".git")))
2830
+ if (existsSync3(join4(dir, ".git")))
2687
2831
  return dir;
2688
2832
  const parent = dirname(dir);
2689
2833
  if (parent === dir)
@@ -2693,11 +2837,13 @@ function findGitRoot(startDir) {
2693
2837
  return null;
2694
2838
  }
2695
2839
  function migrateGlobalDir() {
2840
+ if (!selectsMementosLocalStore())
2841
+ return;
2696
2842
  const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
2697
- const newDir = join3(home, ".hasna", "mementos");
2698
- const oldDir = join3(home, ".mementos");
2699
- if (!existsSync2(newDir) && existsSync2(oldDir)) {
2700
- mkdirSync2(join3(home, ".hasna"), { recursive: true });
2843
+ const newDir = getDataRoot();
2844
+ const oldDir = join4(home, ".mementos");
2845
+ if (!existsSync3(newDir) && existsSync3(oldDir)) {
2846
+ mkdirSync2(dirname(newDir), { recursive: true });
2701
2847
  cpSync(oldDir, newDir, { recursive: true });
2702
2848
  }
2703
2849
  }
@@ -2714,18 +2860,17 @@ function getDbPath() {
2714
2860
  if (process.env["MEMENTOS_DB_SCOPE"] === "project") {
2715
2861
  const gitRoot = findGitRoot(cwd);
2716
2862
  if (gitRoot) {
2717
- return join3(gitRoot, ".mementos", "mementos.db");
2863
+ return join4(gitRoot, ".mementos", "mementos.db");
2718
2864
  }
2719
2865
  }
2720
2866
  migrateGlobalDir();
2721
- const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
2722
- return join3(home, ".hasna", "mementos", "mementos.db");
2867
+ return join4(getDataRoot(), "mementos.db");
2723
2868
  }
2724
2869
  function ensureDir(filePath) {
2725
2870
  if (isInMemoryDb(filePath))
2726
2871
  return;
2727
- const dir = dirname(resolve(filePath));
2728
- if (!existsSync2(dir)) {
2872
+ const dir = dirname(resolve2(filePath));
2873
+ if (!existsSync3(dir)) {
2729
2874
  mkdirSync2(dir, { recursive: true });
2730
2875
  }
2731
2876
  }
@@ -2755,6 +2900,9 @@ function getDatabase(dbPath) {
2755
2900
  throw new Error("REFUSING-UNPINNED-TEST-OPEN: a test process tried to open the default local " + `SQLite store (${getDbPath()}) with no explicit dbPath argument and no ` + `${DB_PATH_ENV_KEYS.join("/")} set. Pass an explicit dbPath (e.g. ":memory:" ` + "or a scratch file), or set one of those env keys to a scratch path, before " + "calling getDatabase(). This guard exists because an unpinned test process " + "would otherwise silently read and write the real, shared, on-disk memory " + "store (todos 57b8b8c5).");
2756
2901
  }
2757
2902
  }
2903
+ if (process.env["NODE_ENV"] !== "test" && !isServerContext()) {
2904
+ assertClientStoreConfigured();
2905
+ }
2758
2906
  }
2759
2907
  const path = dbPath || getDbPath();
2760
2908
  if (_db) {
@@ -2767,7 +2915,6 @@ function getDatabase(dbPath) {
2767
2915
  ensureDir(path);
2768
2916
  _db = new SqliteAdapter(path);
2769
2917
  _db.run("PRAGMA journal_mode = WAL");
2770
- _db.run("PRAGMA busy_timeout = 5000");
2771
2918
  _db.run("PRAGMA foreign_keys = ON");
2772
2919
  _db.run("PRAGMA wal_autocheckpoint = 100");
2773
2920
  runMigrations(_db);
@@ -2863,6 +3010,8 @@ var init_database = __esm(() => {
2863
3010
  init_storage();
2864
3011
  init_api_mode();
2865
3012
  init_migrations();
3013
+ init_paths();
3014
+ init_local_opt_in();
2866
3015
  ALLOWED_TABLES = new Set([
2867
3016
  "memories",
2868
3017
  "agents",
@@ -2882,7 +3031,7 @@ var init_database = __esm(() => {
2882
3031
 
2883
3032
  // src/project-registration/authority.ts
2884
3033
  import { createHash as createHash2 } from "crypto";
2885
- import { resolve as resolve2 } from "path";
3034
+ import { resolve as resolve3 } from "path";
2886
3035
 
2887
3036
  // src/db/projects.ts
2888
3037
  init_database();
@@ -2891,13 +3040,13 @@ import { createHash } from "crypto";
2891
3040
 
2892
3041
  // src/lib/package-version.ts
2893
3042
  import { readFileSync as readFileSync2 } from "fs";
2894
- import { dirname as dirname2, join as join4 } from "path";
3043
+ import { dirname as dirname2, join as join5 } from "path";
2895
3044
  import { fileURLToPath as fileURLToPath2 } from "url";
2896
3045
  function getMementosPackageVersion() {
2897
3046
  const here = dirname2(fileURLToPath2(import.meta.url));
2898
3047
  for (const candidate of [
2899
- join4(here, "..", "..", "package.json"),
2900
- join4(here, "..", "package.json")
3048
+ join5(here, "..", "..", "package.json"),
3049
+ join5(here, "..", "package.json")
2901
3050
  ]) {
2902
3051
  try {
2903
3052
  const parsed = JSON.parse(readFileSync2(candidate, "utf8"));
@@ -3750,7 +3899,7 @@ function ownedPath(target) {
3750
3899
  }
3751
3900
  const path = target.withOwnedPath((value) => value);
3752
3901
  requireString(path, "target path", { max: 4096 });
3753
- if (path !== resolve2(path)) {
3902
+ if (path !== resolve3(path)) {
3754
3903
  throw new MementosProjectRegistrationError("MEMENTOS_PROJECT_REGISTRATION_INVALID_INPUT", "target path must already be canonical and absolute");
3755
3904
  }
3756
3905
  return path;
@@ -5084,6 +5233,17 @@ var SECRET_PATTERNS = [
5084
5233
  { name: "jwt", pattern: /eyJ[a-zA-Z0-9_-]{10,}\.eyJ[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}/g },
5085
5234
  { name: "hex_secret", pattern: /(?<=(?:key|token|secret|password|hash)\s*[:=]\s*["']?)[0-9a-f]{32,}(?=["']?)/gi }
5086
5235
  ];
5236
+ var KEY_TAG_SECRET_PATTERNS = SECRET_PATTERNS.filter((p) => new Set([
5237
+ "openai_key",
5238
+ "anthropic_key",
5239
+ "aws_key",
5240
+ "github_token",
5241
+ "github_oauth",
5242
+ "npm_token",
5243
+ "stripe_key",
5244
+ "slack_token",
5245
+ "jwt"
5246
+ ]).has(p.name));
5087
5247
  function redactSecrets(text) {
5088
5248
  let result = text;
5089
5249
  for (const { pattern } of SECRET_PATTERNS) {
@@ -5092,6 +5252,14 @@ function redactSecrets(text) {
5092
5252
  }
5093
5253
  return result;
5094
5254
  }
5255
+ function redactCredentialKey(text) {
5256
+ let result = text;
5257
+ for (const { pattern } of KEY_TAG_SECRET_PATTERNS) {
5258
+ pattern.lastIndex = 0;
5259
+ result = result.replace(pattern, REDACTED);
5260
+ }
5261
+ return result;
5262
+ }
5095
5263
  function containsSecrets(text) {
5096
5264
  for (const { pattern } of SECRET_PATTERNS) {
5097
5265
  pattern.lastIndex = 0;
@@ -5100,6 +5268,40 @@ function containsSecrets(text) {
5100
5268
  }
5101
5269
  return false;
5102
5270
  }
5271
+ function redactValueTree(value) {
5272
+ if (typeof value === "string")
5273
+ return redactSecrets(value);
5274
+ if (Array.isArray(value))
5275
+ return value.map(redactValueTree);
5276
+ if (value !== null && typeof value === "object") {
5277
+ const out = {};
5278
+ for (const [k, v] of Object.entries(value)) {
5279
+ out[k] = redactValueTree(v);
5280
+ }
5281
+ return out;
5282
+ }
5283
+ return value;
5284
+ }
5285
+ function redactMemoryWriteInput(input) {
5286
+ const out = { ...input };
5287
+ if (typeof out.key === "string")
5288
+ out.key = redactCredentialKey(out.key);
5289
+ if (typeof out.value === "string")
5290
+ out.value = redactSecrets(out.value);
5291
+ if (out.summary !== undefined && out.summary !== null) {
5292
+ out.summary = redactSecrets(String(out.summary));
5293
+ }
5294
+ if (Array.isArray(out.tags)) {
5295
+ out.tags = out.tags.map((t) => redactCredentialKey(String(t)));
5296
+ }
5297
+ if (typeof out.when_to_use === "string") {
5298
+ out.when_to_use = redactSecrets(out.when_to_use);
5299
+ }
5300
+ if (out.metadata !== undefined) {
5301
+ out.metadata = redactValueTree(out.metadata);
5302
+ }
5303
+ return out;
5304
+ }
5103
5305
 
5104
5306
  // src/lib/hooks.ts
5105
5307
  var _idCounter = 0;
@@ -5412,6 +5614,7 @@ function createMemory(input, dedupeMode = "merge", db) {
5412
5614
  if (reservedViolation) {
5413
5615
  throw new Error(reservedViolation);
5414
5616
  }
5617
+ input = redactMemoryWriteInput(input);
5415
5618
  if (!db && isApiMode()) {
5416
5619
  const { status, data } = apiJson("POST", "/memories", { ...input, dedupe: dedupeMode });
5417
5620
  if (!data || !data.id) {
@@ -5443,12 +5646,22 @@ function createMemory(input, dedupeMode = "merge", db) {
5443
5646
  const effectiveMode = dedupeMode === "overwrite" ? "merge" : dedupeMode === "version-fork" ? "create" : dedupeMode;
5444
5647
  if (effectiveMode === "error") {
5445
5648
  const existing = d.query(`SELECT id, agent_id, updated_at FROM memories
5446
- WHERE key = ? AND scope = ? AND COALESCE(project_id, '') = ? AND status = 'active'
5649
+ WHERE key = ? AND scope = ? AND COALESCE(project_id, '') = ?
5447
5650
  LIMIT 1`).get(input.key, input.scope || "private", input.project_id || "");
5448
5651
  if (existing) {
5449
5652
  throw new MemoryConflictError(input.key, existing);
5450
5653
  }
5451
5654
  }
5655
+ if (effectiveMode === "create") {
5656
+ const existing = d.query(`SELECT id, agent_id, updated_at FROM memories
5657
+ WHERE key = ? AND scope = ?
5658
+ AND COALESCE(agent_id, '') = ?
5659
+ AND COALESCE(project_id, '') = ?
5660
+ AND COALESCE(session_id, '') = ?`).get(input.key, input.scope || "private", input.agent_id || "", input.project_id || "", input.session_id || "");
5661
+ if (existing) {
5662
+ throw new MemoryConflictError(input.key, existing);
5663
+ }
5664
+ }
5452
5665
  if (effectiveMode === "merge") {
5453
5666
  const existing = d.query(`SELECT id, version FROM memories
5454
5667
  WHERE key = ? AND scope = ?
@@ -5859,6 +6072,7 @@ function listMemoriesBounded(filter = {}, target, db) {
5859
6072
  };
5860
6073
  }
5861
6074
  function updateMemory(id, input, db) {
6075
+ input = redactMemoryWriteInput(input);
5862
6076
  if (!db && isApiMode()) {
5863
6077
  const { status, data } = apiJson("PATCH", `/memories/${encodeURIComponent(id)}`, input, { allow404: true });
5864
6078
  if (status === 404)
@@ -5876,6 +6090,17 @@ function updateMemory(id, input, db) {
5876
6090
  if (existing.version !== input.version) {
5877
6091
  throw new VersionConflictError(id, input.version, existing.version);
5878
6092
  }
6093
+ if (input.scope !== undefined && input.scope !== existing.scope) {
6094
+ const conflict = d.query(`SELECT id, agent_id, updated_at FROM memories
6095
+ WHERE key = ? AND scope = ?
6096
+ AND COALESCE(agent_id, '') = ?
6097
+ AND COALESCE(project_id, '') = ?
6098
+ AND COALESCE(session_id, '') = ?
6099
+ AND id != ?`).get(existing.key, input.scope, existing.agent_id || "", existing.project_id || "", existing.session_id || "", memoryId);
6100
+ if (conflict) {
6101
+ throw new MemoryConflictError(existing.key, conflict);
6102
+ }
6103
+ }
5879
6104
  const sets = ["version = version + 1", "updated_at = ?"];
5880
6105
  const params = [now()];
5881
6106
  if (input.value !== undefined) {