@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.
- package/README.md +44 -9
- package/bun.lock +4 -62
- package/dist/cli/commands/info-history.d.ts.map +1 -1
- package/dist/cli/commands/info-stale.d.ts.map +1 -1
- package/dist/cli/commands/io-backup.d.ts.map +1 -1
- package/dist/cli/commands/io-export.d.ts.map +1 -1
- package/dist/cli/commands/io-restore.d.ts.map +1 -1
- package/dist/cli/commands/memory-cmd-chain.d.ts.map +1 -1
- package/dist/cli/commands/memory-cmd-crud.d.ts.map +1 -1
- package/dist/cli/commands/memory-cmd-recall.d.ts.map +1 -1
- package/dist/cli/commands/memory-cmd-search.d.ts.map +1 -1
- package/dist/cli/commands/memory-cmd-tail.d.ts.map +1 -1
- package/dist/cli/commands/memory-cmd-view.d.ts.map +1 -1
- package/dist/cli/commands/memory-cmd-when-to-use.d.ts.map +1 -1
- package/dist/cli/commands/storage.d.ts.map +1 -1
- package/dist/cli/commands/system-doctor.d.ts.map +1 -1
- package/dist/cli/commands/system-profile.d.ts.map +1 -1
- package/dist/cli/commands/system-status.d.ts +43 -0
- package/dist/cli/commands/system-status.d.ts.map +1 -0
- package/dist/cli/commands/system.d.ts.map +1 -1
- package/dist/cli/helpers.d.ts.map +1 -1
- package/dist/cli/index.js +3735 -2021
- package/dist/cli/register-all.d.ts +1 -1
- package/dist/cli/register-all.d.ts.map +1 -1
- package/dist/db/api-mode.d.ts +135 -37
- package/dist/db/api-mode.d.ts.map +1 -1
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/memories.d.ts.map +1 -1
- package/dist/db/pg-migrate.d.ts.map +1 -1
- package/dist/db/store-backend.d.ts +8 -2
- package/dist/db/store-backend.d.ts.map +1 -1
- package/dist/diagnostics/historical-project-registration-receipt.js +152 -11
- package/dist/generated/storage-kit/backend.d.ts +12 -2
- package/dist/generated/storage-kit/backend.d.ts.map +1 -1
- package/dist/generated/storage-kit/index.d.ts +1 -1
- package/dist/generated/storage-kit/index.d.ts.map +1 -1
- package/dist/generated/storage-kit/migrations.d.ts +21 -0
- package/dist/generated/storage-kit/migrations.d.ts.map +1 -1
- package/dist/generated/storage-kit/pool.d.ts +2 -5
- package/dist/generated/storage-kit/pool.d.ts.map +1 -1
- package/dist/generated/storage-kit/query.d.ts +1 -1
- package/dist/generated/storage-kit/query.d.ts.map +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3057 -1266
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/env.d.ts +10 -0
- package/dist/lib/env.d.ts.map +1 -0
- package/dist/lib/gatherer.d.ts.map +1 -1
- package/dist/lib/local-opt-in.d.ts +114 -0
- package/dist/lib/local-opt-in.d.ts.map +1 -0
- package/dist/lib/model-config.d.ts +1 -1
- package/dist/lib/paths.d.ts +60 -0
- package/dist/lib/paths.d.ts.map +1 -0
- package/dist/lib/profile-sync.d.ts.map +1 -1
- package/dist/lib/redact.d.ts +70 -10
- package/dist/lib/redact.d.ts.map +1 -1
- package/dist/lib/reflection.d.ts.map +1 -1
- package/dist/lib/search.d.ts.map +1 -1
- package/dist/lib/storage-sync.d.ts.map +1 -1
- package/dist/lib/sync.d.ts.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +3108 -1276
- package/dist/project-registration.js +378 -153
- package/dist/sdk/index.d.ts +173 -8
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/index.js +953 -12
- package/dist/server/auth.d.ts +2 -0
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/helpers.d.ts +48 -3
- package/dist/server/helpers.d.ts.map +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +3146 -1324
- package/dist/server/routes/system-synthesis.d.ts.map +1 -1
- package/dist/storage.d.ts +18 -0
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +149 -83
- package/dist/test-support/store-isolation.d.ts +50 -11
- package/dist/test-support/store-isolation.d.ts.map +1 -1
- package/docker-entrypoint.sh +7 -0
- package/hasna.contract.json +17 -5
- package/package.json +8 -8
- package/postinstall.mjs +85 -0
- package/dashboard/bun.lock +0 -519
- package/dist/cli/brains.d.ts +0 -3
- package/dist/cli/brains.d.ts.map +0 -1
- package/dist/lib/retired-storage-mode.d.ts +0 -11
- 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
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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 {
|
|
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 (!
|
|
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
|
|
435
|
-
if (readEnv(
|
|
436
|
-
return
|
|
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
|
|
447
|
-
return
|
|
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(
|
|
453
|
-
return resolveServerDataBackend("mementos",
|
|
513
|
+
function getStorageBackend(env2 = process.env) {
|
|
514
|
+
return resolveServerDataBackend("mementos", env2).backend;
|
|
454
515
|
}
|
|
455
|
-
function getStorageBackendDatabaseUrl(
|
|
456
|
-
return resolveDatabaseUrl("mementos",
|
|
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
|
|
547
|
-
const envUrl =
|
|
548
|
-
if (
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
899
|
-
|
|
900
|
-
|
|
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 (
|
|
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 =
|
|
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 =
|
|
1007
|
-
STORAGE_CONFIG_PATH =
|
|
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
|
|
1160
|
+
import { join as join3 } from "path";
|
|
1034
1161
|
import { writeFileSync as writeFileSync2, unlinkSync } from "fs";
|
|
1035
1162
|
import { randomUUID } from "crypto";
|
|
1036
|
-
|
|
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 =
|
|
1172
|
+
const v = env2[k]?.trim();
|
|
1039
1173
|
if (v)
|
|
1040
1174
|
return v;
|
|
1041
1175
|
}
|
|
1042
1176
|
return;
|
|
1043
1177
|
}
|
|
1044
|
-
function
|
|
1045
|
-
|
|
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
|
|
1052
|
-
|
|
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
|
-
` +
|
|
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
|
|
1078
|
-
|
|
1079
|
-
|
|
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
|
-
|
|
1100
|
-
if (firstEnvKey(DB_PATH_ENV_KEYS))
|
|
1214
|
+
function getApiConfig(env2 = process.env, options = {}) {
|
|
1215
|
+
if (hasDatabaseUrl(env2))
|
|
1101
1216
|
return null;
|
|
1102
|
-
|
|
1103
|
-
const apiKey = firstEnv2(API_KEY_ENV_KEYS);
|
|
1104
|
-
if (!rawBase || !apiKey)
|
|
1217
|
+
if (selectsMementosLocalStore(env2))
|
|
1105
1218
|
return null;
|
|
1106
|
-
|
|
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 (
|
|
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 =
|
|
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",
|
|
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
|
-
|
|
1234
|
-
API_URL_ENV_KEYS =
|
|
1235
|
-
API_KEY_ENV_KEYS =
|
|
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 =
|
|
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
|
|
2664
|
-
import { dirname, join as
|
|
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 =
|
|
2813
|
+
let dir = resolve2(startDir);
|
|
2670
2814
|
const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
2671
|
-
const legacyHomeDb =
|
|
2815
|
+
const legacyHomeDb = resolve2(home, ".mementos", "mementos.db");
|
|
2672
2816
|
while (true) {
|
|
2673
|
-
const candidate =
|
|
2674
|
-
if (
|
|
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 =
|
|
2828
|
+
let dir = resolve2(startDir);
|
|
2685
2829
|
while (true) {
|
|
2686
|
-
if (
|
|
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 =
|
|
2698
|
-
const oldDir =
|
|
2699
|
-
if (!
|
|
2700
|
-
mkdirSync2(
|
|
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
|
|
2863
|
+
return join4(gitRoot, ".mementos", "mementos.db");
|
|
2718
2864
|
}
|
|
2719
2865
|
}
|
|
2720
2866
|
migrateGlobalDir();
|
|
2721
|
-
|
|
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(
|
|
2728
|
-
if (!
|
|
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
|
|
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
|
|
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
|
-
|
|
2900
|
-
|
|
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 !==
|
|
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, '') = ?
|
|
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) {
|