@hasna/mementos 0.15.2 → 0.16.0

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 (107) hide show
  1. package/README.md +75 -21
  2. package/dist/audit-contract.d.ts +87 -0
  3. package/dist/audit-contract.d.ts.map +1 -0
  4. package/dist/cli/commands/info-context.d.ts.map +1 -1
  5. package/dist/cli/commands/info-history.d.ts.map +1 -1
  6. package/dist/cli/commands/io-backup.d.ts.map +1 -1
  7. package/dist/cli/commands/io-restore.d.ts.map +1 -1
  8. package/dist/cli/commands/memory-cmd-list.d.ts +25 -0
  9. package/dist/cli/commands/memory-cmd-list.d.ts.map +1 -1
  10. package/dist/cli/commands/project.d.ts.map +1 -1
  11. package/dist/cli/commands/system-session.d.ts.map +1 -1
  12. package/dist/cli/commands/system-synthesized-profile.d.ts.map +1 -1
  13. package/dist/cli/index.js +2093 -740
  14. package/dist/db/api-mode.d.ts +10 -0
  15. package/dist/db/api-mode.d.ts.map +1 -1
  16. package/dist/db/api-response-contract.d.ts +23 -0
  17. package/dist/db/api-response-contract.d.ts.map +1 -0
  18. package/dist/db/audit.d.ts +34 -45
  19. package/dist/db/audit.d.ts.map +1 -1
  20. package/dist/db/database.d.ts +1 -0
  21. package/dist/db/database.d.ts.map +1 -1
  22. package/dist/db/locks.d.ts.map +1 -1
  23. package/dist/db/machines.d.ts +50 -13
  24. package/dist/db/machines.d.ts.map +1 -1
  25. package/dist/db/memories.d.ts.map +1 -1
  26. package/dist/db/migrations.d.ts.map +1 -1
  27. package/dist/db/pg-migrate.d.ts.map +1 -1
  28. package/dist/db/pg-migrations.d.ts.map +1 -1
  29. package/dist/db/session-jobs.d.ts +2 -0
  30. package/dist/db/session-jobs.d.ts.map +1 -1
  31. package/dist/diagnostics/historical-project-registration-receipt.js +215 -1
  32. package/dist/index.d.ts +4 -2
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +1523 -206
  35. package/dist/lib/built-in-hooks.d.ts.map +1 -1
  36. package/dist/lib/injection-project.d.ts +10 -0
  37. package/dist/lib/injection-project.d.ts.map +1 -0
  38. package/dist/lib/injector.d.ts +1 -0
  39. package/dist/lib/injector.d.ts.map +1 -1
  40. package/dist/lib/machine-visibility.d.ts.map +1 -1
  41. package/dist/lib/memory-lock.d.ts.map +1 -1
  42. package/dist/lib/profile-synthesizer.d.ts +13 -2
  43. package/dist/lib/profile-synthesizer.d.ts.map +1 -1
  44. package/dist/lib/session-queue.d.ts.map +1 -1
  45. package/dist/lib/session-registry.d.ts +38 -1
  46. package/dist/lib/session-registry.d.ts.map +1 -1
  47. package/dist/lib/storage-sync.d.ts +5 -0
  48. package/dist/lib/storage-sync.d.ts.map +1 -1
  49. package/dist/lib/synthesis/index.d.ts.map +1 -1
  50. package/dist/mcp/index.d.ts +1 -1
  51. package/dist/mcp/index.d.ts.map +1 -1
  52. package/dist/mcp/index.js +3828 -1776
  53. package/dist/mcp/profile.d.ts +23 -0
  54. package/dist/mcp/profile.d.ts.map +1 -0
  55. package/dist/mcp/tools/lock-tools.d.ts.map +1 -1
  56. package/dist/mcp/tools/memory-audit.d.ts +1 -1
  57. package/dist/mcp/tools/memory-audit.d.ts.map +1 -1
  58. package/dist/mcp/tools/memory-crud.d.ts.map +1 -1
  59. package/dist/mcp/tools/memory-inject.d.ts.map +1 -1
  60. package/dist/mcp/tools/project-tools.d.ts.map +1 -1
  61. package/dist/mcp/tools/session-tools.d.ts.map +1 -1
  62. package/dist/mcp/tools/tool-registry.d.ts +22 -19
  63. package/dist/mcp/tools/tool-registry.d.ts.map +1 -1
  64. package/dist/mcp/tools/utility-tools.d.ts +2 -1
  65. package/dist/mcp/tools/utility-tools.d.ts.map +1 -1
  66. package/dist/project-registration.js +276 -20
  67. package/dist/sdk/index.d.ts +109 -6
  68. package/dist/sdk/index.d.ts.map +1 -1
  69. package/dist/sdk/index.js +740 -25
  70. package/dist/server/index.d.ts +2 -0
  71. package/dist/server/index.d.ts.map +1 -1
  72. package/dist/server/index.js +3195 -1134
  73. package/dist/server/openapi.d.ts.map +1 -1
  74. package/dist/server/routes/audit.d.ts +2 -0
  75. package/dist/server/routes/audit.d.ts.map +1 -0
  76. package/dist/server/routes/machines.d.ts +2 -0
  77. package/dist/server/routes/machines.d.ts.map +1 -0
  78. package/dist/server/routes/system-sessions.d.ts.map +1 -1
  79. package/dist/server/routes/system-synthesis.d.ts.map +1 -1
  80. package/dist/types/index.d.ts +3 -0
  81. package/dist/types/index.d.ts.map +1 -1
  82. package/hasna.contract.json +2 -2
  83. package/package.json +8 -5
  84. package/dist/cli/__fixtures__/clean-fallback-stub-server.d.ts +0 -2
  85. package/dist/cli/__fixtures__/clean-fallback-stub-server.d.ts.map +0 -1
  86. package/dist/cli/__fixtures__/io-restore-stub-server.d.ts +0 -2
  87. package/dist/cli/__fixtures__/io-restore-stub-server.d.ts.map +0 -1
  88. package/dist/db/__fixtures__/fail-closed-stub-server.d.ts +0 -2
  89. package/dist/db/__fixtures__/fail-closed-stub-server.d.ts.map +0 -1
  90. package/dist/db/__fixtures__/list-filter-capture-server.d.ts +0 -2
  91. package/dist/db/__fixtures__/list-filter-capture-server.d.ts.map +0 -1
  92. package/dist/db/__fixtures__/list-filter-client-runner.d.ts +0 -2
  93. package/dist/db/__fixtures__/list-filter-client-runner.d.ts.map +0 -1
  94. package/dist/test-support/memories-page-stub-server.d.ts +0 -2
  95. package/dist/test-support/memories-page-stub-server.d.ts.map +0 -1
  96. package/dist/test-support/memories-page-stub.d.ts +0 -28
  97. package/dist/test-support/memories-page-stub.d.ts.map +0 -1
  98. package/dist/test-support/pg-sync-stub-worker.d.ts +0 -2
  99. package/dist/test-support/pg-sync-stub-worker.d.ts.map +0 -1
  100. package/dist/test-support/preload-local-store.d.ts +0 -2
  101. package/dist/test-support/preload-local-store.d.ts.map +0 -1
  102. package/dist/test-support/project-authority-identity.d.ts +0 -8
  103. package/dist/test-support/project-authority-identity.d.ts.map +0 -1
  104. package/dist/test-support/store-isolation.d.ts +0 -147
  105. package/dist/test-support/store-isolation.d.ts.map +0 -1
  106. package/dist/test-support/strip-ansi.d.ts +0 -3
  107. package/dist/test-support/strip-ansi.d.ts.map +0 -1
package/dist/cli/index.js CHANGED
@@ -2176,312 +2176,6 @@ var init_local_opt_in = __esm(() => {
2176
2176
  CONTRACTS_AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
2177
2177
  });
2178
2178
 
2179
- // src/db/api-mode.ts
2180
- import { tmpdir } from "os";
2181
- import { join } from "path";
2182
- import { writeFileSync, unlinkSync } from "fs";
2183
- import { randomUUID } from "crypto";
2184
- import {
2185
- clientTransportEnvKeys as clientTransportEnvKeys2,
2186
- resolveClientTransport,
2187
- resolveCredential,
2188
- toV1BaseUrl,
2189
- ClientTransportConfigurationError
2190
- } from "@hasna/contracts/client";
2191
- function firstEnv(keys, env = process.env) {
2192
- for (const k of keys) {
2193
- const v = env[k]?.trim();
2194
- if (v)
2195
- return v;
2196
- }
2197
- return;
2198
- }
2199
- function firstEnvKey(keys, env = process.env) {
2200
- for (const k of keys) {
2201
- if (env[k]?.trim())
2202
- return k;
2203
- }
2204
- return null;
2205
- }
2206
- function hasDatabaseUrl(env = process.env) {
2207
- return Boolean(firstEnv(DATABASE_URL_ENV_KEYS, env));
2208
- }
2209
- function getApiModeEnvSources(env = process.env) {
2210
- return {
2211
- urlKey: firstEnvKey(API_URL_ENV_KEYS, env),
2212
- keyKey: firstEnvKey(API_KEY_ENV_KEYS, env),
2213
- databaseUrlKey: firstEnvKey(DATABASE_URL_ENV_KEYS, env)
2214
- };
2215
- }
2216
- function assertCleanResolvedBase(baseUrl) {
2217
- if (/[?#]/.test(baseUrl)) {
2218
- throw new Error("mementos base URL must not contain userinfo, query, or fragment data");
2219
- }
2220
- return baseUrl;
2221
- }
2222
- function getConfiguredApiEnv(env = process.env, options = {}) {
2223
- const rawBase = firstEnv(API_URL_ENV_KEYS, env);
2224
- let baseUrl = null;
2225
- if (rawBase) {
2226
- const normalized = toV1BaseUrl(rawBase);
2227
- assertCleanResolvedBase(normalized);
2228
- baseUrl = normalized;
2229
- }
2230
- const inputs = mementosResolverInputs(env, options.credentials);
2231
- return {
2232
- baseUrl,
2233
- apiKeyPresent: resolveCredential("mementos", inputs.env, inputs.credentials) !== null,
2234
- dbPathKey: firstEnvKey(DB_PATH_ENV_KEYS, env)
2235
- };
2236
- }
2237
- function isLoopbackHost(rawHost) {
2238
- const host = rawHost.replace(/^\[/, "").replace(/\]$/, "").toLowerCase();
2239
- return host === "localhost" || host === "::1" || /^127\./.test(host);
2240
- }
2241
- function assertRequestAllowedUnderTest(baseUrl) {
2242
- if (process.env["NODE_ENV"] !== "test")
2243
- return;
2244
- if (process.env[ALLOW_REMOTE_API_IN_TESTS_ENV]?.trim())
2245
- return;
2246
- let host;
2247
- try {
2248
- host = new URL(baseUrl).hostname;
2249
- } catch {
2250
- host = "";
2251
- }
2252
- if (host && isLoopbackHost(host))
2253
- return;
2254
- 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.
2255
- ` + ` host : ${host || "(unparseable base URL)"}
2256
- ` + " 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.
2257
- ` + " fix : build the child/process env via src/test-support/store-isolation.ts, or point the " + `suite at a loopback stub.
2258
- ` + ` override : set ${ALLOW_REMOTE_API_IN_TESTS_ENV}=1 only for a test that must reach a remote endpoint.`);
2259
- }
2260
- function toStoreConfigError(error) {
2261
- const message = error instanceof Error ? error.message : String(error);
2262
- 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`);
2263
- }
2264
- function getApiConfig(env = process.env, options = {}) {
2265
- if (hasDatabaseUrl(env))
2266
- return null;
2267
- if (selectsMementosLocalStore(env))
2268
- return null;
2269
- const inputs = mementosResolverInputs(env, options.credentials);
2270
- let credential;
2271
- try {
2272
- credential = resolveCredential("mementos", inputs.env, inputs.credentials);
2273
- } catch (error) {
2274
- throw toStoreConfigError(error);
2275
- }
2276
- let resolution;
2277
- try {
2278
- resolution = resolveClientTransport("mementos", inputs.env, {
2279
- credentials: credential ? { ...inputs.credentials, apiKey: credential.apiKey } : inputs.credentials
2280
- });
2281
- } catch (error) {
2282
- if (error instanceof ClientTransportConfigurationError && credential === null && /is not set and no API key could be resolved/.test(error.message)) {
2283
- return null;
2284
- }
2285
- throw toStoreConfigError(error);
2286
- }
2287
- return { baseUrl: assertCleanResolvedBase(resolution.baseUrl), apiKey: credential.apiKey };
2288
- }
2289
- function isApiMode() {
2290
- if (hasDatabaseUrl())
2291
- return false;
2292
- return getApiConfig() !== null;
2293
- }
2294
- function getResolvedApiModeReport(env = process.env, options = {}) {
2295
- if (hasDatabaseUrl(env))
2296
- return null;
2297
- if (selectsMementosLocalStore(env))
2298
- return null;
2299
- const inputs = mementosResolverInputs(env, options.credentials);
2300
- let credential;
2301
- try {
2302
- credential = resolveCredential("mementos", inputs.env, inputs.credentials);
2303
- } catch (error) {
2304
- throw toStoreConfigError(error);
2305
- }
2306
- if (!credential) {
2307
- try {
2308
- resolveClientTransport("mementos", inputs.env, { credentials: inputs.credentials });
2309
- } catch (error) {
2310
- if (error instanceof ClientTransportConfigurationError && /is not set and no API key could be resolved/.test(error.message)) {
2311
- return null;
2312
- }
2313
- throw toStoreConfigError(error);
2314
- }
2315
- return null;
2316
- }
2317
- const resolution = resolveClientTransport("mementos", inputs.env, {
2318
- credentials: { ...inputs.credentials, apiKey: credential.apiKey }
2319
- });
2320
- assertCleanResolvedBase(resolution.baseUrl);
2321
- return {
2322
- baseUrl: resolution.baseUrl,
2323
- apiUrlSource: resolution.apiUrlSource,
2324
- apiKeySource: credential.source,
2325
- apiKeyTier: credential.tier
2326
- };
2327
- }
2328
- function assertClientStoreConfigured(env = process.env, options = {}) {
2329
- if (selectsMementosLocalStore(env))
2330
- return;
2331
- if (hasDatabaseUrl(env))
2332
- return;
2333
- const config = getApiConfig(env, options);
2334
- if (config !== null)
2335
- return;
2336
- throw new MementosStoreConfigError(unconfiguredStoreMessage());
2337
- }
2338
- function unconfiguredStoreMessage() {
2339
- 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).`;
2340
- }
2341
- function apiRequestRaw(method, path, body) {
2342
- const cfg = getApiConfig();
2343
- if (!cfg)
2344
- throw new Error("api-mode: not configured (no Hasna mementos credential resolved)");
2345
- assertRequestAllowedUnderTest(cfg.baseUrl);
2346
- const url = `${cfg.baseUrl}${path.startsWith("/") ? path : `/${path}`}`;
2347
- const hasBody = body !== undefined && body !== null;
2348
- const timeout = process.env["HASNA_MEMENTOS_API_TIMEOUT"] || DEFAULT_TIMEOUT_S;
2349
- const headerLines = `Authorization: Bearer ${cfg.apiKey}
2350
- x-api-key: ${cfg.apiKey}
2351
- `;
2352
- const args = [
2353
- "curl",
2354
- "-sS",
2355
- "--fail-with-body",
2356
- "-m",
2357
- timeout,
2358
- "-X",
2359
- method,
2360
- "-H",
2361
- "@-",
2362
- "-H",
2363
- "Content-Type: application/json",
2364
- "-H",
2365
- "Accept: application/json",
2366
- "-w",
2367
- "\\n%{http_code}"
2368
- ];
2369
- let bodyFile;
2370
- if (hasBody) {
2371
- bodyFile = join(tmpdir(), `mem-req-${process.pid}-${randomUUID()}.json`);
2372
- writeFileSync(bodyFile, JSON.stringify(body), { mode: 384 });
2373
- args.push("--data-binary", `@${bodyFile}`);
2374
- }
2375
- args.push(url);
2376
- const childEnv = {};
2377
- for (const [k, v] of Object.entries(process.env)) {
2378
- if (v === undefined)
2379
- continue;
2380
- if (k === "HASNA_MEMENTOS_API_KEY" || k === "MEMENTOS_API_KEY")
2381
- continue;
2382
- childEnv[k] = v;
2383
- }
2384
- let out = "";
2385
- let err = "";
2386
- try {
2387
- const proc = Bun.spawnSync(args, {
2388
- stdin: Buffer.from(headerLines),
2389
- stdout: "pipe",
2390
- stderr: "pipe",
2391
- env: childEnv
2392
- });
2393
- out = proc.stdout ? new TextDecoder().decode(proc.stdout) : "";
2394
- err = proc.stderr ? new TextDecoder().decode(proc.stderr) : "";
2395
- } finally {
2396
- if (bodyFile) {
2397
- try {
2398
- unlinkSync(bodyFile);
2399
- } catch {}
2400
- }
2401
- }
2402
- const nl = out.lastIndexOf(`
2403
- `);
2404
- const codeStr = nl >= 0 ? out.slice(nl + 1).trim() : "";
2405
- const respBody = nl >= 0 ? out.slice(0, nl) : out;
2406
- const status = parseInt(codeStr, 10);
2407
- if (!Number.isFinite(status) || status === 0) {
2408
- throw new ApiRequestError(`mementos cloud request failed (${method} ${path}): ${err.trim() || "no HTTP status"}`, 0, respBody);
2409
- }
2410
- return { status, body: respBody };
2411
- }
2412
- function apiJson(method, path, body, options) {
2413
- const raw = apiRequestRaw(method, path, body);
2414
- if (raw.status >= 200 && raw.status < 300) {
2415
- let data;
2416
- if (raw.body.trim()) {
2417
- try {
2418
- data = JSON.parse(raw.body);
2419
- } catch (e) {
2420
- throw new ApiRequestError(`mementos cloud ${method} ${path} returned status ${raw.status} with a body that is not valid JSON (${e instanceof Error ? e.message : String(e)}) \u2014 the response is truncated or the server is unhealthy`, raw.status, raw.body.slice(0, 500));
2421
- }
2422
- } else {
2423
- data = undefined;
2424
- }
2425
- return { status: raw.status, data };
2426
- }
2427
- if (raw.status === 404 && options?.allow404) {
2428
- return { status: 404, data: undefined };
2429
- }
2430
- let msg = `mementos cloud ${method} ${path} \u2192 ${raw.status}`;
2431
- try {
2432
- const parsed = JSON.parse(raw.body);
2433
- if (parsed.error || parsed.message)
2434
- msg += `: ${parsed.error || parsed.message}`;
2435
- } catch {
2436
- if (raw.body.trim())
2437
- msg += `: ${raw.body.slice(0, 200)}`;
2438
- }
2439
- throw new ApiRequestError(msg, raw.status, raw.body);
2440
- }
2441
- function toQuery(params) {
2442
- const sp = new URLSearchParams;
2443
- for (const [k, v] of Object.entries(params)) {
2444
- if (v === undefined || v === null || v === "")
2445
- continue;
2446
- if (Array.isArray(v)) {
2447
- if (v.length === 0)
2448
- continue;
2449
- sp.set(k, v.join(","));
2450
- } else if (typeof v === "boolean") {
2451
- sp.set(k, v ? "true" : "false");
2452
- } else {
2453
- sp.set(k, String(v));
2454
- }
2455
- }
2456
- const s = sp.toString();
2457
- return s ? `?${s}` : "";
2458
- }
2459
- 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";
2460
- var init_api_mode = __esm(() => {
2461
- init_local_opt_in();
2462
- API_URL_ENV_KEYS = clientTransportEnvKeys2("mementos").apiUrlKeys;
2463
- API_KEY_ENV_KEYS = clientTransportEnvKeys2("mementos").apiKeyKeys;
2464
- DATABASE_URL_ENV_KEYS = ["HASNA_MEMENTOS_DATABASE_URL", "MEMENTOS_DATABASE_URL"];
2465
- DB_PATH_ENV_KEYS = MEMENTOS_DB_PATH_ENV_KEYS;
2466
- MementosStoreConfigError = class MementosStoreConfigError extends Error {
2467
- code = "MEMENTOS_STORE_CONFIG";
2468
- constructor(message) {
2469
- super(message);
2470
- this.name = "MementosStoreConfigError";
2471
- }
2472
- };
2473
- ApiRequestError = class ApiRequestError extends Error {
2474
- status;
2475
- body;
2476
- constructor(message, status, body) {
2477
- super(message);
2478
- this.status = status;
2479
- this.body = body;
2480
- this.name = "ApiRequestError";
2481
- }
2482
- };
2483
- });
2484
-
2485
2179
  // src/generated/storage-kit/own.ts
2486
2180
  function ownProp(source, key) {
2487
2181
  if (source === null || source === undefined)
@@ -2538,7 +2232,7 @@ var init_backend = () => {};
2538
2232
  // src/lib/paths.ts
2539
2233
  import { existsSync } from "fs";
2540
2234
  import { homedir } from "os";
2541
- import { join as join2, resolve } from "path";
2235
+ import { join, resolve } from "path";
2542
2236
  import { homedir as pathsResolverHomedir } from "os";
2543
2237
  import { join as pathsResolverJoin } from "path";
2544
2238
  function pathsResolverAssertApp(app) {
@@ -2593,7 +2287,7 @@ function effectiveHome() {
2593
2287
  return process.env["HOME"] || process.env["USERPROFILE"] || homedir() || "/tmp";
2594
2288
  }
2595
2289
  function legacyDataRoot() {
2596
- return join2(effectiveHome(), ".hasna", "mementos");
2290
+ return join(effectiveHome(), ".hasna", "mementos");
2597
2291
  }
2598
2292
  function resolverDataRoot() {
2599
2293
  return dataDir({
@@ -2605,7 +2299,7 @@ function adoptResolverDataRoot(resolved, env = process.env) {
2605
2299
  const dataOverride = env.HASNA_DATA_HOME;
2606
2300
  if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
2607
2301
  return true;
2608
- return existsSync(join2(resolved, "mementos.db"));
2302
+ return existsSync(join(resolved, "mementos.db"));
2609
2303
  }
2610
2304
  function exactDataRoot() {
2611
2305
  for (const key of ["HASNA_MEMENTOS_HOME", "MEMENTOS_HOME"]) {
@@ -2648,8 +2342,8 @@ var init_env = __esm(() => {
2648
2342
 
2649
2343
  // src/storage.ts
2650
2344
  import { Database } from "bun:sqlite";
2651
- import { existsSync as existsSync2, mkdirSync, readFileSync, writeFileSync as writeFileSync2 } from "fs";
2652
- import { join as join3 } from "path";
2345
+ import { existsSync as existsSync2, mkdirSync, readFileSync, writeFileSync } from "fs";
2346
+ import { join as join2 } from "path";
2653
2347
  import { fileURLToPath } from "url";
2654
2348
  import { Worker } from "worker_threads";
2655
2349
  import pg from "pg";
@@ -3359,9 +3053,9 @@ var init_storage = __esm(() => {
3359
3053
  const ext = import.meta.url.endsWith(".ts") ? ".ts" : ".js";
3360
3054
  const here = fileURLToPath(new URL(".", import.meta.url));
3361
3055
  const candidates = [
3362
- join3(here, `pg-sync-worker${ext}`),
3363
- join3(here, "..", `pg-sync-worker${ext}`),
3364
- join3(here, "..", "..", `pg-sync-worker${ext}`)
3056
+ join2(here, `pg-sync-worker${ext}`),
3057
+ join2(here, "..", `pg-sync-worker${ext}`),
3058
+ join2(here, "..", "..", `pg-sync-worker${ext}`)
3365
3059
  ];
3366
3060
  for (const candidate of candidates) {
3367
3061
  if (existsSync2(candidate))
@@ -3466,8 +3160,8 @@ var init_storage = __esm(() => {
3466
3160
  schedule_minutes: 0
3467
3161
  }
3468
3162
  };
3469
- STORAGE_CONFIG_DIR = join3(LOCAL_DATA_DIR, "storage");
3470
- STORAGE_CONFIG_PATH = join3(STORAGE_CONFIG_DIR, "config.json");
3163
+ STORAGE_CONFIG_DIR = join2(LOCAL_DATA_DIR, "storage");
3164
+ STORAGE_CONFIG_PATH = join2(STORAGE_CONFIG_DIR, "config.json");
3471
3165
  DATABASE_ENV_NAMES = [
3472
3166
  { name: MEMENTOS_STORAGE_ENV.databaseUrl, deprecated: false },
3473
3167
  { name: MEMENTOS_STORAGE_FALLBACK_ENV.databaseUrl, deprecated: false }
@@ -3490,6 +3184,349 @@ var init_storage = __esm(() => {
3490
3184
  ];
3491
3185
  });
3492
3186
 
3187
+ // src/db/api-mode.ts
3188
+ var exports_api_mode = {};
3189
+ __export(exports_api_mode, {
3190
+ unconfiguredStoreMessage: () => unconfiguredStoreMessage,
3191
+ toQuery: () => toQuery,
3192
+ isApiMode: () => isApiMode,
3193
+ getResolvedApiModeReport: () => getResolvedApiModeReport,
3194
+ getConfiguredApiEnv: () => getConfiguredApiEnv,
3195
+ getApiModeEnvSources: () => getApiModeEnvSources,
3196
+ getApiConfig: () => getApiConfig,
3197
+ assertUnambiguousStoreEnv: () => assertUnambiguousStoreEnv,
3198
+ assertClientStoreConfigured: () => assertClientStoreConfigured,
3199
+ apiJson: () => apiJson,
3200
+ MementosStoreConfigError: () => MementosStoreConfigError,
3201
+ DB_PATH_ENV_KEYS: () => DB_PATH_ENV_KEYS,
3202
+ DATABASE_URL_ENV_KEYS: () => DATABASE_URL_ENV_KEYS,
3203
+ ApiRequestError: () => ApiRequestError,
3204
+ API_URL_ENV_KEYS: () => API_URL_ENV_KEYS,
3205
+ API_KEY_ENV_KEYS: () => API_KEY_ENV_KEYS,
3206
+ ALLOW_REMOTE_API_IN_TESTS_ENV: () => ALLOW_REMOTE_API_IN_TESTS_ENV
3207
+ });
3208
+ import { tmpdir } from "os";
3209
+ import { join as join3 } from "path";
3210
+ import { writeFileSync as writeFileSync2, unlinkSync } from "fs";
3211
+ import { randomUUID } from "crypto";
3212
+ import {
3213
+ clientTransportEnvKeys as clientTransportEnvKeys2,
3214
+ resolveClientTransport,
3215
+ resolveCredential,
3216
+ toV1BaseUrl,
3217
+ ClientTransportConfigurationError
3218
+ } from "@hasna/contracts/client";
3219
+ function firstEnv(keys, env2 = process.env) {
3220
+ for (const k of keys) {
3221
+ const v = env2[k]?.trim();
3222
+ if (v)
3223
+ return v;
3224
+ }
3225
+ return;
3226
+ }
3227
+ function firstEnvKey(keys, env2 = process.env) {
3228
+ for (const k of keys) {
3229
+ if (env2[k]?.trim())
3230
+ return k;
3231
+ }
3232
+ return null;
3233
+ }
3234
+ function hasDatabaseUrl(env2 = process.env) {
3235
+ return Boolean(firstEnv(DATABASE_URL_ENV_KEYS, env2));
3236
+ }
3237
+ function getApiModeEnvSources(env2 = process.env) {
3238
+ return {
3239
+ urlKey: firstEnvKey(API_URL_ENV_KEYS, env2),
3240
+ keyKey: firstEnvKey(API_KEY_ENV_KEYS, env2),
3241
+ databaseUrlKey: firstEnvKey(DATABASE_URL_ENV_KEYS, env2)
3242
+ };
3243
+ }
3244
+ function assertCleanResolvedBase(baseUrl) {
3245
+ if (/[?#]/.test(baseUrl)) {
3246
+ throw new Error("mementos base URL must not contain userinfo, query, or fragment data");
3247
+ }
3248
+ return baseUrl;
3249
+ }
3250
+ function getConfiguredApiEnv(env2 = process.env, options = {}) {
3251
+ const rawBase = firstEnv(API_URL_ENV_KEYS, env2);
3252
+ let baseUrl = null;
3253
+ if (rawBase) {
3254
+ const normalized = toV1BaseUrl(rawBase);
3255
+ assertCleanResolvedBase(normalized);
3256
+ baseUrl = normalized;
3257
+ }
3258
+ const inputs = mementosResolverInputs(env2, options.credentials);
3259
+ return {
3260
+ baseUrl,
3261
+ apiKeyPresent: resolveCredential("mementos", inputs.env, inputs.credentials) !== null,
3262
+ dbPathKey: firstEnvKey(DB_PATH_ENV_KEYS, env2)
3263
+ };
3264
+ }
3265
+ function isLoopbackHost(rawHost) {
3266
+ const host = rawHost.replace(/^\[/, "").replace(/\]$/, "").toLowerCase();
3267
+ return host === "localhost" || host === "::1" || /^127\./.test(host);
3268
+ }
3269
+ function assertRequestAllowedUnderTest(baseUrl) {
3270
+ if (process.env["NODE_ENV"] !== "test")
3271
+ return;
3272
+ if (process.env[ALLOW_REMOTE_API_IN_TESTS_ENV]?.trim())
3273
+ return;
3274
+ let host;
3275
+ try {
3276
+ host = new URL(baseUrl).hostname;
3277
+ } catch {
3278
+ host = "";
3279
+ }
3280
+ if (host && isLoopbackHost(host))
3281
+ return;
3282
+ 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.
3283
+ ` + ` host : ${host || "(unparseable base URL)"}
3284
+ ` + " 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.
3285
+ ` + " fix : build the child/process env via src/test-support/store-isolation.ts, or point the " + `suite at a loopback stub.
3286
+ ` + ` override : set ${ALLOW_REMOTE_API_IN_TESTS_ENV}=1 only for a test that must reach a remote endpoint.`);
3287
+ }
3288
+ function toStoreConfigError(error) {
3289
+ const message = error instanceof Error ? error.message : String(error);
3290
+ 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`);
3291
+ }
3292
+ function getApiConfig(env2 = process.env, options = {}) {
3293
+ if (hasDatabaseUrl(env2))
3294
+ return null;
3295
+ if (selectsMementosLocalStore(env2))
3296
+ return null;
3297
+ const inputs = mementosResolverInputs(env2, options.credentials);
3298
+ let credential;
3299
+ try {
3300
+ credential = resolveCredential("mementos", inputs.env, inputs.credentials);
3301
+ } catch (error) {
3302
+ throw toStoreConfigError(error);
3303
+ }
3304
+ let resolution;
3305
+ try {
3306
+ resolution = resolveClientTransport("mementos", inputs.env, {
3307
+ credentials: credential ? { ...inputs.credentials, apiKey: credential.apiKey } : inputs.credentials
3308
+ });
3309
+ } catch (error) {
3310
+ if (error instanceof ClientTransportConfigurationError && credential === null && /is not set and no API key could be resolved/.test(error.message)) {
3311
+ return null;
3312
+ }
3313
+ throw toStoreConfigError(error);
3314
+ }
3315
+ return { baseUrl: assertCleanResolvedBase(resolution.baseUrl), apiKey: credential.apiKey };
3316
+ }
3317
+ function isApiMode() {
3318
+ if (hasDatabaseUrl())
3319
+ return false;
3320
+ if (isServerContext())
3321
+ return false;
3322
+ return getApiConfig() !== null;
3323
+ }
3324
+ function getResolvedApiModeReport(env2 = process.env, options = {}) {
3325
+ if (hasDatabaseUrl(env2))
3326
+ return null;
3327
+ if (selectsMementosLocalStore(env2))
3328
+ return null;
3329
+ const inputs = mementosResolverInputs(env2, options.credentials);
3330
+ let credential;
3331
+ try {
3332
+ credential = resolveCredential("mementos", inputs.env, inputs.credentials);
3333
+ } catch (error) {
3334
+ throw toStoreConfigError(error);
3335
+ }
3336
+ if (!credential) {
3337
+ try {
3338
+ resolveClientTransport("mementos", inputs.env, { credentials: inputs.credentials });
3339
+ } catch (error) {
3340
+ if (error instanceof ClientTransportConfigurationError && /is not set and no API key could be resolved/.test(error.message)) {
3341
+ return null;
3342
+ }
3343
+ throw toStoreConfigError(error);
3344
+ }
3345
+ return null;
3346
+ }
3347
+ const resolution = resolveClientTransport("mementos", inputs.env, {
3348
+ credentials: { ...inputs.credentials, apiKey: credential.apiKey }
3349
+ });
3350
+ assertCleanResolvedBase(resolution.baseUrl);
3351
+ return {
3352
+ baseUrl: resolution.baseUrl,
3353
+ apiUrlSource: resolution.apiUrlSource,
3354
+ apiKeySource: credential.source,
3355
+ apiKeyTier: credential.tier
3356
+ };
3357
+ }
3358
+ function assertUnambiguousStoreEnv(env2 = process.env, options = {}) {
3359
+ if (hasExplicitLocalDbPath(env2))
3360
+ return;
3361
+ if (hasDatabaseUrl(env2))
3362
+ return;
3363
+ getApiConfig(env2, options);
3364
+ }
3365
+ function assertClientStoreConfigured(env2 = process.env, options = {}) {
3366
+ if (selectsMementosLocalStore(env2))
3367
+ return;
3368
+ if (hasDatabaseUrl(env2))
3369
+ return;
3370
+ const config = getApiConfig(env2, options);
3371
+ if (config !== null)
3372
+ return;
3373
+ throw new MementosStoreConfigError(unconfiguredStoreMessage());
3374
+ }
3375
+ function unconfiguredStoreMessage() {
3376
+ 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).`;
3377
+ }
3378
+ function apiRequestRaw(method, path, body) {
3379
+ const cfg = getApiConfig();
3380
+ if (!cfg)
3381
+ throw new Error("api-mode: not configured (no Hasna mementos credential resolved)");
3382
+ assertRequestAllowedUnderTest(cfg.baseUrl);
3383
+ const url = `${cfg.baseUrl}${path.startsWith("/") ? path : `/${path}`}`;
3384
+ const hasBody = body !== undefined && body !== null;
3385
+ const timeout = process.env["HASNA_MEMENTOS_API_TIMEOUT"] || DEFAULT_TIMEOUT_S;
3386
+ const headerLines = `Authorization: Bearer ${cfg.apiKey}
3387
+ x-api-key: ${cfg.apiKey}
3388
+ `;
3389
+ const args = [
3390
+ "curl",
3391
+ "-sS",
3392
+ "--fail-with-body",
3393
+ "-m",
3394
+ timeout,
3395
+ "-X",
3396
+ method,
3397
+ "-H",
3398
+ "@-",
3399
+ "-H",
3400
+ "Content-Type: application/json",
3401
+ "-H",
3402
+ "Accept: application/json",
3403
+ "-w",
3404
+ "\\n%{http_code}"
3405
+ ];
3406
+ let bodyFile;
3407
+ if (hasBody) {
3408
+ bodyFile = join3(tmpdir(), `mem-req-${process.pid}-${randomUUID()}.json`);
3409
+ try {
3410
+ writeFileSync2(bodyFile, JSON.stringify(body), { mode: 384 });
3411
+ } catch (error) {
3412
+ try {
3413
+ unlinkSync(bodyFile);
3414
+ } catch {}
3415
+ throw error;
3416
+ }
3417
+ args.push("--data-binary", `@${bodyFile}`);
3418
+ }
3419
+ args.push(url);
3420
+ const childEnv = {};
3421
+ for (const [k, v] of Object.entries(process.env)) {
3422
+ if (v === undefined)
3423
+ continue;
3424
+ if (k === "HASNA_MEMENTOS_API_KEY" || k === "MEMENTOS_API_KEY")
3425
+ continue;
3426
+ childEnv[k] = v;
3427
+ }
3428
+ let out = "";
3429
+ let err = "";
3430
+ try {
3431
+ const proc = Bun.spawnSync(args, {
3432
+ stdin: Buffer.from(headerLines),
3433
+ stdout: "pipe",
3434
+ stderr: "pipe",
3435
+ env: childEnv
3436
+ });
3437
+ out = proc.stdout ? new TextDecoder().decode(proc.stdout) : "";
3438
+ err = proc.stderr ? new TextDecoder().decode(proc.stderr) : "";
3439
+ } finally {
3440
+ if (bodyFile) {
3441
+ try {
3442
+ unlinkSync(bodyFile);
3443
+ } catch {}
3444
+ }
3445
+ }
3446
+ const nl = out.lastIndexOf(`
3447
+ `);
3448
+ const codeStr = nl >= 0 ? out.slice(nl + 1).trim() : "";
3449
+ const respBody = nl >= 0 ? out.slice(0, nl) : out;
3450
+ const status = parseInt(codeStr, 10);
3451
+ if (!Number.isFinite(status) || status === 0) {
3452
+ throw new ApiRequestError(`mementos cloud request failed (${method} ${path}): ${err.trim() || "no HTTP status"}`, 0, respBody);
3453
+ }
3454
+ return { status, body: respBody };
3455
+ }
3456
+ function apiJson(method, path, body, options) {
3457
+ const raw = apiRequestRaw(method, path, body);
3458
+ if (raw.status >= 200 && raw.status < 300) {
3459
+ let data;
3460
+ if (raw.body.trim()) {
3461
+ try {
3462
+ data = JSON.parse(raw.body);
3463
+ } catch (e) {
3464
+ throw new ApiRequestError(`mementos cloud ${method} ${path} returned status ${raw.status} with a body that is not valid JSON (${e instanceof Error ? e.message : String(e)}) \u2014 the response is truncated or the server is unhealthy`, raw.status, "");
3465
+ }
3466
+ } else {
3467
+ data = undefined;
3468
+ }
3469
+ return { status: raw.status, data };
3470
+ }
3471
+ if (raw.status === 404 && options?.allow404) {
3472
+ return { status: 404, data: undefined };
3473
+ }
3474
+ let msg = `mementos cloud ${method} ${path} \u2192 ${raw.status}`;
3475
+ try {
3476
+ const parsed = JSON.parse(raw.body);
3477
+ if (parsed.error || parsed.message)
3478
+ msg += `: ${parsed.error || parsed.message}`;
3479
+ } catch {
3480
+ if (raw.body.trim())
3481
+ msg += `: ${raw.body.slice(0, 200)}`;
3482
+ }
3483
+ throw new ApiRequestError(msg, raw.status, raw.body);
3484
+ }
3485
+ function toQuery(params) {
3486
+ const sp = new URLSearchParams;
3487
+ for (const [k, v] of Object.entries(params)) {
3488
+ if (v === undefined || v === null || v === "")
3489
+ continue;
3490
+ if (Array.isArray(v)) {
3491
+ if (v.length === 0)
3492
+ continue;
3493
+ sp.set(k, v.join(","));
3494
+ } else if (typeof v === "boolean") {
3495
+ sp.set(k, v ? "true" : "false");
3496
+ } else {
3497
+ sp.set(k, String(v));
3498
+ }
3499
+ }
3500
+ const s = sp.toString();
3501
+ return s ? `?${s}` : "";
3502
+ }
3503
+ 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";
3504
+ var init_api_mode = __esm(() => {
3505
+ init_local_opt_in();
3506
+ init_storage();
3507
+ API_URL_ENV_KEYS = clientTransportEnvKeys2("mementos").apiUrlKeys;
3508
+ API_KEY_ENV_KEYS = clientTransportEnvKeys2("mementos").apiKeyKeys;
3509
+ DATABASE_URL_ENV_KEYS = ["HASNA_MEMENTOS_DATABASE_URL", "MEMENTOS_DATABASE_URL"];
3510
+ DB_PATH_ENV_KEYS = MEMENTOS_DB_PATH_ENV_KEYS;
3511
+ MementosStoreConfigError = class MementosStoreConfigError extends Error {
3512
+ code = "MEMENTOS_STORE_CONFIG";
3513
+ constructor(message) {
3514
+ super(message);
3515
+ this.name = "MementosStoreConfigError";
3516
+ }
3517
+ };
3518
+ ApiRequestError = class ApiRequestError extends Error {
3519
+ status;
3520
+ body;
3521
+ constructor(message, status, body) {
3522
+ super(message);
3523
+ this.status = status;
3524
+ this.body = body;
3525
+ this.name = "ApiRequestError";
3526
+ }
3527
+ };
3528
+ });
3529
+
3493
3530
  // src/project-registration/schema.ts
3494
3531
  function sqliteMementosProjectRegistrationSchemaSql() {
3495
3532
  return `
@@ -4845,6 +4882,220 @@ INSERT OR IGNORE INTO _migrations (id) VALUES (38);
4845
4882
  `
4846
4883
  ${sqliteMementosMemoryProjectLinkSchemaSql()}
4847
4884
  INSERT OR IGNORE INTO _migrations (id) VALUES (40);
4885
+ `,
4886
+ `
4887
+ DROP TABLE IF EXISTS temp.mementos_m41_machine_preflight;
4888
+ CREATE TEMP TABLE mementos_m41_machine_preflight (
4889
+ id TEXT PRIMARY KEY,
4890
+ name TEXT NOT NULL CHECK(
4891
+ length(name) BETWEEN 1 AND 128
4892
+ AND instr(name, char(0)) = 0
4893
+ AND instr(name, char(1)) = 0
4894
+ AND instr(name, char(2)) = 0
4895
+ AND instr(name, char(3)) = 0
4896
+ AND instr(name, char(4)) = 0
4897
+ AND instr(name, char(5)) = 0
4898
+ AND instr(name, char(6)) = 0
4899
+ AND instr(name, char(7)) = 0
4900
+ AND instr(name, char(8)) = 0
4901
+ AND instr(name, char(9)) = 0
4902
+ AND instr(name, char(10)) = 0
4903
+ AND instr(name, char(11)) = 0
4904
+ AND instr(name, char(12)) = 0
4905
+ AND instr(name, char(13)) = 0
4906
+ AND instr(name, char(14)) = 0
4907
+ AND instr(name, char(15)) = 0
4908
+ AND instr(name, char(16)) = 0
4909
+ AND instr(name, char(17)) = 0
4910
+ AND instr(name, char(18)) = 0
4911
+ AND instr(name, char(19)) = 0
4912
+ AND instr(name, char(20)) = 0
4913
+ AND instr(name, char(21)) = 0
4914
+ AND instr(name, char(22)) = 0
4915
+ AND instr(name, char(23)) = 0
4916
+ AND instr(name, char(24)) = 0
4917
+ AND instr(name, char(25)) = 0
4918
+ AND instr(name, char(26)) = 0
4919
+ AND instr(name, char(27)) = 0
4920
+ AND instr(name, char(28)) = 0
4921
+ AND instr(name, char(29)) = 0
4922
+ AND instr(name, char(30)) = 0
4923
+ AND instr(name, char(31)) = 0
4924
+ AND instr(name, char(127)) = 0
4925
+ ),
4926
+ hostname TEXT NOT NULL UNIQUE CHECK(
4927
+ length(hostname) BETWEEN 1 AND 253
4928
+ AND hostname = lower(rtrim(trim(hostname), '.'))
4929
+ AND instr(hostname, ' ') = 0
4930
+ AND instr(hostname, char(9)) = 0
4931
+ AND instr(hostname, char(10)) = 0
4932
+ AND instr(hostname, char(13)) = 0
4933
+ AND instr(hostname, '/') = 0
4934
+ AND instr(hostname, char(92)) = 0
4935
+ ),
4936
+ platform TEXT NOT NULL CHECK(
4937
+ length(platform) BETWEEN 1 AND 64
4938
+ AND platform = lower(trim(platform))
4939
+ AND platform NOT GLOB '*[^a-z0-9._-]*'
4940
+ ),
4941
+ is_primary INTEGER NOT NULL CHECK(is_primary IN (0, 1)),
4942
+ created_at TEXT NOT NULL CHECK(julianday(created_at) IS NOT NULL),
4943
+ last_seen_at TEXT NOT NULL CHECK(
4944
+ julianday(last_seen_at) IS NOT NULL
4945
+ AND julianday(last_seen_at) >= julianday(created_at)
4946
+ )
4947
+ );
4948
+ INSERT INTO mementos_m41_machine_preflight
4949
+ (id, name, hostname, platform, is_primary, created_at, last_seen_at)
4950
+ SELECT
4951
+ id,
4952
+ name,
4953
+ lower(rtrim(trim(hostname), '.')),
4954
+ platform,
4955
+ is_primary,
4956
+ created_at,
4957
+ last_seen_at
4958
+ FROM machines;
4959
+
4960
+ UPDATE machines
4961
+ SET hostname = (
4962
+ SELECT preflight.hostname
4963
+ FROM mementos_m41_machine_preflight preflight
4964
+ WHERE preflight.id = machines.id
4965
+ );
4966
+
4967
+ DROP TABLE mementos_m41_machine_preflight;
4968
+ DROP TRIGGER IF EXISTS machines_single_primary_insert;
4969
+ DROP TRIGGER IF EXISTS machines_single_primary_update;
4970
+ DROP INDEX IF EXISTS idx_machines_hostname;
4971
+ CREATE UNIQUE INDEX idx_machines_hostname ON machines(hostname);
4972
+ DROP INDEX IF EXISTS idx_machines_single_primary;
4973
+ CREATE UNIQUE INDEX idx_machines_single_primary
4974
+ ON machines(is_primary) WHERE is_primary = 1;
4975
+
4976
+ CREATE TRIGGER IF NOT EXISTS machines_hostname_canonical_insert
4977
+ BEFORE INSERT ON machines
4978
+ WHEN NEW.hostname != lower(rtrim(trim(NEW.hostname), '.'))
4979
+ OR length(NEW.hostname) NOT BETWEEN 1 AND 253
4980
+ OR instr(NEW.hostname, ' ') > 0
4981
+ OR instr(NEW.hostname, char(9)) > 0
4982
+ OR instr(NEW.hostname, char(10)) > 0
4983
+ OR instr(NEW.hostname, char(13)) > 0
4984
+ OR instr(NEW.hostname, '/') > 0
4985
+ OR instr(NEW.hostname, char(92)) > 0
4986
+ BEGIN
4987
+ SELECT RAISE(ABORT, 'machines.hostname must be canonical');
4988
+ END;
4989
+
4990
+ CREATE TRIGGER IF NOT EXISTS machines_hostname_canonical_update
4991
+ BEFORE UPDATE OF hostname ON machines
4992
+ WHEN NEW.hostname != lower(rtrim(trim(NEW.hostname), '.'))
4993
+ OR length(NEW.hostname) NOT BETWEEN 1 AND 253
4994
+ OR instr(NEW.hostname, ' ') > 0
4995
+ OR instr(NEW.hostname, char(9)) > 0
4996
+ OR instr(NEW.hostname, char(10)) > 0
4997
+ OR instr(NEW.hostname, char(13)) > 0
4998
+ OR instr(NEW.hostname, '/') > 0
4999
+ OR instr(NEW.hostname, char(92)) > 0
5000
+ BEGIN
5001
+ SELECT RAISE(ABORT, 'machines.hostname must be canonical');
5002
+ END;
5003
+
5004
+ CREATE TRIGGER IF NOT EXISTS machines_runtime_contract_insert
5005
+ BEFORE INSERT ON machines
5006
+ WHEN length(NEW.name) NOT BETWEEN 1 AND 128
5007
+ OR instr(NEW.name, char(0)) > 0
5008
+ OR instr(NEW.name, char(1)) > 0
5009
+ OR instr(NEW.name, char(2)) > 0
5010
+ OR instr(NEW.name, char(3)) > 0
5011
+ OR instr(NEW.name, char(4)) > 0
5012
+ OR instr(NEW.name, char(5)) > 0
5013
+ OR instr(NEW.name, char(6)) > 0
5014
+ OR instr(NEW.name, char(7)) > 0
5015
+ OR instr(NEW.name, char(8)) > 0
5016
+ OR instr(NEW.name, char(9)) > 0
5017
+ OR instr(NEW.name, char(10)) > 0
5018
+ OR instr(NEW.name, char(11)) > 0
5019
+ OR instr(NEW.name, char(12)) > 0
5020
+ OR instr(NEW.name, char(13)) > 0
5021
+ OR instr(NEW.name, char(14)) > 0
5022
+ OR instr(NEW.name, char(15)) > 0
5023
+ OR instr(NEW.name, char(16)) > 0
5024
+ OR instr(NEW.name, char(17)) > 0
5025
+ OR instr(NEW.name, char(18)) > 0
5026
+ OR instr(NEW.name, char(19)) > 0
5027
+ OR instr(NEW.name, char(20)) > 0
5028
+ OR instr(NEW.name, char(21)) > 0
5029
+ OR instr(NEW.name, char(22)) > 0
5030
+ OR instr(NEW.name, char(23)) > 0
5031
+ OR instr(NEW.name, char(24)) > 0
5032
+ OR instr(NEW.name, char(25)) > 0
5033
+ OR instr(NEW.name, char(26)) > 0
5034
+ OR instr(NEW.name, char(27)) > 0
5035
+ OR instr(NEW.name, char(28)) > 0
5036
+ OR instr(NEW.name, char(29)) > 0
5037
+ OR instr(NEW.name, char(30)) > 0
5038
+ OR instr(NEW.name, char(31)) > 0
5039
+ OR instr(NEW.name, char(127)) > 0
5040
+ OR length(NEW.platform) NOT BETWEEN 1 AND 64
5041
+ OR NEW.platform != lower(trim(NEW.platform))
5042
+ OR NEW.platform GLOB '*[^a-z0-9._-]*'
5043
+ OR NEW.is_primary NOT IN (0, 1)
5044
+ OR julianday(NEW.created_at) IS NULL
5045
+ OR julianday(NEW.last_seen_at) IS NULL
5046
+ OR julianday(NEW.last_seen_at) < julianday(NEW.created_at)
5047
+ BEGIN
5048
+ SELECT RAISE(ABORT, 'machines row violates the persisted runtime contract');
5049
+ END;
5050
+
5051
+ CREATE TRIGGER IF NOT EXISTS machines_runtime_contract_update
5052
+ BEFORE UPDATE OF name, platform, is_primary, created_at, last_seen_at ON machines
5053
+ WHEN length(NEW.name) NOT BETWEEN 1 AND 128
5054
+ OR instr(NEW.name, char(0)) > 0
5055
+ OR instr(NEW.name, char(1)) > 0
5056
+ OR instr(NEW.name, char(2)) > 0
5057
+ OR instr(NEW.name, char(3)) > 0
5058
+ OR instr(NEW.name, char(4)) > 0
5059
+ OR instr(NEW.name, char(5)) > 0
5060
+ OR instr(NEW.name, char(6)) > 0
5061
+ OR instr(NEW.name, char(7)) > 0
5062
+ OR instr(NEW.name, char(8)) > 0
5063
+ OR instr(NEW.name, char(9)) > 0
5064
+ OR instr(NEW.name, char(10)) > 0
5065
+ OR instr(NEW.name, char(11)) > 0
5066
+ OR instr(NEW.name, char(12)) > 0
5067
+ OR instr(NEW.name, char(13)) > 0
5068
+ OR instr(NEW.name, char(14)) > 0
5069
+ OR instr(NEW.name, char(15)) > 0
5070
+ OR instr(NEW.name, char(16)) > 0
5071
+ OR instr(NEW.name, char(17)) > 0
5072
+ OR instr(NEW.name, char(18)) > 0
5073
+ OR instr(NEW.name, char(19)) > 0
5074
+ OR instr(NEW.name, char(20)) > 0
5075
+ OR instr(NEW.name, char(21)) > 0
5076
+ OR instr(NEW.name, char(22)) > 0
5077
+ OR instr(NEW.name, char(23)) > 0
5078
+ OR instr(NEW.name, char(24)) > 0
5079
+ OR instr(NEW.name, char(25)) > 0
5080
+ OR instr(NEW.name, char(26)) > 0
5081
+ OR instr(NEW.name, char(27)) > 0
5082
+ OR instr(NEW.name, char(28)) > 0
5083
+ OR instr(NEW.name, char(29)) > 0
5084
+ OR instr(NEW.name, char(30)) > 0
5085
+ OR instr(NEW.name, char(31)) > 0
5086
+ OR instr(NEW.name, char(127)) > 0
5087
+ OR length(NEW.platform) NOT BETWEEN 1 AND 64
5088
+ OR NEW.platform != lower(trim(NEW.platform))
5089
+ OR NEW.platform GLOB '*[^a-z0-9._-]*'
5090
+ OR NEW.is_primary NOT IN (0, 1)
5091
+ OR julianday(NEW.created_at) IS NULL
5092
+ OR julianday(NEW.last_seen_at) IS NULL
5093
+ OR julianday(NEW.last_seen_at) < julianday(NEW.created_at)
5094
+ BEGIN
5095
+ SELECT RAISE(ABORT, 'machines row violates the persisted runtime contract');
5096
+ END;
5097
+
5098
+ INSERT OR IGNORE INTO _migrations (id) VALUES (41);
4848
5099
  `
4849
5100
  ];
4850
5101
  });
@@ -4860,7 +5111,8 @@ __export(exports_database, {
4860
5111
  getDbPath: () => getDbPath,
4861
5112
  getDatabase: () => getDatabase,
4862
5113
  escapeLikePrefix: () => escapeLikePrefix,
4863
- closeDatabase: () => closeDatabase
5114
+ closeDatabase: () => closeDatabase,
5115
+ applyMachineIdentityMigration: () => applyMachineIdentityMigration
4864
5116
  });
4865
5117
  import { existsSync as existsSync3, mkdirSync as mkdirSync2, cpSync } from "fs";
4866
5118
  import { dirname, join as join4, resolve as resolve2 } from "path";
@@ -4988,32 +5240,62 @@ function getDatabase(dbPath) {
4988
5240
  return _db;
4989
5241
  }
4990
5242
  function runMigrations(db) {
5243
+ let currentLevel = 0;
4991
5244
  try {
4992
5245
  const result = db.query("SELECT MAX(id) as max_id FROM _migrations").get();
4993
- const currentLevel = result?.max_id ?? 0;
4994
- for (let i = currentLevel;i < MIGRATIONS.length; i++) {
4995
- try {
4996
- applyMigration(db, i);
4997
- } catch (e) {
4998
- console.warn(`[mementos] Migration ${i + 1} failed: ${e instanceof Error ? e.message : String(e)}`);
4999
- }
5000
- }
5001
- } catch {
5002
- for (let i = 0;i < MIGRATIONS.length; i++) {
5003
- try {
5004
- applyMigration(db, i);
5005
- } catch (e) {
5006
- console.warn(`[mementos] Migration ${i + 1} failed: ${e instanceof Error ? e.message : String(e)}`);
5246
+ currentLevel = result?.max_id ?? 0;
5247
+ } catch {}
5248
+ for (let i = currentLevel;i < MIGRATIONS.length; i++) {
5249
+ try {
5250
+ applyMigration(db, i);
5251
+ } catch (error) {
5252
+ const message = error instanceof Error ? error.message : String(error);
5253
+ if (i === 40) {
5254
+ throw new Error(`Mementos migration 41 refused unsafe machine identity data: ${message}`);
5007
5255
  }
5256
+ console.warn(`[mementos] Migration ${i + 1} failed: ${message}`);
5008
5257
  }
5009
5258
  }
5010
5259
  }
5011
5260
  function applyMigration(db, index) {
5012
- if (index === 32) {
5261
+ if (index === 32)
5013
5262
  ensureMachinePrimaryColumn(db);
5263
+ if (index === 40) {
5264
+ applyMachineIdentityMigration(db);
5265
+ return;
5014
5266
  }
5015
5267
  db.exec(MIGRATIONS[index]);
5016
5268
  }
5269
+ function applyMachineIdentityMigration(db) {
5270
+ db.transaction(() => {
5271
+ const rows = db.query("SELECT id, name, hostname, platform, is_primary, created_at, last_seen_at FROM machines").all();
5272
+ const identities = new Map;
5273
+ let primaryCount = 0;
5274
+ for (const row of rows) {
5275
+ const canonical = row.hostname.trim().replace(/\.+$/, "").toLowerCase();
5276
+ const createdAt = new Date(row.created_at).getTime();
5277
+ const lastSeenAt = new Date(row.last_seen_at).getTime();
5278
+ const primaryValueValid = row.is_primary === 0 || row.is_primary === 1 || row.is_primary === false || row.is_primary === true;
5279
+ if (!row.id || !row.name || row.name.length > 128 || /[\u0000-\u001f\u007f]/.test(row.name) || !canonical || canonical.length > 253 || /[\u0000-\u001f\u007f/\\\s]/.test(canonical) || !row.platform || row.platform.length > 64 || row.platform !== row.platform.trim().toLowerCase() || !/^[a-z0-9._-]+$/.test(row.platform) || !primaryValueValid || !Number.isFinite(createdAt) || !Number.isFinite(lastSeenAt) || lastSeenAt < createdAt) {
5280
+ throw new Error(`invalid legacy machine row ${row.id || "(missing id)"}`);
5281
+ }
5282
+ const owner = identities.get(canonical);
5283
+ if (owner && owner !== row.id) {
5284
+ throw new Error(`ambiguous normalized hostname '${canonical}' belongs to both ${owner} and ${row.id}`);
5285
+ }
5286
+ identities.set(canonical, row.id);
5287
+ if (Boolean(row.is_primary))
5288
+ primaryCount += 1;
5289
+ }
5290
+ if (primaryCount > 1) {
5291
+ throw new Error(`legacy machine registry contains ${primaryCount} primary rows`);
5292
+ }
5293
+ db.exec(MIGRATIONS[40]);
5294
+ const receipt = db.query("SELECT id FROM _migrations WHERE id = 41").get();
5295
+ if (receipt?.id !== 41)
5296
+ throw new Error("machine identity migration did not record completion");
5297
+ });
5298
+ }
5017
5299
  function ensureMachinePrimaryColumn(db) {
5018
5300
  const columns = db.query("PRAGMA table_info(machines)").all();
5019
5301
  if (columns.length === 0 || columns.some((column) => column.name === "is_primary")) {
@@ -5087,6 +5369,70 @@ var init_database = __esm(() => {
5087
5369
  ]);
5088
5370
  });
5089
5371
 
5372
+ // src/db/api-response-contract.ts
5373
+ function expectObject(value, operation) {
5374
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
5375
+ throw new MementosApiProtocolError(operation, "expected a JSON object");
5376
+ }
5377
+ return value;
5378
+ }
5379
+ function expectArray(value, operation, field) {
5380
+ if (!Array.isArray(value)) {
5381
+ throw new MementosApiProtocolError(operation, field ? `expected '${field}' to be an array` : "expected a JSON array");
5382
+ }
5383
+ return value;
5384
+ }
5385
+ function expectString(object, field, operation, options = {}) {
5386
+ const value = object[field];
5387
+ if (typeof value !== "string" || !options.allowEmpty && value.length === 0) {
5388
+ throw new MementosApiProtocolError(operation, `expected '${field}' to be ${options.allowEmpty ? "a string" : "a non-empty string"}`);
5389
+ }
5390
+ return value;
5391
+ }
5392
+ function expectNullableString(object, field, operation) {
5393
+ const value = object[field];
5394
+ if (value === null)
5395
+ return null;
5396
+ if (typeof value !== "string") {
5397
+ throw new MementosApiProtocolError(operation, `expected '${field}' to be a string or null`);
5398
+ }
5399
+ return value.length === 0 ? null : value;
5400
+ }
5401
+ function expectBoolean(object, field, operation) {
5402
+ const value = object[field];
5403
+ if (typeof value !== "boolean") {
5404
+ throw new MementosApiProtocolError(operation, `expected '${field}' to be a boolean`);
5405
+ }
5406
+ return value;
5407
+ }
5408
+ function expectNonNegativeInteger(object, field, operation) {
5409
+ const value = object[field];
5410
+ if (!Number.isSafeInteger(value) || value < 0) {
5411
+ throw new MementosApiProtocolError(operation, `expected '${field}' to be a non-negative safe integer`);
5412
+ }
5413
+ return value;
5414
+ }
5415
+ function expectRecord(object, field, operation) {
5416
+ try {
5417
+ return expectObject(object[field], operation);
5418
+ } catch (error) {
5419
+ if (error instanceof MementosApiProtocolError) {
5420
+ throw new MementosApiProtocolError(operation, `expected '${field}' to be a JSON object`);
5421
+ }
5422
+ throw error;
5423
+ }
5424
+ }
5425
+ var MementosApiProtocolError;
5426
+ var init_api_response_contract = __esm(() => {
5427
+ MementosApiProtocolError = class MementosApiProtocolError extends Error {
5428
+ code = "MEMENTOS_API_PROTOCOL";
5429
+ constructor(operation, detail) {
5430
+ super(`mementos cloud ${operation} returned a malformed 2xx response: ${detail}`);
5431
+ this.name = "MementosApiProtocolError";
5432
+ }
5433
+ };
5434
+ });
5435
+
5090
5436
  // src/lib/redact.ts
5091
5437
  function redactSecrets(text) {
5092
5438
  let result = text;
@@ -6076,7 +6422,7 @@ function buildMemoryListConditions(filter) {
6076
6422
  conditions.push("status = 'active'");
6077
6423
  }
6078
6424
  if (filter.project_id) {
6079
- conditions.push("project_id = ?");
6425
+ conditions.push(filter.include_unassigned_project === true ? "(project_id = ? OR project_id IS NULL)" : "project_id = ?");
6080
6426
  params.push(filter.project_id);
6081
6427
  }
6082
6428
  if (filter.agent_id) {
@@ -6156,6 +6502,7 @@ function listMemoriesPage(filter, db) {
6156
6502
  pinned: f.pinned,
6157
6503
  agent_id: f.agent_id,
6158
6504
  project_id: f.project_id,
6505
+ include_unassigned_project: f.project_id && f.include_unassigned_project === true ? true : undefined,
6159
6506
  session_id: f.session_id,
6160
6507
  machine_id: f.machine_id,
6161
6508
  visible_to_machine_id: f.visible_to_machine_id,
@@ -6180,7 +6527,7 @@ function listMemoriesPage(filter, db) {
6180
6527
  if (conditions.length > 0) {
6181
6528
  sql += ` WHERE ${conditions.join(" AND ")}`;
6182
6529
  }
6183
- sql += " ORDER BY importance DESC, created_at DESC";
6530
+ sql += " ORDER BY importance DESC, created_at DESC, id DESC";
6184
6531
  if (f.limit) {
6185
6532
  sql += " LIMIT ?";
6186
6533
  params.push(f.limit);
@@ -6324,7 +6671,7 @@ function listMemoryHistoryPage(opts = {}, db) {
6324
6671
  }
6325
6672
  const d = db || getDatabase();
6326
6673
  const params = [limit];
6327
- let sql = "SELECT * FROM memories WHERE status = 'active' AND accessed_at IS NOT NULL ORDER BY accessed_at DESC LIMIT ?";
6674
+ let sql = "SELECT * FROM memories WHERE status = 'active' AND accessed_at IS NOT NULL ORDER BY accessed_at DESC, id DESC LIMIT ?";
6328
6675
  if (offset) {
6329
6676
  sql += " OFFSET ?";
6330
6677
  params.push(offset);
@@ -7093,7 +7440,7 @@ __export(exports_projects, {
7093
7440
  ProjectGuardedUpdateError: () => ProjectGuardedUpdateError,
7094
7441
  ProjectCollisionError: () => ProjectCollisionError
7095
7442
  });
7096
- import { createHash } from "crypto";
7443
+ import { createHash as createHash2 } from "crypto";
7097
7444
  function parseProjectRow(row) {
7098
7445
  return {
7099
7446
  id: row["id"],
@@ -7132,7 +7479,7 @@ function canonicalProjectUpdateJson(value) {
7132
7479
  return JSON.stringify(canonicalizeProjectUpdateValue(value));
7133
7480
  }
7134
7481
  function digestProjectUpdateValue(value) {
7135
- return createHash("sha256").update(canonicalProjectUpdateJson(value)).digest("hex");
7482
+ return createHash2("sha256").update(canonicalProjectUpdateJson(value)).digest("hex");
7136
7483
  }
7137
7484
  function timestampString(value) {
7138
7485
  return value instanceof Date ? value.toISOString() : String(value);
@@ -9293,6 +9640,245 @@ var init_search = __esm(() => {
9293
9640
  ]);
9294
9641
  });
9295
9642
 
9643
+ // src/lib/config.ts
9644
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync6, readdirSync, writeFileSync as writeFileSync3, unlinkSync as unlinkSync2, cpSync as cpSync2 } from "fs";
9645
+ import { homedir as homedir4 } from "os";
9646
+ import { basename, dirname as dirname4, join as join9, resolve as resolve14 } from "path";
9647
+ function isInMemoryDb2(path) {
9648
+ return path === ":memory:" || path.startsWith("file::memory:");
9649
+ }
9650
+ function homeDir() {
9651
+ return process.env["HOME"] || process.env["USERPROFILE"] || homedir4();
9652
+ }
9653
+ function deepMerge(target, source) {
9654
+ const result = { ...target };
9655
+ for (const key of Object.keys(source)) {
9656
+ const sourceVal = source[key];
9657
+ const targetVal = result[key];
9658
+ if (sourceVal !== null && typeof sourceVal === "object" && !Array.isArray(sourceVal) && targetVal !== null && typeof targetVal === "object" && !Array.isArray(targetVal)) {
9659
+ result[key] = deepMerge(targetVal, sourceVal);
9660
+ } else {
9661
+ result[key] = sourceVal;
9662
+ }
9663
+ }
9664
+ return result;
9665
+ }
9666
+ function isValidScope(value) {
9667
+ return VALID_SCOPES2.includes(value);
9668
+ }
9669
+ function isValidCategory(value) {
9670
+ return VALID_CATEGORIES2.includes(value);
9671
+ }
9672
+ function loadConfig() {
9673
+ const configPath = join9(getDataRoot(), "config.json");
9674
+ let fileConfig = {};
9675
+ if (existsSync6(configPath)) {
9676
+ try {
9677
+ const raw = readFileSync6(configPath, "utf-8");
9678
+ fileConfig = JSON.parse(raw);
9679
+ } catch {}
9680
+ }
9681
+ const merged = deepMerge(DEFAULT_CONFIG, fileConfig);
9682
+ const envScope = env.defaultScope();
9683
+ if (envScope && isValidScope(envScope)) {
9684
+ merged.default_scope = envScope;
9685
+ }
9686
+ const envCategory = env.defaultCategory();
9687
+ if (envCategory && isValidCategory(envCategory)) {
9688
+ merged.default_category = envCategory;
9689
+ }
9690
+ const envImportance = env.defaultImportance();
9691
+ if (envImportance) {
9692
+ const parsed = parseInt(envImportance, 10);
9693
+ if (!Number.isNaN(parsed) && parsed >= 1 && parsed <= 10) {
9694
+ merged.default_importance = parsed;
9695
+ }
9696
+ }
9697
+ return merged;
9698
+ }
9699
+ function findFileWalkingUp(filename) {
9700
+ let dir = process.cwd();
9701
+ const legacyHomeMementosDb = resolve14(homeDir(), ".mementos", "mementos.db");
9702
+ while (true) {
9703
+ const candidate = join9(dir, filename);
9704
+ if (existsSync6(candidate) && resolve14(candidate) !== legacyHomeMementosDb) {
9705
+ return candidate;
9706
+ }
9707
+ const parent = dirname4(dir);
9708
+ if (parent === dir) {
9709
+ return null;
9710
+ }
9711
+ dir = parent;
9712
+ }
9713
+ }
9714
+ function findGitRoot2() {
9715
+ let dir = process.cwd();
9716
+ while (true) {
9717
+ if (existsSync6(join9(dir, ".git"))) {
9718
+ return dir;
9719
+ }
9720
+ const parent = dirname4(dir);
9721
+ if (parent === dir) {
9722
+ return null;
9723
+ }
9724
+ dir = parent;
9725
+ }
9726
+ }
9727
+ function profilesDir() {
9728
+ return join9(getDataRoot(), "profiles");
9729
+ }
9730
+ function globalConfigPath() {
9731
+ return join9(getDataRoot(), "config.json");
9732
+ }
9733
+ function readGlobalConfig() {
9734
+ const p = globalConfigPath();
9735
+ if (!existsSync6(p))
9736
+ return {};
9737
+ try {
9738
+ return JSON.parse(readFileSync6(p, "utf-8"));
9739
+ } catch {
9740
+ return {};
9741
+ }
9742
+ }
9743
+ function readGlobalConfigForWrite() {
9744
+ const p = globalConfigPath();
9745
+ if (!existsSync6(p))
9746
+ return {};
9747
+ try {
9748
+ const data = JSON.parse(readFileSync6(p, "utf-8"));
9749
+ if (data === null || typeof data !== "object" || Array.isArray(data)) {
9750
+ throw new Error("expected a JSON object");
9751
+ }
9752
+ return data;
9753
+ } catch (error) {
9754
+ const detail = error instanceof Error ? error.message : String(error);
9755
+ throw new Error(`Cannot update global config at ${p}: ${detail}`);
9756
+ }
9757
+ }
9758
+ function writeGlobalConfig(data) {
9759
+ const p = globalConfigPath();
9760
+ ensureDir2(dirname4(p));
9761
+ writeFileSync3(p, JSON.stringify(data, null, 2), "utf-8");
9762
+ }
9763
+ function getActiveProfile() {
9764
+ const envProfile = process.env["MEMENTOS_PROFILE"];
9765
+ if (envProfile)
9766
+ return envProfile.trim();
9767
+ const cfg = readGlobalConfig();
9768
+ return cfg["active_profile"] || null;
9769
+ }
9770
+ function setActiveProfile(name) {
9771
+ const cfg = readGlobalConfigForWrite();
9772
+ if (name === null) {
9773
+ delete cfg["active_profile"];
9774
+ } else {
9775
+ cfg["active_profile"] = name;
9776
+ }
9777
+ writeGlobalConfig(cfg);
9778
+ }
9779
+ function listProfiles() {
9780
+ const dir = profilesDir();
9781
+ if (!existsSync6(dir))
9782
+ return [];
9783
+ return readdirSync(dir).filter((f) => f.endsWith(".db")).map((f) => basename(f, ".db")).sort();
9784
+ }
9785
+ function deleteProfile(name) {
9786
+ const dbPath = join9(profilesDir(), `${name}.db`);
9787
+ if (!existsSync6(dbPath))
9788
+ return false;
9789
+ unlinkSync2(dbPath);
9790
+ if (getActiveProfile() === name)
9791
+ setActiveProfile(null);
9792
+ return true;
9793
+ }
9794
+ function getDbPath2() {
9795
+ const _home = homeDir();
9796
+ const _newDir = getDataRoot();
9797
+ const _oldDir = join9(_home, ".mementos");
9798
+ if (selectsMementosLocalStore() && !existsSync6(_newDir) && existsSync6(_oldDir)) {
9799
+ mkdirSync3(join9(_home, ".hasna"), { recursive: true });
9800
+ cpSync2(_oldDir, _newDir, { recursive: true });
9801
+ }
9802
+ const envDbPath = process.env["HASNA_MEMENTOS_DB_PATH"] ?? process.env["MEMENTOS_DB_PATH"];
9803
+ if (envDbPath) {
9804
+ if (isInMemoryDb2(envDbPath)) {
9805
+ return envDbPath;
9806
+ }
9807
+ const resolved = resolve14(envDbPath);
9808
+ ensureDir2(dirname4(resolved));
9809
+ return resolved;
9810
+ }
9811
+ const profile = getActiveProfile();
9812
+ if (profile) {
9813
+ const profilePath = join9(profilesDir(), `${profile}.db`);
9814
+ ensureDir2(dirname4(profilePath));
9815
+ return profilePath;
9816
+ }
9817
+ const dbScope = process.env["MEMENTOS_DB_SCOPE"];
9818
+ if (dbScope === "project") {
9819
+ const gitRoot = findGitRoot2();
9820
+ if (gitRoot) {
9821
+ const dbPath = join9(gitRoot, ".mementos", "mementos.db");
9822
+ ensureDir2(dirname4(dbPath));
9823
+ return dbPath;
9824
+ }
9825
+ }
9826
+ const found = findFileWalkingUp(join9(".mementos", "mementos.db"));
9827
+ if (found) {
9828
+ return found;
9829
+ }
9830
+ const fallback = join9(getDataRoot(), "mementos.db");
9831
+ ensureDir2(dirname4(fallback));
9832
+ return fallback;
9833
+ }
9834
+ function ensureDir2(dir) {
9835
+ if (!existsSync6(dir)) {
9836
+ mkdirSync3(dir, { recursive: true });
9837
+ }
9838
+ }
9839
+ var DEFAULT_CONFIG, VALID_SCOPES2, VALID_CATEGORIES2;
9840
+ var init_config = __esm(() => {
9841
+ init_paths();
9842
+ init_env();
9843
+ init_local_opt_in();
9844
+ DEFAULT_CONFIG = {
9845
+ default_scope: "private",
9846
+ default_category: "knowledge",
9847
+ default_importance: 5,
9848
+ max_entries: 1000,
9849
+ max_entries_per_scope: {
9850
+ global: 500,
9851
+ shared: 300,
9852
+ private: 200,
9853
+ working: 100
9854
+ },
9855
+ injection: {
9856
+ max_tokens: 500,
9857
+ min_importance: 5,
9858
+ categories: ["preference", "fact"],
9859
+ refresh_interval: 5
9860
+ },
9861
+ extraction: {
9862
+ enabled: true,
9863
+ min_confidence: 0.5
9864
+ },
9865
+ sync_agents: ["claude", "codex", "gemini"],
9866
+ auto_cleanup: {
9867
+ enabled: true,
9868
+ expired_check_interval: 3600,
9869
+ unused_archive_days: 7,
9870
+ stale_deprioritize_days: 14
9871
+ }
9872
+ };
9873
+ VALID_SCOPES2 = ["global", "shared", "private", "working"];
9874
+ VALID_CATEGORIES2 = [
9875
+ "preference",
9876
+ "fact",
9877
+ "knowledge",
9878
+ "history"
9879
+ ];
9880
+ });
9881
+
9296
9882
  // src/db/relations.ts
9297
9883
  function parseRelationRow(row) {
9298
9884
  return {
@@ -10958,17 +11544,80 @@ var exports_profile_synthesizer = {};
10958
11544
  __export(exports_profile_synthesizer, {
10959
11545
  synthesizeProfile: () => synthesizeProfile,
10960
11546
  markProfileStale: () => markProfileStale,
10961
- getProfileKey: () => getProfileKey
11547
+ getProfileKey: () => getProfileKey,
11548
+ ProfileSynthesisError: () => ProfileSynthesisError,
11549
+ ProfileScopeError: () => ProfileScopeError,
11550
+ PROFILE_SYNTHESIS_CONTRACT: () => PROFILE_SYNTHESIS_CONTRACT
10962
11551
  });
11552
+ function resolveProfileTarget(options) {
11553
+ const scope = options.scope ?? (options.project_id ? "project" : options.agent_id ? "agent" : "global");
11554
+ if (scope === "project") {
11555
+ if (!options.project_id)
11556
+ throw new ProfileScopeError("project scope requires project_id");
11557
+ return { scope, id: options.project_id, memory_scope: "shared", project_id: options.project_id };
11558
+ }
11559
+ if (scope === "agent") {
11560
+ if (!options.agent_id)
11561
+ throw new ProfileScopeError("agent scope requires agent_id");
11562
+ return { scope, id: options.agent_id, memory_scope: "private", agent_id: options.agent_id };
11563
+ }
11564
+ return { scope: "global", id: "global", memory_scope: "global" };
11565
+ }
10963
11566
  function getProfileKey(scope, id) {
10964
11567
  return `_profile_${scope}_${id}`;
10965
11568
  }
11569
+ function listProfileMemories(category, target) {
11570
+ if (target.scope !== "global") {
11571
+ return listMemories({
11572
+ category,
11573
+ scope: target.memory_scope,
11574
+ project_id: target.project_id,
11575
+ agent_id: target.agent_id,
11576
+ machine_id: null,
11577
+ status: "active",
11578
+ limit: 30
11579
+ });
11580
+ }
11581
+ return [
11582
+ ...listMemories({ category, scope: "global", machine_id: null, status: "active", limit: 30 }),
11583
+ ...listMemories({ category, scope: "shared", machine_id: null, status: "active", limit: 1000 })
11584
+ ].filter((memory) => memory.agent_id === null && memory.project_id === null).slice(0, 30);
11585
+ }
10966
11586
  async function synthesizeProfile(options) {
10967
- const scope = options.scope || (options.project_id ? "project" : options.agent_id ? "agent" : "global");
10968
- const id = options.project_id || options.agent_id || "global";
10969
- const profileKey = getProfileKey(scope, id);
11587
+ if (!isServerContext() && isApiMode()) {
11588
+ const operation = "POST /profile/synthesize";
11589
+ const { data } = apiJson("POST", "/profile/synthesize", {
11590
+ project_id: options.project_id,
11591
+ agent_id: options.agent_id,
11592
+ scope: options.scope,
11593
+ force_refresh: options.force_refresh === true
11594
+ });
11595
+ const response = expectObject(data, operation);
11596
+ if (response["contract"] !== PROFILE_SYNTHESIS_CONTRACT) {
11597
+ throw new MementosApiProtocolError(operation, `expected contract '${PROFILE_SYNTHESIS_CONTRACT}'`);
11598
+ }
11599
+ if (!("profile" in response)) {
11600
+ throw new MementosApiProtocolError(operation, "missing 'profile'");
11601
+ }
11602
+ if (response["profile"] === null) {
11603
+ if (response["reason"] !== "no_memories") {
11604
+ throw new MementosApiProtocolError(operation, "null profile requires reason 'no_memories'");
11605
+ }
11606
+ return null;
11607
+ }
11608
+ if (typeof response["profile"] !== "string" || response["profile"].length === 0) {
11609
+ throw new MementosApiProtocolError(operation, "expected 'profile' to be a non-empty string or null");
11610
+ }
11611
+ return {
11612
+ profile: response["profile"],
11613
+ memory_count: expectNonNegativeInteger(response, "memory_count", operation),
11614
+ from_cache: expectBoolean(response, "from_cache", operation)
11615
+ };
11616
+ }
11617
+ const target = resolveProfileTarget(options);
11618
+ const profileKey = getProfileKey(target.scope, target.id);
10970
11619
  if (!options.force_refresh) {
10971
- const cached = getMemoryByKey(profileKey, "shared", undefined, options.project_id);
11620
+ const cached = getMemoryByKey(profileKey, "shared", target.agent_id, target.project_id);
10972
11621
  if (cached) {
10973
11622
  const age = Date.now() - new Date(cached.updated_at).getTime();
10974
11623
  const maxAge = 24 * 60 * 60 * 1000;
@@ -10978,20 +11627,8 @@ async function synthesizeProfile(options) {
10978
11627
  }
10979
11628
  }
10980
11629
  }
10981
- const prefMemories = listMemories({
10982
- category: "preference",
10983
- project_id: options.project_id,
10984
- machine_id: null,
10985
- status: "active",
10986
- limit: 30
10987
- });
10988
- const factMemories = listMemories({
10989
- category: "fact",
10990
- project_id: options.project_id,
10991
- machine_id: null,
10992
- status: "active",
10993
- limit: 30
10994
- });
11630
+ const prefMemories = listProfileMemories("preference", target);
11631
+ const factMemories = listProfileMemories("fact", target);
10995
11632
  const allMemories = [...prefMemories, ...factMemories];
10996
11633
  if (allMemories.length === 0)
10997
11634
  return null;
@@ -11001,7 +11638,7 @@ async function synthesizeProfile(options) {
11001
11638
  `);
11002
11639
  const fallbackProfile = `## Profile
11003
11640
  ${lines}`;
11004
- saveProfile(profileKey, fallbackProfile, allMemories.length, options);
11641
+ saveProfile(profileKey, fallbackProfile, allMemories.length, target);
11005
11642
  return { profile: fallbackProfile, memory_count: allMemories.length, from_cache: false };
11006
11643
  }
11007
11644
  try {
@@ -11023,41 +11660,63 @@ ${lines}`;
11023
11660
  ${memoryList}` }]
11024
11661
  })
11025
11662
  });
11026
- if (!response.ok)
11663
+ if (!response.ok) {
11664
+ if (options.fail_on_provider_error)
11665
+ throw new ProfileSynthesisError;
11027
11666
  return null;
11667
+ }
11028
11668
  const data = await response.json();
11029
11669
  const profile = data.content?.[0]?.text?.trim();
11030
- if (!profile)
11670
+ if (!profile) {
11671
+ if (options.fail_on_provider_error)
11672
+ throw new ProfileSynthesisError;
11031
11673
  return null;
11032
- saveProfile(profileKey, profile, allMemories.length, options);
11674
+ }
11675
+ saveProfile(profileKey, profile, allMemories.length, target);
11033
11676
  return { profile, memory_count: allMemories.length, from_cache: false };
11034
- } catch {
11677
+ } catch (error) {
11678
+ if (options.fail_on_provider_error) {
11679
+ if (error instanceof ProfileSynthesisError)
11680
+ throw error;
11681
+ throw new ProfileSynthesisError;
11682
+ }
11035
11683
  return null;
11036
11684
  }
11037
11685
  }
11038
- function saveProfile(key, value, memoryCount, options) {
11686
+ function saveProfile(key, value, memoryCount, target) {
11039
11687
  try {
11040
11688
  createMemory({
11041
11689
  key,
11042
11690
  value,
11043
- category: "fact",
11691
+ category: "resource",
11044
11692
  scope: "shared",
11045
11693
  importance: 10,
11046
11694
  source: "auto",
11047
11695
  tags: ["profile", "synthesized"],
11048
11696
  when_to_use: "When needing to understand this agent's or project's preferences, style, and conventions",
11049
11697
  metadata: { memory_count: memoryCount, synthesized_at: new Date().toISOString(), stale: false },
11050
- agent_id: options.agent_id,
11051
- project_id: options.project_id
11698
+ agent_id: target.agent_id,
11699
+ project_id: target.project_id
11052
11700
  });
11053
11701
  } catch {}
11054
11702
  }
11055
- function markProfileStale(projectId, _agentId) {
11703
+ function markProfileStale(projectId, agentId) {
11704
+ const targets = [{ scope: "global", id: "global", memory_scope: "global" }];
11705
+ if (projectId)
11706
+ targets.push({ scope: "project", id: projectId, memory_scope: "shared", project_id: projectId });
11707
+ if (agentId)
11708
+ targets.push({ scope: "agent", id: agentId, memory_scope: "private", agent_id: agentId });
11056
11709
  try {
11057
- const { getDatabase: getDatabase2 } = (init_database(), __toCommonJS(exports_database));
11058
- const db = getDatabase2();
11059
- db.run(`UPDATE memories SET metadata = json_set(COALESCE(metadata, '{}'), '$.stale', json('true'))
11060
- WHERE key LIKE '_profile_%' AND COALESCE(project_id, '') = ?`, [projectId || ""]);
11710
+ const db = !isServerContext() && isApiMode() ? undefined : (init_database(), __toCommonJS(exports_database)).getDatabase();
11711
+ for (const target of targets) {
11712
+ const cached = getMemoryByKey(getProfileKey(target.scope, target.id), "shared", target.agent_id, target.project_id, undefined, db);
11713
+ if (!cached)
11714
+ continue;
11715
+ updateMemory(cached.id, {
11716
+ metadata: { ...cached.metadata ?? {}, stale: true },
11717
+ version: cached.version
11718
+ }, db);
11719
+ }
11061
11720
  } catch {}
11062
11721
  }
11063
11722
  var PROFILE_PROMPT = `You synthesize a coherent agent/project profile from individual preference and fact memories.
@@ -11070,9 +11729,26 @@ Output a concise profile (200-300 words max) organized by:
11070
11729
  - **Key Facts**: Architecture decisions, constraints, team conventions
11071
11730
 
11072
11731
  Only include sections that have relevant data. Be specific and actionable.
11073
- Output in markdown format.`;
11732
+ Output in markdown format.`, PROFILE_SYNTHESIS_CONTRACT = "mementos.profile.synthesize.v2", ProfileScopeError, ProfileSynthesisError;
11074
11733
  var init_profile_synthesizer = __esm(() => {
11075
11734
  init_memories();
11735
+ init_api_mode();
11736
+ init_api_response_contract();
11737
+ init_storage();
11738
+ ProfileScopeError = class ProfileScopeError extends Error {
11739
+ code = "MEMENTOS_PROFILE_SCOPE";
11740
+ constructor(message) {
11741
+ super(message);
11742
+ this.name = "ProfileScopeError";
11743
+ }
11744
+ };
11745
+ ProfileSynthesisError = class ProfileSynthesisError extends Error {
11746
+ code = "MEMENTOS_PROFILE_SYNTHESIS";
11747
+ constructor() {
11748
+ super("Profile synthesis provider failed");
11749
+ this.name = "ProfileSynthesisError";
11750
+ }
11751
+ };
11076
11752
  });
11077
11753
 
11078
11754
  // src/lib/contradiction.ts
@@ -11356,6 +12032,22 @@ var init_built_in_hooks = __esm(() => {
11356
12032
  } catch {}
11357
12033
  }
11358
12034
  });
12035
+ hookRegistry.register({
12036
+ type: "PostMemoryUpdate",
12037
+ blocking: false,
12038
+ builtin: true,
12039
+ priority: 65,
12040
+ description: "Mark synthesized profile as stale when a preference or fact memory is updated",
12041
+ handler: async (ctx) => {
12042
+ const category = ctx.memory?.category;
12043
+ if (category !== "preference" && category !== "fact")
12044
+ return;
12045
+ try {
12046
+ const { markProfileStale: markProfileStale2 } = await Promise.resolve().then(() => (init_profile_synthesizer(), exports_profile_synthesizer));
12047
+ markProfileStale2(ctx.projectId, ctx.agentId);
12048
+ } catch {}
12049
+ }
12050
+ });
11359
12051
  hookRegistry.register({
11360
12052
  type: "PostMemorySave",
11361
12053
  blocking: false,
@@ -12213,10 +12905,20 @@ __export(exports_synthesis2, {
12213
12905
  getSynthesisStatus: () => getSynthesisStatus
12214
12906
  });
12215
12907
  async function runSynthesis(options = {}) {
12216
- const d = options.db || getDatabase();
12217
12908
  const projectId = options.projectId ?? null;
12218
12909
  const agentId = options.agentId ?? null;
12219
12910
  const dryRun = options.dryRun ?? false;
12911
+ if (!options.db && isApiMode()) {
12912
+ const { data } = apiJson("POST", "/synthesis/run", {
12913
+ project_id: projectId,
12914
+ agent_id: agentId,
12915
+ dry_run: dryRun,
12916
+ max_proposals: options.maxProposals,
12917
+ provider: options.provider
12918
+ });
12919
+ return data;
12920
+ }
12921
+ const d = options.db || getDatabase();
12220
12922
  const run = createSynthesisRun({
12221
12923
  triggered_by: "manual",
12222
12924
  project_id: projectId,
@@ -12305,6 +13007,10 @@ async function runSynthesis(options = {}) {
12305
13007
  }
12306
13008
  }
12307
13009
  async function rollbackSynthesis(runId, db) {
13010
+ if (!db && isApiMode()) {
13011
+ const { data } = apiJson("POST", `/synthesis/rollback/${encodeURIComponent(runId)}`);
13012
+ return data;
13013
+ }
12308
13014
  const d = db || getDatabase();
12309
13015
  const result = await rollbackRun(runId, d);
12310
13016
  if (result.errors.length === 0) {
@@ -12353,8 +13059,37 @@ __export(exports_session_jobs, {
12353
13059
  getSessionJob: () => getSessionJob,
12354
13060
  getNextPendingJob: () => getNextPendingJob,
12355
13061
  createSessionJob: () => createSessionJob,
12356
- claimSessionJob: () => claimSessionJob
13062
+ claimSessionJob: () => claimSessionJob,
13063
+ SESSION_JOBS_PAGE_CONTRACT: () => SESSION_JOBS_PAGE_CONTRACT,
13064
+ SESSION_INGEST_CONTRACT: () => SESSION_INGEST_CONTRACT
12357
13065
  });
13066
+ function parseHostedSessionJob(value, operation) {
13067
+ const row = expectObject(value, operation);
13068
+ const source = expectString(row, "source", operation);
13069
+ const status = expectString(row, "status", operation);
13070
+ if (!SESSION_JOB_SOURCES.has(source)) {
13071
+ throw new MementosApiProtocolError(operation, "unsupported 'source'");
13072
+ }
13073
+ if (!SESSION_JOB_STATUSES.has(status)) {
13074
+ throw new MementosApiProtocolError(operation, "unsupported 'status'");
13075
+ }
13076
+ return {
13077
+ id: expectString(row, "id", operation),
13078
+ session_id: expectString(row, "session_id", operation),
13079
+ agent_id: expectNullableString(row, "agent_id", operation),
13080
+ project_id: expectNullableString(row, "project_id", operation),
13081
+ source,
13082
+ status,
13083
+ transcript: expectString(row, "transcript", operation, { allowEmpty: true }),
13084
+ chunk_count: expectNonNegativeInteger(row, "chunk_count", operation),
13085
+ memories_extracted: expectNonNegativeInteger(row, "memories_extracted", operation),
13086
+ error: expectNullableString(row, "error", operation),
13087
+ metadata: expectRecord(row, "metadata", operation),
13088
+ created_at: expectString(row, "created_at", operation),
13089
+ started_at: expectNullableString(row, "started_at", operation),
13090
+ completed_at: expectNullableString(row, "completed_at", operation)
13091
+ };
13092
+ }
12358
13093
  function parseJobRow(row) {
12359
13094
  return {
12360
13095
  id: row["id"],
@@ -12374,6 +13109,28 @@ function parseJobRow(row) {
12374
13109
  };
12375
13110
  }
12376
13111
  function createSessionJob(input, db) {
13112
+ if (!db && isApiMode()) {
13113
+ const operation = "POST /sessions/ingest";
13114
+ const { data } = apiJson("POST", "/sessions/ingest", {
13115
+ session_id: input.session_id,
13116
+ transcript: input.transcript,
13117
+ source: input.source ?? "manual",
13118
+ agent_id: input.agent_id,
13119
+ project_id: input.project_id,
13120
+ metadata: input.metadata ?? {}
13121
+ });
13122
+ const response = expectObject(data, operation);
13123
+ if (response["contract"] !== SESSION_INGEST_CONTRACT) {
13124
+ throw new MementosApiProtocolError(operation, `expected contract '${SESSION_INGEST_CONTRACT}'`);
13125
+ }
13126
+ const jobId = expectString(response, "job_id", operation);
13127
+ const jobObject = expectObject(response["job"], operation);
13128
+ const job = parseHostedSessionJob({ ...jobObject, transcript: input.transcript }, `${operation} job`);
13129
+ if (job.id !== jobId || job.session_id !== input.session_id) {
13130
+ throw new MementosApiProtocolError(operation, "job receipt identity does not match the request");
13131
+ }
13132
+ return job;
13133
+ }
12377
13134
  const d = db || getDatabase();
12378
13135
  const id = uuid();
12379
13136
  const timestamp2 = now();
@@ -12395,10 +13152,11 @@ function createSessionJob(input, db) {
12395
13152
  }
12396
13153
  function getSessionJob(id, db) {
12397
13154
  if (!db && isApiMode()) {
13155
+ const operation = `GET /sessions/jobs/${encodeURIComponent(id)}`;
12398
13156
  const { status, data } = apiJson("GET", `/sessions/jobs/${encodeURIComponent(id)}`, undefined, { allow404: true });
12399
- if (status === 404 || !data)
13157
+ if (status === 404)
12400
13158
  return null;
12401
- return data;
13159
+ return parseHostedSessionJob(data, operation);
12402
13160
  }
12403
13161
  const d = db || getDatabase();
12404
13162
  const row = d.query("SELECT * FROM session_memory_jobs WHERE id = ?").get(id);
@@ -12412,10 +13170,58 @@ function listSessionJobs(filter, db) {
12412
13170
  agent_id: filter?.agent_id,
12413
13171
  project_id: filter?.project_id,
12414
13172
  status: filter?.status,
12415
- limit: filter?.limit
13173
+ session_id: filter?.session_id,
13174
+ limit: filter?.limit,
13175
+ offset: filter?.offset
12416
13176
  });
13177
+ const operation = "GET /sessions/jobs";
12417
13178
  const { data } = apiJson("GET", `/sessions/jobs${q}`);
12418
- return data?.jobs ?? [];
13179
+ const response = expectObject(data, operation);
13180
+ if (response["contract"] !== SESSION_JOBS_PAGE_CONTRACT) {
13181
+ throw new MementosApiProtocolError(operation, `expected contract '${SESSION_JOBS_PAGE_CONTRACT}'`);
13182
+ }
13183
+ const jobs = expectArray(response["jobs"], operation, "jobs").map((job, index) => parseHostedSessionJob(job, `${operation} item ${index}`));
13184
+ const count = expectNonNegativeInteger(response, "count", operation);
13185
+ const responseLimit = expectNonNegativeInteger(response, "limit", operation);
13186
+ const responseOffset = expectNonNegativeInteger(response, "offset", operation);
13187
+ const hasMore = expectBoolean(response, "has_more", operation);
13188
+ const nextOffset = response["next_offset"];
13189
+ if (responseLimit < 1) {
13190
+ throw new MementosApiProtocolError(operation, "expected 'limit' to be positive");
13191
+ }
13192
+ if (count !== jobs.length || count > responseLimit) {
13193
+ throw new MementosApiProtocolError(operation, "page count is inconsistent with jobs/limit");
13194
+ }
13195
+ if (nextOffset !== null && (!Number.isSafeInteger(nextOffset) || nextOffset < 0)) {
13196
+ throw new MementosApiProtocolError(operation, "expected 'next_offset' to be a non-negative safe integer or null");
13197
+ }
13198
+ if (hasMore && nextOffset !== responseOffset + jobs.length) {
13199
+ throw new MementosApiProtocolError(operation, "'has_more' requires the exact next offset");
13200
+ }
13201
+ if (!hasMore && nextOffset !== null) {
13202
+ throw new MementosApiProtocolError(operation, "terminal page must set 'next_offset' to null");
13203
+ }
13204
+ if (filter?.limit !== undefined && responseLimit !== filter.limit) {
13205
+ throw new MementosApiProtocolError(operation, "server did not preserve requested 'limit'");
13206
+ }
13207
+ if (filter?.offset !== undefined && responseOffset !== filter.offset) {
13208
+ throw new MementosApiProtocolError(operation, "server did not preserve requested 'offset'");
13209
+ }
13210
+ for (const job of jobs) {
13211
+ if (filter?.agent_id !== undefined && job.agent_id !== filter.agent_id) {
13212
+ throw new MementosApiProtocolError(operation, "server did not preserve requested 'agent_id'");
13213
+ }
13214
+ if (filter?.project_id !== undefined && job.project_id !== filter.project_id) {
13215
+ throw new MementosApiProtocolError(operation, "server did not preserve requested 'project_id'");
13216
+ }
13217
+ if (filter?.session_id !== undefined && job.session_id !== filter.session_id) {
13218
+ throw new MementosApiProtocolError(operation, "server did not preserve requested 'session_id'");
13219
+ }
13220
+ if (filter?.status !== undefined && job.status !== filter.status) {
13221
+ throw new MementosApiProtocolError(operation, "server did not preserve requested 'status'");
13222
+ }
13223
+ }
13224
+ return jobs;
12419
13225
  }
12420
13226
  const d = db || getDatabase();
12421
13227
  const conditions = [];
@@ -12495,9 +13301,13 @@ function recoverStaleProcessingJobs(maxAgeMs, db) {
12495
13301
  const result = d.run("UPDATE session_memory_jobs SET status = 'pending', started_at = NULL WHERE status = 'processing' AND started_at < ?", [cutoff]);
12496
13302
  return result.changes;
12497
13303
  }
13304
+ var SESSION_JOBS_PAGE_CONTRACT = "mementos.sessions.jobs.v2", SESSION_INGEST_CONTRACT = "mementos.sessions.ingest.v2", SESSION_JOB_SOURCES, SESSION_JOB_STATUSES;
12498
13305
  var init_session_jobs = __esm(() => {
12499
13306
  init_database();
12500
13307
  init_api_mode();
13308
+ init_api_response_contract();
13309
+ SESSION_JOB_SOURCES = new Set(["claude-code", "codex", "manual", "open-sessions"]);
13310
+ SESSION_JOB_STATUSES = new Set(["pending", "processing", "completed", "failed"]);
12501
13311
  });
12502
13312
 
12503
13313
  // src/db/tool-events.ts
@@ -13120,12 +13930,25 @@ __export(exports_session_queue, {
13120
13930
  enqueueSessionJob: () => enqueueSessionJob
13121
13931
  });
13122
13932
  function enqueueSessionJob(jobId) {
13933
+ if (!isServerContext() && isApiMode())
13934
+ return;
13123
13935
  _pendingQueue.add(jobId);
13124
13936
  if (!_isProcessing) {
13125
13937
  _processNext();
13126
13938
  }
13127
13939
  }
13128
13940
  function getSessionQueueStats() {
13941
+ if (!isServerContext() && isApiMode()) {
13942
+ const operation = "GET /sessions/queue/stats";
13943
+ const { data } = apiJson("GET", "/sessions/queue/stats");
13944
+ const response = expectObject(data, operation);
13945
+ return {
13946
+ pending: expectNonNegativeInteger(response, "pending", operation),
13947
+ processing: expectNonNegativeInteger(response, "processing", operation),
13948
+ completed: expectNonNegativeInteger(response, "completed", operation),
13949
+ failed: expectNonNegativeInteger(response, "failed", operation)
13950
+ };
13951
+ }
13129
13952
  try {
13130
13953
  const db = getDatabase();
13131
13954
  const rows = db.query("SELECT status, COUNT(*) as count FROM session_memory_jobs GROUP BY status").all();
@@ -13146,7 +13969,9 @@ function getSessionQueueStats() {
13146
13969
  stats.failed = row.count;
13147
13970
  }
13148
13971
  return stats;
13149
- } catch {
13972
+ } catch (error) {
13973
+ if (isServerContext())
13974
+ throw error;
13150
13975
  return {
13151
13976
  pending: _pendingQueue.size,
13152
13977
  processing: _isProcessing ? 1 : 0,
@@ -13158,6 +13983,8 @@ function getSessionQueueStats() {
13158
13983
  function startSessionQueueWorker() {
13159
13984
  if (_workerStarted)
13160
13985
  return;
13986
+ if (!isServerContext() && isApiMode())
13987
+ return;
13161
13988
  _workerStarted = true;
13162
13989
  setInterval(() => {
13163
13990
  try {
@@ -13199,6 +14026,9 @@ var init_session_queue = __esm(() => {
13199
14026
  init_database();
13200
14027
  init_session_jobs();
13201
14028
  init_session_processor();
14029
+ init_api_mode();
14030
+ init_api_response_contract();
14031
+ init_storage();
13202
14032
  _pendingQueue = new Set;
13203
14033
  });
13204
14034
 
@@ -13207,6 +14037,8 @@ var exports_session_registry = {};
13207
14037
  __export(exports_session_registry, {
13208
14038
  updateSessionAgent: () => updateSessionAgent,
13209
14039
  unregisterSession: () => unregisterSession,
14040
+ sessionRegistryUsesLocalStore: () => sessionRegistryUsesLocalStore,
14041
+ sessionRegistryPath: () => sessionRegistryPath,
13210
14042
  registerSession: () => registerSession,
13211
14043
  listSessions: () => listSessions,
13212
14044
  heartbeatSession: () => heartbeatSession,
@@ -13214,17 +14046,36 @@ __export(exports_session_registry, {
13214
14046
  getSessionByAgent: () => getSessionByAgent,
13215
14047
  getSession: () => getSession,
13216
14048
  closeRegistry: () => closeRegistry,
13217
- cleanStaleSessions: () => cleanStaleSessions
14049
+ cleanStaleSessions: () => cleanStaleSessions,
14050
+ __resetProcessLocalRegistry: () => __resetProcessLocalRegistry,
14051
+ SESSION_REGISTRY_FILE: () => SESSION_REGISTRY_FILE
13218
14052
  });
13219
14053
  import { existsSync as existsSync10, mkdirSync as mkdirSync5 } from "fs";
13220
14054
  import { dirname as dirname6, join as join14 } from "path";
14055
+ function sessionRegistryUsesLocalStore(env2 = process.env) {
14056
+ return isServerContext() || selectsMementosLocalStore(env2);
14057
+ }
14058
+ function sessionRegistryPath() {
14059
+ const store = getDbPath2();
14060
+ if (store === ":memory:")
14061
+ return ":memory:";
14062
+ return join14(dirname6(store), SESSION_REGISTRY_FILE);
14063
+ }
13221
14064
  function getDb() {
13222
- if (_db2)
14065
+ const path = sessionRegistryPath();
14066
+ if (_db2 && _dbPath2 === path)
13223
14067
  return _db2;
13224
- const dir = dirname6(DB_PATH);
13225
- if (!existsSync10(dir))
13226
- mkdirSync5(dir, { recursive: true });
13227
- _db2 = new SqliteAdapter(DB_PATH);
14068
+ if (_db2) {
14069
+ _db2.close();
14070
+ _db2 = null;
14071
+ }
14072
+ if (path !== ":memory:") {
14073
+ const dir = dirname6(path);
14074
+ if (!existsSync10(dir))
14075
+ mkdirSync5(dir, { recursive: true });
14076
+ }
14077
+ _db2 = new SqliteAdapter(path);
14078
+ _dbPath2 = path;
13228
14079
  _db2.run("PRAGMA journal_mode = WAL");
13229
14080
  _db2.run("PRAGMA busy_timeout = 3000");
13230
14081
  _db2.exec(`
@@ -13281,12 +14132,45 @@ function isProcessAlive(pid) {
13281
14132
  return false;
13282
14133
  }
13283
14134
  }
14135
+ function matchesFilter(s, filter) {
14136
+ if (!filter)
14137
+ return true;
14138
+ if (filter.project_name && s.project_name !== filter.project_name)
14139
+ return false;
14140
+ if (filter.git_root && s.git_root !== filter.git_root)
14141
+ return false;
14142
+ if (filter.mcp_server && s.mcp_server !== filter.mcp_server)
14143
+ return false;
14144
+ if (filter.agent_name && s.agent_name !== filter.agent_name)
14145
+ return false;
14146
+ if (filter.exclude_pid && s.pid === filter.exclude_pid)
14147
+ return false;
14148
+ return true;
14149
+ }
13284
14150
  function registerSession(opts) {
13285
- const db = getDb();
13286
14151
  const pid = process.pid;
13287
14152
  const cwd = opts.cwd || process.cwd();
13288
- const id = generateId();
13289
14153
  const timestamp2 = now3();
14154
+ if (!sessionRegistryUsesLocalStore()) {
14155
+ const existing2 = [..._memory.values()].find((s) => s.pid === pid && s.mcp_server === opts.mcp_server);
14156
+ const record = {
14157
+ id: existing2?.id ?? generateId(),
14158
+ pid,
14159
+ cwd,
14160
+ git_root: opts.git_root || null,
14161
+ agent_name: opts.agent_name || null,
14162
+ project_name: opts.project_name || null,
14163
+ tty: opts.tty || null,
14164
+ mcp_server: opts.mcp_server,
14165
+ metadata: { ...opts.metadata || {} },
14166
+ registered_at: existing2?.registered_at ?? timestamp2,
14167
+ last_seen_at: timestamp2
14168
+ };
14169
+ _memory.set(record.id, record);
14170
+ return { ...record, metadata: { ...record.metadata } };
14171
+ }
14172
+ const db = getDb();
14173
+ const id = generateId();
13290
14174
  const existing = db.query("SELECT id FROM sessions WHERE pid = ? AND mcp_server = ?").get(pid, opts.mcp_server);
13291
14175
  if (existing) {
13292
14176
  db.run(`UPDATE sessions SET agent_name = ?, project_name = ?, cwd = ?,
@@ -13319,19 +14203,41 @@ function registerSession(opts) {
13319
14203
  return getSession(id);
13320
14204
  }
13321
14205
  function heartbeatSession(id) {
14206
+ if (!sessionRegistryUsesLocalStore()) {
14207
+ const s = _memory.get(id);
14208
+ if (s)
14209
+ s.last_seen_at = now3();
14210
+ return;
14211
+ }
13322
14212
  const db = getDb();
13323
14213
  db.run("UPDATE sessions SET last_seen_at = ? WHERE id = ?", [now3(), id]);
13324
14214
  }
13325
14215
  function unregisterSession(id) {
14216
+ if (!sessionRegistryUsesLocalStore()) {
14217
+ _memory.delete(id);
14218
+ return;
14219
+ }
13326
14220
  const db = getDb();
13327
14221
  db.run("DELETE FROM sessions WHERE id = ?", [id]);
13328
14222
  }
13329
14223
  function getSession(id) {
14224
+ if (!sessionRegistryUsesLocalStore()) {
14225
+ const s = _memory.get(id);
14226
+ return s ? { ...s, metadata: { ...s.metadata } } : null;
14227
+ }
13330
14228
  const db = getDb();
13331
14229
  const row = db.query("SELECT * FROM sessions WHERE id = ?").get(id);
13332
14230
  return row ? parseRow2(row) : null;
13333
14231
  }
13334
14232
  function listSessions(filter) {
14233
+ if (!sessionRegistryUsesLocalStore()) {
14234
+ return [..._memory.values()].filter((s) => matchesFilter(s, filter)).filter((s) => {
14235
+ if (isProcessAlive(s.pid))
14236
+ return true;
14237
+ _memory.delete(s.id);
14238
+ return false;
14239
+ }).sort((a, b) => a.last_seen_at < b.last_seen_at ? 1 : a.last_seen_at > b.last_seen_at ? -1 : 0).map((s) => ({ ...s, metadata: { ...s.metadata } }));
14240
+ }
13335
14241
  const db = getDb();
13336
14242
  const conditions = [];
13337
14243
  const params = [];
@@ -13372,6 +14278,16 @@ function getSessionsByProject(projectName) {
13372
14278
  return listSessions({ project_name: projectName });
13373
14279
  }
13374
14280
  function cleanStaleSessions() {
14281
+ if (!sessionRegistryUsesLocalStore()) {
14282
+ let cleaned2 = 0;
14283
+ for (const s of [..._memory.values()]) {
14284
+ if (!isProcessAlive(s.pid)) {
14285
+ _memory.delete(s.id);
14286
+ cleaned2++;
14287
+ }
14288
+ }
14289
+ return cleaned2;
14290
+ }
13375
14291
  const db = getDb();
13376
14292
  const rows = db.query("SELECT id, pid FROM sessions").all();
13377
14293
  let cleaned = 0;
@@ -13384,20 +14300,35 @@ function cleanStaleSessions() {
13384
14300
  return cleaned;
13385
14301
  }
13386
14302
  function updateSessionAgent(mcpServer, agentName) {
13387
- const db = getDb();
13388
14303
  const pid = process.pid;
14304
+ if (!sessionRegistryUsesLocalStore()) {
14305
+ for (const s of _memory.values()) {
14306
+ if (s.pid === pid && s.mcp_server === mcpServer) {
14307
+ s.agent_name = agentName;
14308
+ s.last_seen_at = now3();
14309
+ }
14310
+ }
14311
+ return;
14312
+ }
14313
+ const db = getDb();
13389
14314
  db.run("UPDATE sessions SET agent_name = ?, last_seen_at = ? WHERE pid = ? AND mcp_server = ?", [agentName, now3(), pid, mcpServer]);
13390
14315
  }
13391
14316
  function closeRegistry() {
13392
14317
  if (_db2) {
13393
14318
  _db2.close();
13394
14319
  _db2 = null;
14320
+ _dbPath2 = null;
13395
14321
  }
13396
14322
  }
13397
- var DB_PATH, _db2 = null;
14323
+ function __resetProcessLocalRegistry() {
14324
+ _memory.clear();
14325
+ }
14326
+ var SESSION_REGISTRY_FILE = "sessions-registry.db", _db2 = null, _dbPath2 = null, _memory;
13398
14327
  var init_session_registry = __esm(() => {
13399
14328
  init_storage();
13400
- DB_PATH = join14(process.env["HOME"] || process.env["USERPROFILE"] || "~", ".open-sessions-registry.db");
14329
+ init_config();
14330
+ init_local_opt_in();
14331
+ _memory = new Map;
13401
14332
  });
13402
14333
 
13403
14334
  // src/db/pg-migrations.ts
@@ -14177,6 +15108,79 @@ var init_pg_migrations = __esm(() => {
14177
15108
  `
14178
15109
  ${postgresMementosMemoryProjectLinkSchemaSql()}
14179
15110
  INSERT INTO _migrations (id) VALUES (40) ON CONFLICT DO NOTHING;
15111
+ `,
15112
+ `
15113
+ DROP TABLE IF EXISTS pg_temp.mementos_m41_machine_preflight;
15114
+ CREATE TEMP TABLE mementos_m41_machine_preflight (
15115
+ id TEXT PRIMARY KEY,
15116
+ name TEXT NOT NULL CHECK(
15117
+ length(name) BETWEEN 1 AND 128
15118
+ AND name !~ '[[:cntrl:]]'
15119
+ ),
15120
+ hostname TEXT NOT NULL UNIQUE CHECK(
15121
+ length(hostname) BETWEEN 1 AND 253
15122
+ AND hostname = lower(regexp_replace(btrim(hostname), '\\.+$', ''))
15123
+ AND hostname !~ '[[:space:]/\\\\]'
15124
+ ),
15125
+ platform TEXT NOT NULL CHECK(
15126
+ length(platform) BETWEEN 1 AND 64
15127
+ AND platform = lower(btrim(platform))
15128
+ AND platform ~ '^[a-z0-9._-]+$'
15129
+ ),
15130
+ is_primary BOOLEAN NOT NULL,
15131
+ created_at TIMESTAMPTZ NOT NULL,
15132
+ last_seen_at TIMESTAMPTZ NOT NULL CHECK(last_seen_at >= created_at)
15133
+ ) ON COMMIT DROP;
15134
+
15135
+ INSERT INTO mementos_m41_machine_preflight
15136
+ (id, name, hostname, platform, is_primary, created_at, last_seen_at)
15137
+ SELECT
15138
+ id,
15139
+ name,
15140
+ lower(regexp_replace(btrim(hostname), '\\.+$', '')),
15141
+ platform,
15142
+ is_primary,
15143
+ created_at,
15144
+ last_seen_at
15145
+ FROM machines;
15146
+
15147
+ UPDATE machines machine
15148
+ SET hostname = preflight.hostname
15149
+ FROM mementos_m41_machine_preflight preflight
15150
+ WHERE preflight.id = machine.id;
15151
+
15152
+ DROP TRIGGER IF EXISTS machines_single_primary_insert ON machines;
15153
+ DROP TRIGGER IF EXISTS machines_single_primary_update ON machines;
15154
+ DROP INDEX IF EXISTS idx_machines_hostname;
15155
+ CREATE UNIQUE INDEX idx_machines_hostname ON machines(hostname);
15156
+ DROP INDEX IF EXISTS idx_machines_single_primary;
15157
+ CREATE UNIQUE INDEX idx_machines_single_primary
15158
+ ON machines ((1)) WHERE is_primary = TRUE;
15159
+
15160
+ ALTER TABLE machines DROP CONSTRAINT IF EXISTS machines_hostname_canonical;
15161
+ ALTER TABLE machines ADD CONSTRAINT machines_hostname_canonical CHECK(
15162
+ length(hostname) BETWEEN 1 AND 253
15163
+ AND hostname = lower(regexp_replace(btrim(hostname), '\\.+$', ''))
15164
+ AND hostname !~ '[[:space:]/\\\\]'
15165
+ );
15166
+
15167
+ ALTER TABLE machines DROP CONSTRAINT IF EXISTS machines_name_runtime;
15168
+ ALTER TABLE machines ADD CONSTRAINT machines_name_runtime CHECK(
15169
+ length(name) BETWEEN 1 AND 128
15170
+ AND name !~ '[[:cntrl:]]'
15171
+ );
15172
+ ALTER TABLE machines DROP CONSTRAINT IF EXISTS machines_platform_runtime;
15173
+ ALTER TABLE machines ADD CONSTRAINT machines_platform_runtime CHECK(
15174
+ length(platform) BETWEEN 1 AND 64
15175
+ AND platform = lower(btrim(platform))
15176
+ AND platform ~ '^[a-z0-9._-]+$'
15177
+ );
15178
+ ALTER TABLE machines DROP CONSTRAINT IF EXISTS machines_liveness_runtime;
15179
+ ALTER TABLE machines ADD CONSTRAINT machines_liveness_runtime CHECK(
15180
+ last_seen_at >= created_at
15181
+ );
15182
+
15183
+ INSERT INTO _migrations (id) VALUES (41) ON CONFLICT DO NOTHING;
14180
15184
  `
14181
15185
  ];
14182
15186
  });
@@ -14232,24 +15236,32 @@ async function applyPgMigrations(connectionString) {
14232
15236
  totalMigrations: PG_MIGRATIONS.length
14233
15237
  };
14234
15238
  try {
14235
- await pg2.run(`CREATE TABLE IF NOT EXISTS _pg_migrations (
14236
- id SERIAL PRIMARY KEY,
14237
- version INT UNIQUE NOT NULL,
14238
- applied_at TIMESTAMPTZ DEFAULT NOW()
14239
- )`);
14240
- const applied = await pg2.all("SELECT version FROM _pg_migrations ORDER BY version");
14241
- const appliedSet = new Set(applied.map((r) => r.version));
15239
+ const migrationLockKey = 21760041;
15240
+ await pg2.transaction(async (client) => {
15241
+ await client.query("SELECT pg_advisory_xact_lock($1)", [migrationLockKey]);
15242
+ await client.query(`CREATE TABLE IF NOT EXISTS _pg_migrations (
15243
+ id SERIAL PRIMARY KEY,
15244
+ version INT UNIQUE NOT NULL,
15245
+ applied_at TIMESTAMPTZ DEFAULT NOW()
15246
+ )`);
15247
+ });
14242
15248
  for (let i = 0;i < PG_MIGRATIONS.length; i++) {
14243
- if (appliedSet.has(i)) {
14244
- result.alreadyApplied.push(i);
14245
- continue;
14246
- }
14247
15249
  try {
14248
- await pg2.exec(PG_MIGRATIONS[i]);
14249
- await pg2.run("INSERT INTO _pg_migrations (version) VALUES ($1) ON CONFLICT DO NOTHING", i);
14250
- result.applied.push(i);
14251
- } catch (err) {
14252
- result.errors.push(`Migration ${i}: ${err?.message ?? String(err)}`);
15250
+ const outcome = await pg2.transaction(async (client) => {
15251
+ await client.query("SELECT pg_advisory_xact_lock($1)", [migrationLockKey]);
15252
+ const already = await client.query("SELECT 1 FROM _pg_migrations WHERE version = $1", [i]);
15253
+ if (already.rowCount)
15254
+ return "already";
15255
+ await client.query(PG_MIGRATIONS[i]);
15256
+ await client.query("INSERT INTO _pg_migrations (version) VALUES ($1)", [i]);
15257
+ return "applied";
15258
+ });
15259
+ if (outcome === "already")
15260
+ result.alreadyApplied.push(i);
15261
+ else
15262
+ result.applied.push(i);
15263
+ } catch (error) {
15264
+ result.errors.push(`Migration ${i}: ${error instanceof Error ? error.message : String(error)}`);
14253
15265
  break;
14254
15266
  }
14255
15267
  }
@@ -46679,7 +47691,7 @@ var require_token_io = __commonJS((exports, module) => {
46679
47691
  module.exports = __toCommonJS2(token_io_exports);
46680
47692
  var import_path2 = __toESM2(__require("path"));
46681
47693
  var import_fs2 = __toESM2(__require("fs"));
46682
- var import_os3 = __toESM2(__require("os"));
47694
+ var import_os2 = __toESM2(__require("os"));
46683
47695
  var import_token_error = require_token_error();
46684
47696
  function findRootDir() {
46685
47697
  try {
@@ -46700,11 +47712,11 @@ var require_token_io = __commonJS((exports, module) => {
46700
47712
  if (process.env.XDG_DATA_HOME) {
46701
47713
  return process.env.XDG_DATA_HOME;
46702
47714
  }
46703
- switch (import_os3.default.platform()) {
47715
+ switch (import_os2.default.platform()) {
46704
47716
  case "darwin":
46705
- return import_path2.default.join(import_os3.default.homedir(), "Library/Application Support");
47717
+ return import_path2.default.join(import_os2.default.homedir(), "Library/Application Support");
46706
47718
  case "linux":
46707
- return import_path2.default.join(import_os3.default.homedir(), ".local/share");
47719
+ return import_path2.default.join(import_os2.default.homedir(), ".local/share");
46708
47720
  case "win32":
46709
47721
  if (process.env.LOCALAPPDATA) {
46710
47722
  return process.env.LOCALAPPDATA;
@@ -46814,10 +47826,10 @@ var require_oauth = __commonJS((exports, module) => {
46814
47826
  refreshTokenRequest: () => refreshTokenRequest
46815
47827
  });
46816
47828
  module.exports = __toCommonJS2(oauth_exports);
46817
- var import_os3 = __require("os");
47829
+ var import_os2 = __require("os");
46818
47830
  var VERCEL_ISSUER = "https://vercel.com";
46819
47831
  var VERCEL_CLI_CLIENT_ID = "cl_HYyOPBNtFMfHhaUn9L4QPfTZz6TP47bp";
46820
- var userAgent = `@vercel/oidc node-${process.version} ${(0, import_os3.platform)()} (${(0, import_os3.arch)()}) ${(0, import_os3.hostname)()}`;
47832
+ var userAgent = `@vercel/oidc node-${process.version} ${(0, import_os2.platform)()} (${(0, import_os2.arch)()}) ${(0, import_os2.hostname)()}`;
46821
47833
  var _tokenEndpoint = null;
46822
47834
  async function getTokenEndpoint() {
46823
47835
  if (_tokenEndpoint) {
@@ -64449,65 +65461,283 @@ import { fileURLToPath as fileURLToPath5 } from "url";
64449
65461
 
64450
65462
  // src/db/machines.ts
64451
65463
  init_database();
65464
+ init_api_mode();
65465
+ init_api_response_contract();
65466
+ import { createHash } from "crypto";
64452
65467
  import { hostname, platform } from "os";
64453
- function parseMachine(row) {
65468
+ var MACHINE_REGISTRATION_CONTRACT = "mementos.machine-registration.v1";
65469
+ var MACHINE_LIST_CONTRACT = "mementos.machines.v1";
65470
+ class MachineRegistryError extends Error {
65471
+ code;
65472
+ constructor(code, message) {
65473
+ super(message);
65474
+ this.code = code;
65475
+ this.name = "MachineRegistryError";
65476
+ }
65477
+ }
65478
+ var ISO_UTC_TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
65479
+ function canonicalTimestamp(value, operation, field) {
65480
+ const stringValue = value instanceof Date ? value.toISOString() : value;
65481
+ if (typeof stringValue !== "string" || !ISO_UTC_TIMESTAMP.test(stringValue)) {
65482
+ throw new MementosApiProtocolError(operation, `expected '${field}' to be a canonical UTC ISO-8601 timestamp`);
65483
+ }
65484
+ const parsed = new Date(stringValue);
65485
+ if (!Number.isFinite(parsed.getTime()) || parsed.toISOString() !== stringValue) {
65486
+ throw new MementosApiProtocolError(operation, `expected '${field}' to be a real calendar timestamp`);
65487
+ }
65488
+ return stringValue;
65489
+ }
65490
+ function machineFromDb(row, operation = "machine database row") {
64454
65491
  if (!row)
64455
65492
  return null;
65493
+ if (typeof row.id !== "string" || !row.id || typeof row.name !== "string" || !row.name || row.name.length > 128 || /[\u0000-\u001f\u007f]/.test(row.name) || typeof row.hostname !== "string" || !row.hostname || typeof row.platform !== "string" || !row.platform) {
65494
+ throw new Error(`${operation} is missing or violates required machine identity fields`);
65495
+ }
65496
+ const canonicalHostname = normalizeMachineHostname(row.hostname);
65497
+ const canonicalPlatform = normalizeMachinePlatform(row.platform);
65498
+ if (canonicalHostname !== row.hostname || canonicalPlatform !== row.platform) {
65499
+ throw new Error(`${operation} contains noncanonical hostname or platform data`);
65500
+ }
65501
+ if (!["boolean", "number"].includes(typeof row.is_primary)) {
65502
+ throw new Error(`${operation} has an invalid is_primary value`);
65503
+ }
65504
+ const createdValue = row.created_at instanceof Date ? row.created_at : new Date(String(row.created_at ?? ""));
65505
+ const lastSeenValue = row.last_seen_at instanceof Date ? row.last_seen_at : new Date(String(row.last_seen_at ?? ""));
65506
+ if (!Number.isFinite(createdValue.getTime()) || !Number.isFinite(lastSeenValue.getTime())) {
65507
+ throw new Error(`${operation} has an invalid timestamp`);
65508
+ }
65509
+ const createdAt = createdValue.toISOString();
65510
+ const lastSeenAt = lastSeenValue.toISOString();
65511
+ if (lastSeenAt < createdAt)
65512
+ throw new Error(`${operation} has last_seen_at before created_at`);
64456
65513
  return {
64457
- ...row,
64458
- is_primary: Boolean(row.is_primary)
64459
- };
65514
+ id: row.id,
65515
+ name: row.name,
65516
+ hostname: canonicalHostname,
65517
+ platform: canonicalPlatform,
65518
+ is_primary: Boolean(row.is_primary),
65519
+ created_at: createdAt,
65520
+ last_seen_at: lastSeenAt
65521
+ };
65522
+ }
65523
+ function decodeMachine(value, operation) {
65524
+ const object = expectObject(value, operation);
65525
+ const name = expectString(object, "name", operation);
65526
+ if (name.length > 128 || /[\u0000-\u001f\u007f]/.test(name)) {
65527
+ throw new MementosApiProtocolError(operation, "machine.name is outside the public contract");
65528
+ }
65529
+ const rawHost = expectString(object, "hostname", operation);
65530
+ const rawPlatform = expectString(object, "platform", operation);
65531
+ let machineHostname;
65532
+ let machinePlatform;
65533
+ try {
65534
+ machineHostname = normalizeMachineHostname(rawHost);
65535
+ machinePlatform = normalizeMachinePlatform(rawPlatform);
65536
+ } catch {
65537
+ throw new MementosApiProtocolError(operation, "machine hostname or platform is not canonical");
65538
+ }
65539
+ if (machineHostname !== rawHost || machinePlatform !== rawPlatform) {
65540
+ throw new MementosApiProtocolError(operation, "machine hostname or platform is not canonical");
65541
+ }
65542
+ const createdAt = canonicalTimestamp(object["created_at"], operation, "created_at");
65543
+ const lastSeenAt = canonicalTimestamp(object["last_seen_at"], operation, "last_seen_at");
65544
+ if (lastSeenAt < createdAt) {
65545
+ throw new MementosApiProtocolError(operation, "last_seen_at precedes created_at");
65546
+ }
65547
+ return {
65548
+ id: expectString(object, "id", operation),
65549
+ name,
65550
+ hostname: machineHostname,
65551
+ platform: machinePlatform,
65552
+ is_primary: expectBoolean(object, "is_primary", operation),
65553
+ created_at: createdAt,
65554
+ last_seen_at: lastSeenAt
65555
+ };
65556
+ }
65557
+ function expectContract(object, contract, operation) {
65558
+ if (object["contract"] !== contract) {
65559
+ throw new MementosApiProtocolError(operation, `expected contract '${contract}'`);
65560
+ }
65561
+ }
65562
+ function decodeRegistration(value, expected) {
65563
+ const operation = "POST /v1/machines";
65564
+ const object = expectObject(value, operation);
65565
+ expectContract(object, MACHINE_REGISTRATION_CONTRACT, operation);
65566
+ const machine = decodeMachine(object["machine"], operation);
65567
+ const created = expectBoolean(object, "created", operation);
65568
+ const identity = expectObject(object["identity"], operation);
65569
+ if (identity["idempotency_key"] !== "normalized_hostname") {
65570
+ throw new MementosApiProtocolError(operation, "expected normalized-hostname identity semantics");
65571
+ }
65572
+ if (expectString(identity, "stable_id", operation) !== machine.id) {
65573
+ throw new MementosApiProtocolError(operation, "identity stable_id does not match machine.id");
65574
+ }
65575
+ const expectedHostname = normalizeMachineHostname(expected.hostname);
65576
+ const expectedPlatform = normalizeMachinePlatform(expected.platform);
65577
+ if (machine.hostname !== expectedHostname || machine.platform !== expectedPlatform) {
65578
+ throw new MementosApiProtocolError(operation, "registration receipt does not match the requested hostname/platform");
65579
+ }
65580
+ if (created && machine.name !== normalizeMachineName(expected.name, expectedHostname)) {
65581
+ throw new MementosApiProtocolError(operation, "created registration receipt does not match the requested name");
65582
+ }
65583
+ return { machine, created };
65584
+ }
65585
+ function decodeMachineList(value) {
65586
+ const operation = "GET /v1/machines";
65587
+ const object = expectObject(value, operation);
65588
+ expectContract(object, MACHINE_LIST_CONTRACT, operation);
65589
+ const values = expectArray(object["machines"], operation, "machines");
65590
+ const count = expectNonNegativeInteger(object, "count", operation);
65591
+ if (count !== values.length) {
65592
+ throw new MementosApiProtocolError(operation, "count does not match machines.length");
65593
+ }
65594
+ if (object["complete"] !== true) {
65595
+ throw new MementosApiProtocolError(operation, "expected complete=true");
65596
+ }
65597
+ const machines = values.map((entry, index) => decodeMachine(entry, `${operation} machines[${index}]`));
65598
+ const ids = new Set;
65599
+ const hosts = new Set;
65600
+ const names = new Set;
65601
+ let primaryCount = 0;
65602
+ for (const machine of machines) {
65603
+ if (ids.has(machine.id))
65604
+ throw new MementosApiProtocolError(operation, `duplicate machine id '${machine.id}'`);
65605
+ if (hosts.has(machine.hostname))
65606
+ throw new MementosApiProtocolError(operation, `duplicate machine hostname '${machine.hostname}'`);
65607
+ if (names.has(machine.name))
65608
+ throw new MementosApiProtocolError(operation, `duplicate machine name '${machine.name}'`);
65609
+ ids.add(machine.id);
65610
+ hosts.add(machine.hostname);
65611
+ names.add(machine.name);
65612
+ if (machine.is_primary)
65613
+ primaryCount += 1;
65614
+ }
65615
+ if (primaryCount > 1)
65616
+ throw new MementosApiProtocolError(operation, "more than one machine is primary");
65617
+ return machines;
65618
+ }
65619
+ function normalizeMachineHostname(host) {
65620
+ const normalized = host.trim().replace(/\.+$/, "").toLowerCase();
65621
+ if (!normalized || normalized.length > 253 || /[\u0000-\u001f\u007f/\\\s]/.test(normalized)) {
65622
+ throw new MachineRegistryError("MACHINE_INVALID_INPUT", "Machine hostname must be a non-empty hostname of at most 253 characters");
65623
+ }
65624
+ return normalized;
65625
+ }
65626
+ function normalizeMachinePlatform(value) {
65627
+ const normalized = value.trim().toLowerCase();
65628
+ if (!normalized || normalized.length > 64 || !/^[a-z0-9._-]+$/.test(normalized)) {
65629
+ throw new MachineRegistryError("MACHINE_INVALID_INPUT", "Machine platform must contain 1-64 lowercase letters, digits, dots, underscores, or hyphens");
65630
+ }
65631
+ return normalized;
64460
65632
  }
64461
- function normalizeHostname(host) {
64462
- return host.replace(/\.(local|lan|home|internal)$/i, "");
65633
+ function normalizeMachineName(value, fallback) {
65634
+ const normalized = value?.trim() || fallback;
65635
+ if (!normalized || normalized.length > 128 || /[\u0000-\u001f\u007f]/.test(normalized)) {
65636
+ throw new MachineRegistryError("MACHINE_INVALID_INPUT", "Machine name must contain 1-128 printable characters");
65637
+ }
65638
+ return normalized;
64463
65639
  }
64464
- function registerMachine(name, db = getDatabase()) {
64465
- const rawHost = hostname();
64466
- const host = normalizeHostname(rawHost);
64467
- const plat = platform();
64468
- const machineName = name?.trim() || host;
64469
- const existing = parseMachine(db.query("SELECT * FROM machines WHERE hostname = ?").get(host));
64470
- if (existing) {
64471
- db.run("UPDATE machines SET last_seen_at = ? WHERE id = ?", [now(), existing.id]);
64472
- return parseMachine(db.query("SELECT * FROM machines WHERE id = ?").get(existing.id));
65640
+ function registerMachineRecord(input, db) {
65641
+ const d = db ?? getDatabase();
65642
+ const host = normalizeMachineHostname(input.hostname);
65643
+ const machineName = normalizeMachineName(input.name, host);
65644
+ const machinePlatform = normalizeMachinePlatform(input.platform);
65645
+ const proposedId = uuid();
65646
+ const seenAt = now();
65647
+ try {
65648
+ const row = d.query(`INSERT INTO machines (id, name, hostname, platform, created_at, last_seen_at)
65649
+ VALUES (?, ?, ?, ?, ?, ?)
65650
+ ON CONFLICT(hostname) DO UPDATE SET
65651
+ platform = excluded.platform,
65652
+ last_seen_at = CASE
65653
+ WHEN machines.last_seen_at > excluded.last_seen_at THEN machines.last_seen_at
65654
+ ELSE excluded.last_seen_at
65655
+ END
65656
+ RETURNING *, id = ? AS registration_created`).get(proposedId, machineName, host, machinePlatform, seenAt, seenAt, proposedId);
65657
+ const machine = machineFromDb(row, "machine registration upsert");
65658
+ if (!machine || typeof row?.registration_created !== "boolean" && typeof row?.registration_created !== "number") {
65659
+ throw new MachineRegistryError("MACHINE_REGISTRATION_CONFLICT", "Machine registration returned no stable identity receipt");
65660
+ }
65661
+ return { machine, created: Boolean(row.registration_created) };
65662
+ } catch (error) {
65663
+ if (error instanceof MachineRegistryError)
65664
+ throw error;
65665
+ const details = error && typeof error === "object" ? error : {};
65666
+ const diagnostic = `${String(details.constraint ?? "")} ${String(details.message ?? "")}`;
65667
+ if (/machines(?:_|\.)name|machines_name_key/i.test(diagnostic)) {
65668
+ throw new MachineRegistryError("MACHINE_NAME_CONFLICT", `Machine name already taken: ${machineName}`);
65669
+ }
65670
+ throw error;
65671
+ }
65672
+ }
65673
+ function registerMachine(name, db) {
65674
+ const host = normalizeMachineHostname(hostname());
65675
+ const plat = normalizeMachinePlatform(platform());
65676
+ if (!db && isApiMode()) {
65677
+ const input = { name: name?.trim() || undefined, hostname: host, platform: plat };
65678
+ const { data } = apiJson("POST", "/machines", input);
65679
+ return decodeRegistration(data, input).machine;
64473
65680
  }
64474
- let finalName = machineName;
64475
- let suffix = 2;
64476
- while (db.query("SELECT id FROM machines WHERE name = ?").get(finalName)) {
64477
- finalName = `${machineName}-${suffix++}`;
65681
+ return registerMachineRecord({ name, hostname: host, platform: plat }, db).machine;
65682
+ }
65683
+ function listMachines(db) {
65684
+ if (!db && isApiMode()) {
65685
+ const { data } = apiJson("GET", "/machines");
65686
+ return decodeMachineList(data);
64478
65687
  }
64479
- const id = uuid();
64480
- db.run("INSERT INTO machines (id, name, hostname, platform) VALUES (?, ?, ?, ?)", [id, finalName, host, plat]);
64481
- return parseMachine(db.query("SELECT * FROM machines WHERE id = ?").get(id));
65688
+ const d = db ?? getDatabase();
65689
+ const rows = d.query("SELECT * FROM machines ORDER BY is_primary DESC, last_seen_at DESC, created_at ASC, id ASC").all();
65690
+ return rows.map((row) => machineFromDb(row));
64482
65691
  }
64483
- function getPrimaryMachine(db = getDatabase()) {
64484
- return parseMachine(db.query("SELECT * FROM machines WHERE is_primary = 1 LIMIT 1").get());
65692
+ function getPrimaryMachine(db) {
65693
+ if (!db && isApiMode())
65694
+ return listMachines().find((machine) => machine.is_primary) ?? null;
65695
+ const d = db ?? getDatabase();
65696
+ return machineFromDb(d.query("SELECT * FROM machines WHERE is_primary = 1 LIMIT 1").get());
64485
65697
  }
64486
- function getPrimaryMachineCandidate(db = getDatabase()) {
64487
- if (getPrimaryMachine(db))
65698
+ function getPrimaryMachineCandidate(db) {
65699
+ if (!db && isApiMode()) {
65700
+ const machines = listMachines();
65701
+ if (machines.some((machine) => machine.is_primary))
65702
+ return null;
65703
+ return [...machines].sort((a, b) => a.created_at.localeCompare(b.created_at) || a.id.localeCompare(b.id))[0] ?? null;
65704
+ }
65705
+ const d = db ?? getDatabase();
65706
+ if (getPrimaryMachine(d))
64488
65707
  return null;
64489
- return parseMachine(db.query("SELECT * FROM machines ORDER BY created_at ASC, id ASC LIMIT 1").get());
65708
+ return machineFromDb(d.query("SELECT * FROM machines ORDER BY created_at ASC, id ASC LIMIT 1").get());
64490
65709
  }
64491
- function getPrimaryMachineStartupWarning(db = getDatabase()) {
65710
+ function getPrimaryMachineStartupWarning(db) {
64492
65711
  if (getPrimaryMachine(db))
64493
65712
  return null;
64494
65713
  const candidate = getPrimaryMachineCandidate(db);
64495
- if (!candidate) {
65714
+ if (!candidate)
64496
65715
  return "No primary machine configured. Fallback sync target is unset because no machines are registered yet.";
64497
- }
64498
65716
  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.`;
64499
65717
  }
64500
- function touchMachine(id, db = getDatabase()) {
64501
- db.run("UPDATE machines SET last_seen_at = ? WHERE id = ?", [now(), id]);
64502
- }
64503
- function getCurrentMachineId(db = getDatabase()) {
64504
- const host = normalizeHostname(hostname());
64505
- const m = db.query("SELECT id FROM machines WHERE hostname = ?").get(host);
64506
- if (m) {
64507
- touchMachine(m.id, db);
64508
- return m.id;
65718
+ var hostedMachineCache = null;
65719
+ function getCurrentMachineId(db) {
65720
+ if (!db && isApiMode()) {
65721
+ const host = normalizeMachineHostname(hostname());
65722
+ const plat = normalizeMachinePlatform(platform());
65723
+ const configBefore = getApiConfig();
65724
+ if (!configBefore)
65725
+ throw new Error("mementos hosted machine identity requires a resolved API authority");
65726
+ const credentialFingerprint = createHash("sha256").update(configBefore.apiKey).digest("hex");
65727
+ const key = `${configBefore.baseUrl}\x00${credentialFingerprint}\x00${host}\x00${plat}`;
65728
+ if (hostedMachineCache?.key === key)
65729
+ return hostedMachineCache.id;
65730
+ const id = registerMachine().id;
65731
+ const configAfter = getApiConfig();
65732
+ const fingerprintAfter = configAfter ? createHash("sha256").update(configAfter.apiKey).digest("hex") : null;
65733
+ if (!configAfter || configAfter.baseUrl !== configBefore.baseUrl || fingerprintAfter !== credentialFingerprint) {
65734
+ throw new Error("MEMENTOS_AUTHORITY_CHANGED: the machine registry authority or credential changed while resolving the stable machine identity");
65735
+ }
65736
+ hostedMachineCache = { key, id };
65737
+ return id;
64509
65738
  }
64510
- return registerMachine(undefined, db).id;
65739
+ const d = db ?? getDatabase();
65740
+ return registerMachine(undefined, d).id;
64511
65741
  }
64512
65742
 
64513
65743
  // src/cli/startup-side-effects.ts
@@ -65256,7 +66486,7 @@ init_memories();
65256
66486
  init_package_version();
65257
66487
  init_schema();
65258
66488
  init_identity();
65259
- import { createHash as createHash2 } from "crypto";
66489
+ import { createHash as createHash3 } from "crypto";
65260
66490
 
65261
66491
  class MemoryProjectLinkError extends Error {
65262
66492
  code;
@@ -65296,7 +66526,7 @@ function canonicalJson(value) {
65296
66526
  return JSON.stringify(canonicalize(value));
65297
66527
  }
65298
66528
  function digest(value) {
65299
- return createHash2("sha256").update(canonicalJson(value)).digest("hex");
66529
+ return createHash3("sha256").update(canonicalJson(value)).digest("hex");
65300
66530
  }
65301
66531
  function timestampString2(value) {
65302
66532
  return value instanceof Date ? value.toISOString() : String(value);
@@ -65881,7 +67111,7 @@ function getMemoryProjectLinkReceipt(memoryId, receiptId, identity = linkAuthori
65881
67111
  }
65882
67112
 
65883
67113
  // src/project-registration/authority.ts
65884
- import { createHash as createHash3 } from "crypto";
67114
+ import { createHash as createHash4 } from "crypto";
65885
67115
  init_projects();
65886
67116
  init_types2();
65887
67117
  init_identity();
@@ -65903,7 +67133,7 @@ function canonicalize2(value) {
65903
67133
  return out;
65904
67134
  }
65905
67135
  function digestMementosProjectRegistrationValue(value) {
65906
- return createHash3("sha256").update(canonicalMementosProjectRegistrationJson(value)).digest("hex");
67136
+ return createHash4("sha256").update(canonicalMementosProjectRegistrationJson(value)).digest("hex");
65907
67137
  }
65908
67138
  // src/project-registration/http.ts
65909
67139
  init_types2();
@@ -67303,16 +68533,164 @@ init_redact();
67303
68533
  init_helpers();
67304
68534
  import chalk12 from "chalk";
67305
68535
  import { resolve as resolve8 } from "path";
68536
+ var STRUCTURED_PAGE_MAX_ROWS = 1000;
68537
+ var STRUCTURED_ALL_MAX_ROWS = 5000;
68538
+ var STRUCTURED_DEFAULT_MAX_BYTES = 32 * 1024;
68539
+ var STRUCTURED_FULL_MAX_BYTES = 64 * 1024;
68540
+ var STRUCTURED_ALL_MAX_BYTES = 1024 * 1024;
68541
+ var STRUCTURED_MIN_MAX_BYTES = 1024;
68542
+ function structuredMaxBytes(value, opts) {
68543
+ const fallback = opts.all ? STRUCTURED_ALL_MAX_BYTES : opts.detail === "full" ? STRUCTURED_FULL_MAX_BYTES : STRUCTURED_DEFAULT_MAX_BYTES;
68544
+ if (value === undefined)
68545
+ return fallback;
68546
+ const parsed = typeof value === "number" ? value : Number(value);
68547
+ if (!Number.isInteger(parsed) || parsed < STRUCTURED_MIN_MAX_BYTES || parsed > STRUCTURED_ALL_MAX_BYTES) {
68548
+ throw new Error(`--max-bytes must be an integer from ${STRUCTURED_MIN_MAX_BYTES} to ${STRUCTURED_ALL_MAX_BYTES}`);
68549
+ }
68550
+ return parsed;
68551
+ }
68552
+ function compactMemoryForStructuredOutput(memory, opts = {}) {
68553
+ const value = truncateText(memory.value, 160);
68554
+ const summary = memory.summary ? truncateText(memory.summary, 160) : null;
68555
+ return {
68556
+ id: memory.id,
68557
+ key: memory.key,
68558
+ value,
68559
+ ...summary ? { summary } : {},
68560
+ scope: memory.scope,
68561
+ category: memory.category,
68562
+ importance: memory.importance,
68563
+ status: memory.status,
68564
+ pinned: memory.pinned,
68565
+ ...Array.isArray(memory.tags) && memory.tags.length ? { tags: memory.tags.slice(0, 10) } : {},
68566
+ ...memory.agent_id ? { agent_id: memory.agent_id } : {},
68567
+ ...memory.project_id ? { project_id: memory.project_id } : {},
68568
+ ...memory.session_id ? { session_id: memory.session_id } : {},
68569
+ ...opts.history && memory.accessed_at ? { accessed_at: memory.accessed_at } : {},
68570
+ updated_at: memory.updated_at
68571
+ };
68572
+ }
68573
+ function withResponseBytes(envelope) {
68574
+ for (let attempt = 0;attempt < 8; attempt += 1) {
68575
+ const bytes = Buffer.byteLength(`${JSON.stringify(envelope)}
68576
+ `);
68577
+ if (bytes === envelope._meta.response_bytes)
68578
+ return envelope;
68579
+ envelope._meta.response_bytes = bytes;
68580
+ }
68581
+ return envelope;
68582
+ }
68583
+ function makeStructuredEnvelope(args) {
68584
+ const hasMore = args.byteTruncated || args.sourceHasMore;
68585
+ const complete = args.offset === 0 && !hasMore;
68586
+ const nextCursor = hasMore ? args.offset + args.memories.length : null;
68587
+ const truncationReason = args.byteTruncated ? "max_bytes" : args.sourceHasMore ? "limit" : args.offset > 0 ? "cursor" : null;
68588
+ return withResponseBytes({
68589
+ memories: args.memories,
68590
+ _meta: {
68591
+ receipt: args.receipt,
68592
+ count: args.memories.length,
68593
+ limit: args.all ? null : args.limit,
68594
+ offset: args.offset,
68595
+ next_cursor: nextCursor,
68596
+ has_more: hasMore,
68597
+ complete,
68598
+ all: args.all,
68599
+ detail: args.detail,
68600
+ max_rows: args.all ? STRUCTURED_ALL_MAX_ROWS : STRUCTURED_PAGE_MAX_ROWS,
68601
+ max_bytes: args.maxBytes,
68602
+ response_bytes: 0,
68603
+ truncated: !complete,
68604
+ truncation_reason: truncationReason,
68605
+ omitted_from_page: args.omittedFromPage,
68606
+ next_arguments: nextCursor === null ? null : {
68607
+ cursor: nextCursor,
68608
+ limit: args.limit,
68609
+ agent_json: true,
68610
+ ...args.detail === "full" ? { full: true } : {},
68611
+ max_bytes: args.maxBytes
68612
+ },
68613
+ continuation_scope: "unchanged_snapshot"
68614
+ }
68615
+ });
68616
+ }
68617
+ function structuredMemoryOutput(args) {
68618
+ const projected = args.detail === "full" ? args.memories : args.memories.map((memory) => compactMemoryForStructuredOutput(memory, { history: args.history }));
68619
+ const completeEnvelope = makeStructuredEnvelope({
68620
+ memories: projected,
68621
+ receipt: args.receipt,
68622
+ offset: args.offset,
68623
+ limit: args.limit,
68624
+ sourceHasMore: args.sourceHasMore,
68625
+ all: args.all,
68626
+ detail: args.detail,
68627
+ maxBytes: args.maxBytes,
68628
+ byteTruncated: false,
68629
+ omittedFromPage: 0
68630
+ });
68631
+ const completeText = `${JSON.stringify(completeEnvelope)}
68632
+ `;
68633
+ if (Buffer.byteLength(completeText) <= args.maxBytes)
68634
+ return completeText;
68635
+ if (args.all) {
68636
+ throw new Error(`Exhaustive structured output exceeds the hard safety limit of ${args.maxBytes} bytes; use paginated JSON output instead`);
68637
+ }
68638
+ for (let count = projected.length - 1;count >= 0; count -= 1) {
68639
+ const envelope = makeStructuredEnvelope({
68640
+ memories: projected.slice(0, count),
68641
+ receipt: args.receipt,
68642
+ offset: args.offset,
68643
+ limit: args.limit,
68644
+ sourceHasMore: true,
68645
+ all: false,
68646
+ detail: args.detail,
68647
+ maxBytes: args.maxBytes,
68648
+ byteTruncated: true,
68649
+ omittedFromPage: projected.length - count
68650
+ });
68651
+ const text = `${JSON.stringify(envelope)}
68652
+ `;
68653
+ if (Buffer.byteLength(text) > args.maxBytes)
68654
+ continue;
68655
+ if (count === 0 && projected.length > 0) {
68656
+ throw new Error(`One structured memory row exceeds --max-bytes=${args.maxBytes}; use compact detail, a larger --max-bytes value, or mementos show <id>`);
68657
+ }
68658
+ return text;
68659
+ }
68660
+ throw new Error(`Structured output metadata exceeds --max-bytes=${args.maxBytes}`);
68661
+ }
68662
+ function assertReceiptFlags(opts, receiptMode, requestedFormat) {
68663
+ if (!receiptMode && (opts.all || opts.full || opts.maxBytes !== undefined)) {
68664
+ throw new Error("--all, --full, and --max-bytes require --agent-json receipt mode");
68665
+ }
68666
+ if (receiptMode && requestedFormat !== undefined && requestedFormat !== "json") {
68667
+ throw new Error("--agent-json cannot be combined with a non-JSON --format");
68668
+ }
68669
+ if (opts.all && opts.limit !== undefined) {
68670
+ throw new Error("--all cannot be combined with --limit");
68671
+ }
68672
+ }
67306
68673
  function registerListCommand(program2) {
67307
68674
  const handleError = makeHandleError(program2);
67308
- 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) => {
68675
+ 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 (agent JSON page max: ${STRUCTURED_PAGE_MAX_ROWS})`, 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").option("--agent-json", "Output a bounded, receipt-bearing JSON page").option("--all", `Exhaust agent JSON results from offset zero (hard max: ${STRUCTURED_ALL_MAX_ROWS} rows)`).option("--full", "Emit full memory objects in agent JSON instead of compact projections").option("--max-bytes <n>", `Agent JSON response byte ceiling (hard max: ${STRUCTURED_ALL_MAX_BYTES})`, parseInt).action((opts) => {
67309
68676
  try {
67310
68677
  const globalOpts = program2.opts();
68678
+ const requestedFormat = opts.format ?? globalOpts.format;
67311
68679
  const fmt = getOutputFormat(program2, opts.format);
68680
+ const receiptMode = Boolean(opts.agentJson);
67312
68681
  const isStructured = fmt === "json" || fmt === "csv" || fmt === "yaml";
68682
+ assertReceiptFlags(opts, receiptMode, requestedFormat);
67313
68683
  const requestedLimit = opts.limit;
67314
- const limit = requestedLimit === undefined ? isStructured ? undefined : DEFAULT_COMPACT_LIMIT : positiveIntOrDefault(requestedLimit, isStructured ? 50 : DEFAULT_COMPACT_LIMIT);
68684
+ const all = Boolean(opts.all);
68685
+ const detail = opts.full ? "full" : "compact";
68686
+ const limit = requestedLimit === undefined ? receiptMode ? DEFAULT_COMPACT_LIMIT : isStructured ? undefined : DEFAULT_COMPACT_LIMIT : positiveIntOrDefault(requestedLimit, receiptMode ? DEFAULT_COMPACT_LIMIT : isStructured ? 50 : DEFAULT_COMPACT_LIMIT);
68687
+ if (receiptMode && limit !== undefined && limit > STRUCTURED_PAGE_MAX_ROWS) {
68688
+ throw new Error(`--limit cannot exceed the agent JSON page ceiling of ${STRUCTURED_PAGE_MAX_ROWS}; use --all for a bounded exhaustive read`);
68689
+ }
67315
68690
  const offset = cursorOrOffset(opts.cursor, opts.offset) ?? 0;
68691
+ if (all && offset !== 0) {
68692
+ throw new Error("--all requires --cursor/--offset 0");
68693
+ }
67316
68694
  const agentId = resolveAgentFilter(opts.agent || globalOpts.agent);
67317
68695
  const projectPath = opts.project || globalOpts.project;
67318
68696
  let projectId;
@@ -67334,6 +68712,7 @@ function registerListCommand(program2) {
67334
68712
  status: opts.status,
67335
68713
  session_id: opts.session || globalOpts.session
67336
68714
  };
68715
+ const target = all ? STRUCTURED_ALL_MAX_ROWS : limit;
67337
68716
  const { rows: collected, hasMore } = collectPagedRows((cursor, pageLimit) => {
67338
68717
  const page = listMemoriesPage({
67339
68718
  ...filter,
@@ -67345,9 +68724,25 @@ function registerListCommand(program2) {
67345
68724
  has_more: page.has_more,
67346
68725
  next_cursor: page.next_cursor
67347
68726
  };
67348
- }, limit, offset);
67349
- const memories = hasMore && limit !== undefined ? collected.slice(0, limit) : collected;
68727
+ }, target, offset);
68728
+ if (all && hasMore) {
68729
+ throw new Error(`Exhaustive structured output exceeds the hard safety limit of ${STRUCTURED_ALL_MAX_ROWS} rows; use paginated JSON output instead`);
68730
+ }
68731
+ const memories = target === undefined ? collected : collected.slice(0, target);
67350
68732
  const sanitized = memories.map(redactMemoryForOutput);
68733
+ if (receiptMode) {
68734
+ process.stdout.write(structuredMemoryOutput({
68735
+ memories: sanitized,
68736
+ receipt: "mementos.list.page.v1",
68737
+ offset,
68738
+ limit: limit ?? DEFAULT_COMPACT_LIMIT,
68739
+ sourceHasMore: hasMore,
68740
+ all,
68741
+ detail,
68742
+ maxBytes: structuredMaxBytes(opts.maxBytes, { all, detail })
68743
+ }));
68744
+ return;
68745
+ }
67351
68746
  if (fmt === "json") {
67352
68747
  outputJson(sanitized);
67353
68748
  return;
@@ -67381,7 +68776,7 @@ function registerListCommand(program2) {
67381
68776
  offset,
67382
68777
  hasMore,
67383
68778
  command: "mementos list",
67384
- detailHint: "use mementos show <id> for full details or --json for full objects"
68779
+ detailHint: "use mementos show <id> for full details, --json for the compatible full array, or --agent-json for a bounded receipt"
67385
68780
  });
67386
68781
  } catch (e) {
67387
68782
  handleError(e);
@@ -67808,13 +69203,33 @@ init_helpers();
67808
69203
  import chalk16 from "chalk";
67809
69204
  function registerHistoryCommand(program2) {
67810
69205
  const handleError = makeHandleError(program2);
67811
- program2.command("history").description("List memories sorted by most recently accessed").option("--limit <n>", "Max results (compact default: 10)", parseInt).option("--offset <n>", "Offset for pagination", parseInt).option("--cursor <n>", "Cursor offset for the next page", parseInt).option("--verbose", "Show wider memory snippets").action((opts) => {
69206
+ program2.command("history").description("List memories sorted by most recently accessed").option("--limit <n>", `Max results (agent JSON page max: ${STRUCTURED_PAGE_MAX_ROWS})`, parseInt).option("--offset <n>", "Offset for pagination", parseInt).option("--cursor <n>", "Cursor offset for the next page", parseInt).option("--verbose", "Show wider memory snippets").option("--agent-json", "Output a bounded, receipt-bearing JSON page").option("--all", `Exhaust agent JSON results from offset zero (hard max: ${STRUCTURED_ALL_MAX_ROWS} rows)`).option("--full", "Emit full memory objects in agent JSON instead of compact projections").option("--max-bytes <n>", `Agent JSON response byte ceiling (hard max: ${STRUCTURED_ALL_MAX_BYTES})`, parseInt).action((opts) => {
67812
69207
  try {
67813
69208
  const globalOpts = program2.opts();
67814
- const isJson = Boolean(globalOpts.json);
69209
+ const format = getOutputFormat(program2);
69210
+ const receiptMode = Boolean(opts.agentJson);
69211
+ const isJson = format === "json";
69212
+ if (!receiptMode && (opts.all || opts.full || opts.maxBytes !== undefined)) {
69213
+ throw new Error("--all, --full, and --max-bytes require --agent-json receipt mode");
69214
+ }
69215
+ if (receiptMode && globalOpts.format !== undefined && globalOpts.format !== "json") {
69216
+ throw new Error("--agent-json cannot be combined with a non-JSON --format");
69217
+ }
69218
+ if (opts.all && opts.limit !== undefined) {
69219
+ throw new Error("--all cannot be combined with --limit");
69220
+ }
67815
69221
  const requestedLimit = opts.limit;
67816
- const limit = requestedLimit === undefined ? isJson ? undefined : DEFAULT_SEARCH_LIMIT : positiveIntOrDefault(requestedLimit, isJson ? 20 : DEFAULT_SEARCH_LIMIT);
69222
+ const all = Boolean(opts.all);
69223
+ const detail = opts.full ? "full" : "compact";
69224
+ const limit = requestedLimit === undefined ? receiptMode ? DEFAULT_SEARCH_LIMIT : isJson ? undefined : DEFAULT_SEARCH_LIMIT : positiveIntOrDefault(requestedLimit, receiptMode ? DEFAULT_SEARCH_LIMIT : isJson ? 20 : DEFAULT_SEARCH_LIMIT);
69225
+ if (receiptMode && limit !== undefined && limit > STRUCTURED_PAGE_MAX_ROWS) {
69226
+ throw new Error(`--limit cannot exceed the agent JSON page ceiling of ${STRUCTURED_PAGE_MAX_ROWS}; use --all for a bounded exhaustive read`);
69227
+ }
67817
69228
  const offset = cursorOrOffset(opts.cursor, opts.offset) ?? 0;
69229
+ if (all && offset !== 0) {
69230
+ throw new Error("--all requires --cursor/--offset 0");
69231
+ }
69232
+ const target = all ? STRUCTURED_ALL_MAX_ROWS : limit;
67818
69233
  const { rows: collected, hasMore } = collectPagedRows((cursor, pageLimit) => {
67819
69234
  const page = listMemoryHistoryPage({ limit: pageLimit, offset: cursor });
67820
69235
  return {
@@ -67822,10 +69237,27 @@ function registerHistoryCommand(program2) {
67822
69237
  has_more: page.has_more,
67823
69238
  next_cursor: page.next_cursor
67824
69239
  };
67825
- }, limit, offset);
67826
- const memories = hasMore && limit !== undefined ? collected.slice(0, limit) : collected;
69240
+ }, target, offset);
69241
+ if (all && hasMore) {
69242
+ throw new Error(`Exhaustive structured output exceeds the hard safety limit of ${STRUCTURED_ALL_MAX_ROWS} rows; use paginated JSON output instead`);
69243
+ }
69244
+ const memories = target === undefined ? collected : collected.slice(0, target);
67827
69245
  const sanitized = memories.map(redactMemoryForOutput);
67828
- if (globalOpts.json) {
69246
+ if (receiptMode) {
69247
+ process.stdout.write(structuredMemoryOutput({
69248
+ memories: sanitized,
69249
+ receipt: "mementos.history.page.v1",
69250
+ offset,
69251
+ limit: limit ?? DEFAULT_SEARCH_LIMIT,
69252
+ sourceHasMore: hasMore,
69253
+ all,
69254
+ detail,
69255
+ maxBytes: structuredMaxBytes(opts.maxBytes, { all, detail }),
69256
+ history: true
69257
+ }));
69258
+ return;
69259
+ }
69260
+ if (isJson) {
67829
69261
  outputJson(sanitized);
67830
69262
  return;
67831
69263
  }
@@ -67848,7 +69280,7 @@ function registerHistoryCommand(program2) {
67848
69280
  offset,
67849
69281
  hasMore,
67850
69282
  command: "mementos history",
67851
- detailHint: "use mementos show <id> for full details or --json for full objects"
69283
+ detailHint: "use mementos show <id> for full details, --json for the compatible full array, or --agent-json for a bounded receipt"
67852
69284
  });
67853
69285
  } catch (e) {
67854
69286
  handleError(e);
@@ -67863,13 +69295,16 @@ import chalk17 from "chalk";
67863
69295
  import { resolve as resolve11 } from "path";
67864
69296
 
67865
69297
  // src/lib/machine-visibility.ts
69298
+ init_api_mode();
67866
69299
  function resolveVisibleMachineId(machineId, db) {
67867
69300
  if (machineId !== undefined) {
67868
69301
  return machineId;
67869
69302
  }
67870
69303
  try {
67871
69304
  return getCurrentMachineId(db);
67872
- } catch {
69305
+ } catch (error) {
69306
+ if (!db && isApiMode())
69307
+ throw error;
67873
69308
  return null;
67874
69309
  }
67875
69310
  }
@@ -67900,13 +69335,16 @@ function registerContextCommand(program2) {
67900
69335
  const categoriesRaw = opts.categories;
67901
69336
  const categories = categoriesRaw ? categoriesRaw.split(",").map((c) => c.trim()) : undefined;
67902
69337
  const agentId = resolveAgentFilter(opts.agent || globalOpts.agent);
67903
- const projectPath = opts.project || globalOpts.project;
69338
+ const projectPath = opts.project ?? globalOpts.project;
67904
69339
  const visibleMachineId = resolveVisibleMachineId(opts.machine);
67905
69340
  let projectId;
67906
- if (projectPath) {
69341
+ if (projectPath !== undefined) {
69342
+ if (!projectPath.trim())
69343
+ throw new Error("Project not found: empty project path");
67907
69344
  const project = getProject(resolve11(projectPath));
67908
- if (project)
67909
- projectId = project.id;
69345
+ if (!project)
69346
+ throw new Error(`Project not found: ${projectPath}`);
69347
+ projectId = project.id;
67910
69348
  }
67911
69349
  let memories;
67912
69350
  if (query) {
@@ -67954,6 +69392,7 @@ function registerContextCommand(program2) {
67954
69392
  ...baseFilter,
67955
69393
  scope: "private",
67956
69394
  agent_id: agentId,
69395
+ project_id: projectId,
67957
69396
  ...visibleToMachineFilter(visibleMachineId)
67958
69397
  }));
67959
69398
  }
@@ -68090,247 +69529,13 @@ function registerImportCommand(program2) {
68090
69529
  }
68091
69530
 
68092
69531
  // src/cli/commands/io-clean.ts
69532
+ init_config();
68093
69533
  import chalk19 from "chalk";
68094
69534
 
68095
- // src/lib/config.ts
68096
- init_paths();
68097
- init_env();
68098
- init_local_opt_in();
68099
- import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync6, readdirSync, writeFileSync as writeFileSync3, unlinkSync as unlinkSync2, cpSync as cpSync2 } from "fs";
68100
- import { homedir as homedir4 } from "os";
68101
- import { basename, dirname as dirname4, join as join9, resolve as resolve14 } from "path";
68102
- function isInMemoryDb2(path) {
68103
- return path === ":memory:" || path.startsWith("file::memory:");
68104
- }
68105
- function homeDir() {
68106
- return process.env["HOME"] || process.env["USERPROFILE"] || homedir4();
68107
- }
68108
- var DEFAULT_CONFIG = {
68109
- default_scope: "private",
68110
- default_category: "knowledge",
68111
- default_importance: 5,
68112
- max_entries: 1000,
68113
- max_entries_per_scope: {
68114
- global: 500,
68115
- shared: 300,
68116
- private: 200,
68117
- working: 100
68118
- },
68119
- injection: {
68120
- max_tokens: 500,
68121
- min_importance: 5,
68122
- categories: ["preference", "fact"],
68123
- refresh_interval: 5
68124
- },
68125
- extraction: {
68126
- enabled: true,
68127
- min_confidence: 0.5
68128
- },
68129
- sync_agents: ["claude", "codex", "gemini"],
68130
- auto_cleanup: {
68131
- enabled: true,
68132
- expired_check_interval: 3600,
68133
- unused_archive_days: 7,
68134
- stale_deprioritize_days: 14
68135
- }
68136
- };
68137
- function deepMerge(target, source) {
68138
- const result = { ...target };
68139
- for (const key of Object.keys(source)) {
68140
- const sourceVal = source[key];
68141
- const targetVal = result[key];
68142
- if (sourceVal !== null && typeof sourceVal === "object" && !Array.isArray(sourceVal) && targetVal !== null && typeof targetVal === "object" && !Array.isArray(targetVal)) {
68143
- result[key] = deepMerge(targetVal, sourceVal);
68144
- } else {
68145
- result[key] = sourceVal;
68146
- }
68147
- }
68148
- return result;
68149
- }
68150
- var VALID_SCOPES2 = ["global", "shared", "private", "working"];
68151
- var VALID_CATEGORIES2 = [
68152
- "preference",
68153
- "fact",
68154
- "knowledge",
68155
- "history"
68156
- ];
68157
- function isValidScope(value) {
68158
- return VALID_SCOPES2.includes(value);
68159
- }
68160
- function isValidCategory(value) {
68161
- return VALID_CATEGORIES2.includes(value);
68162
- }
68163
- function loadConfig() {
68164
- const configPath = join9(getDataRoot(), "config.json");
68165
- let fileConfig = {};
68166
- if (existsSync6(configPath)) {
68167
- try {
68168
- const raw = readFileSync6(configPath, "utf-8");
68169
- fileConfig = JSON.parse(raw);
68170
- } catch {}
68171
- }
68172
- const merged = deepMerge(DEFAULT_CONFIG, fileConfig);
68173
- const envScope = env.defaultScope();
68174
- if (envScope && isValidScope(envScope)) {
68175
- merged.default_scope = envScope;
68176
- }
68177
- const envCategory = env.defaultCategory();
68178
- if (envCategory && isValidCategory(envCategory)) {
68179
- merged.default_category = envCategory;
68180
- }
68181
- const envImportance = env.defaultImportance();
68182
- if (envImportance) {
68183
- const parsed = parseInt(envImportance, 10);
68184
- if (!Number.isNaN(parsed) && parsed >= 1 && parsed <= 10) {
68185
- merged.default_importance = parsed;
68186
- }
68187
- }
68188
- return merged;
68189
- }
68190
- function findFileWalkingUp(filename) {
68191
- let dir = process.cwd();
68192
- const legacyHomeMementosDb = resolve14(homeDir(), ".mementos", "mementos.db");
68193
- while (true) {
68194
- const candidate = join9(dir, filename);
68195
- if (existsSync6(candidate) && resolve14(candidate) !== legacyHomeMementosDb) {
68196
- return candidate;
68197
- }
68198
- const parent = dirname4(dir);
68199
- if (parent === dir) {
68200
- return null;
68201
- }
68202
- dir = parent;
68203
- }
68204
- }
68205
- function findGitRoot2() {
68206
- let dir = process.cwd();
68207
- while (true) {
68208
- if (existsSync6(join9(dir, ".git"))) {
68209
- return dir;
68210
- }
68211
- const parent = dirname4(dir);
68212
- if (parent === dir) {
68213
- return null;
68214
- }
68215
- dir = parent;
68216
- }
68217
- }
68218
- function profilesDir() {
68219
- return join9(getDataRoot(), "profiles");
68220
- }
68221
- function globalConfigPath() {
68222
- return join9(getDataRoot(), "config.json");
68223
- }
68224
- function readGlobalConfig() {
68225
- const p = globalConfigPath();
68226
- if (!existsSync6(p))
68227
- return {};
68228
- try {
68229
- return JSON.parse(readFileSync6(p, "utf-8"));
68230
- } catch {
68231
- return {};
68232
- }
68233
- }
68234
- function readGlobalConfigForWrite() {
68235
- const p = globalConfigPath();
68236
- if (!existsSync6(p))
68237
- return {};
68238
- try {
68239
- const data = JSON.parse(readFileSync6(p, "utf-8"));
68240
- if (data === null || typeof data !== "object" || Array.isArray(data)) {
68241
- throw new Error("expected a JSON object");
68242
- }
68243
- return data;
68244
- } catch (error) {
68245
- const detail = error instanceof Error ? error.message : String(error);
68246
- throw new Error(`Cannot update global config at ${p}: ${detail}`);
68247
- }
68248
- }
68249
- function writeGlobalConfig(data) {
68250
- const p = globalConfigPath();
68251
- ensureDir2(dirname4(p));
68252
- writeFileSync3(p, JSON.stringify(data, null, 2), "utf-8");
68253
- }
68254
- function getActiveProfile() {
68255
- const envProfile = process.env["MEMENTOS_PROFILE"];
68256
- if (envProfile)
68257
- return envProfile.trim();
68258
- const cfg = readGlobalConfig();
68259
- return cfg["active_profile"] || null;
68260
- }
68261
- function setActiveProfile(name) {
68262
- const cfg = readGlobalConfigForWrite();
68263
- if (name === null) {
68264
- delete cfg["active_profile"];
68265
- } else {
68266
- cfg["active_profile"] = name;
68267
- }
68268
- writeGlobalConfig(cfg);
68269
- }
68270
- function listProfiles() {
68271
- const dir = profilesDir();
68272
- if (!existsSync6(dir))
68273
- return [];
68274
- return readdirSync(dir).filter((f) => f.endsWith(".db")).map((f) => basename(f, ".db")).sort();
68275
- }
68276
- function deleteProfile(name) {
68277
- const dbPath = join9(profilesDir(), `${name}.db`);
68278
- if (!existsSync6(dbPath))
68279
- return false;
68280
- unlinkSync2(dbPath);
68281
- if (getActiveProfile() === name)
68282
- setActiveProfile(null);
68283
- return true;
68284
- }
68285
- function getDbPath2() {
68286
- const _home = homeDir();
68287
- const _newDir = getDataRoot();
68288
- const _oldDir = join9(_home, ".mementos");
68289
- if (selectsMementosLocalStore() && !existsSync6(_newDir) && existsSync6(_oldDir)) {
68290
- mkdirSync3(join9(_home, ".hasna"), { recursive: true });
68291
- cpSync2(_oldDir, _newDir, { recursive: true });
68292
- }
68293
- const envDbPath = process.env["HASNA_MEMENTOS_DB_PATH"] ?? process.env["MEMENTOS_DB_PATH"];
68294
- if (envDbPath) {
68295
- if (isInMemoryDb2(envDbPath)) {
68296
- return envDbPath;
68297
- }
68298
- const resolved = resolve14(envDbPath);
68299
- ensureDir2(dirname4(resolved));
68300
- return resolved;
68301
- }
68302
- const profile = getActiveProfile();
68303
- if (profile) {
68304
- const profilePath = join9(profilesDir(), `${profile}.db`);
68305
- ensureDir2(dirname4(profilePath));
68306
- return profilePath;
68307
- }
68308
- const dbScope = process.env["MEMENTOS_DB_SCOPE"];
68309
- if (dbScope === "project") {
68310
- const gitRoot = findGitRoot2();
68311
- if (gitRoot) {
68312
- const dbPath = join9(gitRoot, ".mementos", "mementos.db");
68313
- ensureDir2(dirname4(dbPath));
68314
- return dbPath;
68315
- }
68316
- }
68317
- const found = findFileWalkingUp(join9(".mementos", "mementos.db"));
68318
- if (found) {
68319
- return found;
68320
- }
68321
- const fallback = join9(getDataRoot(), "mementos.db");
68322
- ensureDir2(dirname4(fallback));
68323
- return fallback;
68324
- }
68325
- function ensureDir2(dir) {
68326
- if (!existsSync6(dir)) {
68327
- mkdirSync3(dir, { recursive: true });
68328
- }
68329
- }
68330
-
68331
69535
  // src/lib/retention.ts
68332
69536
  init_database();
68333
69537
  init_memories();
69538
+ init_config();
68334
69539
  function enforceQuotas(config, db) {
68335
69540
  const d = db || getDatabase();
68336
69541
  let totalEvicted = 0;
@@ -68438,11 +69643,108 @@ function registerCleanCommand(program2) {
68438
69643
 
68439
69644
  // src/cli/commands/io-backup.ts
68440
69645
  init_database();
69646
+ init_api_mode();
69647
+ init_memories();
68441
69648
  init_paths();
68442
69649
  init_helpers();
68443
69650
  import chalk20 from "chalk";
68444
69651
  import { join as join10, resolve as resolve15, dirname as dirname5 } from "path";
68445
- import { existsSync as existsSync7, statSync, copyFileSync, mkdirSync as mkdirSync4, readdirSync as readdirSync2 } from "fs";
69652
+ import { existsSync as existsSync7, statSync, copyFileSync, mkdirSync as mkdirSync4, readdirSync as readdirSync2, unlinkSync as unlinkSync3 } from "fs";
69653
+ function writeMemoriesBackupFile(dest, rows) {
69654
+ const { Database: Database3 } = __require("bun:sqlite");
69655
+ if (existsSync7(dest)) {
69656
+ unlinkSync3(dest);
69657
+ }
69658
+ const backupDb = new Database3(dest);
69659
+ try {
69660
+ backupDb.run(`
69661
+ CREATE TABLE IF NOT EXISTS _backup_carrier (
69662
+ format INTEGER NOT NULL
69663
+ )
69664
+ `);
69665
+ backupDb.run(`INSERT INTO _backup_carrier (format) VALUES (1)`);
69666
+ backupDb.run(`
69667
+ CREATE TABLE IF NOT EXISTS memories (
69668
+ id TEXT PRIMARY KEY,
69669
+ key TEXT NOT NULL,
69670
+ value TEXT NOT NULL,
69671
+ category TEXT,
69672
+ scope TEXT,
69673
+ summary TEXT,
69674
+ tags TEXT,
69675
+ importance INTEGER,
69676
+ source TEXT,
69677
+ status TEXT,
69678
+ pinned INTEGER,
69679
+ agent_id TEXT,
69680
+ project_id TEXT,
69681
+ session_id TEXT,
69682
+ machine_id TEXT,
69683
+ namespace TEXT,
69684
+ created_by_agent TEXT,
69685
+ when_to_use TEXT,
69686
+ sequence_group TEXT,
69687
+ sequence_order INTEGER,
69688
+ metadata TEXT,
69689
+ access_count INTEGER,
69690
+ version INTEGER,
69691
+ expires_at TEXT,
69692
+ valid_from TEXT,
69693
+ valid_until TEXT,
69694
+ ingested_at TEXT,
69695
+ created_at TEXT,
69696
+ updated_at TEXT,
69697
+ accessed_at TEXT
69698
+ )
69699
+ `);
69700
+ const insert = backupDb.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, accessed_at)
69701
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
69702
+ const bind = (v) => {
69703
+ if (v === undefined || v === null)
69704
+ return null;
69705
+ return typeof v === "string" || typeof v === "number" ? v : JSON.stringify(v);
69706
+ };
69707
+ const boolBind = (v) => v === undefined || v === null ? null : v ? 1 : 0;
69708
+ for (const row of rows) {
69709
+ const params = [
69710
+ bind(row["id"]),
69711
+ bind(row["key"]),
69712
+ bind(row["value"]),
69713
+ bind(row["category"]),
69714
+ bind(row["scope"]),
69715
+ bind(row["summary"]),
69716
+ Array.isArray(row["tags"]) ? JSON.stringify(row["tags"]) : bind(row["tags"]),
69717
+ bind(row["importance"]),
69718
+ bind(row["source"]),
69719
+ bind(row["status"]),
69720
+ boolBind(row["pinned"]),
69721
+ bind(row["agent_id"]),
69722
+ bind(row["project_id"]),
69723
+ bind(row["session_id"]),
69724
+ bind(row["machine_id"]),
69725
+ bind(row["namespace"]),
69726
+ bind(row["created_by_agent"]),
69727
+ bind(row["when_to_use"]),
69728
+ bind(row["sequence_group"]),
69729
+ bind(row["sequence_order"]),
69730
+ bind(row["metadata"]),
69731
+ bind(row["access_count"]),
69732
+ bind(row["version"]),
69733
+ bind(row["expires_at"]),
69734
+ bind(row["valid_from"]),
69735
+ bind(row["valid_until"]),
69736
+ bind(row["ingested_at"]),
69737
+ bind(row["created_at"]),
69738
+ bind(row["updated_at"]),
69739
+ bind(row["accessed_at"])
69740
+ ];
69741
+ insert.run(...params);
69742
+ }
69743
+ return rows.length;
69744
+ } finally {
69745
+ backupDb.close();
69746
+ }
69747
+ }
68446
69748
  function registerBackupCommand(program2) {
68447
69749
  const handleError = makeHandleError(program2);
68448
69750
  program2.command("backup [path]").description("Backup the SQLite database to a file").option("--list", "List available backups in the mementos backups dir").action((targetPath, opts) => {
@@ -68491,11 +69793,6 @@ function registerBackupCommand(program2) {
68491
69793
  }
68492
69794
  return;
68493
69795
  }
68494
- const dbPath = getDbPath();
68495
- if (!existsSync7(dbPath)) {
68496
- console.error(chalk20.red(`Database not found at ${dbPath}`));
68497
- process.exit(1);
68498
- }
68499
69796
  let dest;
68500
69797
  if (targetPath) {
68501
69798
  dest = resolve15(targetPath);
@@ -68511,6 +69808,26 @@ function registerBackupCommand(program2) {
68511
69808
  if (!existsSync7(destDir)) {
68512
69809
  mkdirSync4(destDir, { recursive: true });
68513
69810
  }
69811
+ if (isApiMode()) {
69812
+ const { rows } = listMemoriesBounded({}, undefined);
69813
+ const count = writeMemoriesBackupFile(dest, rows);
69814
+ const st2 = statSync(dest);
69815
+ const sizeMB2 = (st2.size / (1024 * 1024)).toFixed(1);
69816
+ const sizeStr2 = st2.size >= 1024 * 1024 ? `${sizeMB2} MB` : `${(st2.size / 1024).toFixed(1)} KB`;
69817
+ if (globalOpts.json) {
69818
+ outputJson({ backed_up_to: dest, size: st2.size, source: "cloud-api", memories: count });
69819
+ return;
69820
+ }
69821
+ console.log(`Backed up to: ${chalk20.green(dest)} (size: ${sizeStr2})`);
69822
+ console.log(` Source: hosted store (cloud-api)`);
69823
+ console.log(` Memories: ${chalk20.cyan(String(count))}`);
69824
+ return;
69825
+ }
69826
+ const dbPath = getDbPath();
69827
+ if (!existsSync7(dbPath)) {
69828
+ console.error(chalk20.red(`Database not found at ${dbPath}`));
69829
+ process.exit(1);
69830
+ }
68514
69831
  copyFileSync(dbPath, dest);
68515
69832
  const st = statSync(dest);
68516
69833
  const sizeMB = (st.size / (1024 * 1024)).toFixed(1);
@@ -68545,6 +69862,28 @@ function readMemoriesFromBackup(source) {
68545
69862
  backupDb.close();
68546
69863
  }
68547
69864
  }
69865
+ function inspectLocalRestoreSource(source) {
69866
+ const { Database: Database3 } = __require("bun:sqlite");
69867
+ let backupDb;
69868
+ try {
69869
+ backupDb = new Database3(source, { readonly: true });
69870
+ } catch (error) {
69871
+ throw new Error(`Backup file is not a readable SQLite database: ${source} ` + `(${error instanceof Error ? error.message : String(error)})`);
69872
+ }
69873
+ try {
69874
+ const tables = backupDb.query("SELECT name FROM sqlite_master WHERE type = 'table'").all().map((t) => t.name);
69875
+ if (tables.includes("_backup_carrier")) {
69876
+ throw new Error(`${source} is a memories-only cloud backup (created by \`mementos backup\` in hosted mode), ` + `not a full local database backup. Restoring it over the on-box database would drop every ` + `non-memories table and its migration history, and the next migration run would collide with ` + `the half-shaped schema. Restore it into the hosted store instead (run \`mementos restore\` with ` + `a hosted credential), or re-import its memories with \`mementos import\`.`);
69877
+ }
69878
+ if (!tables.includes("memories")) {
69879
+ throw new Error(`Backup file has no mementos \`memories\` table: ${source} \u2014 it is not a mementos backup.`);
69880
+ }
69881
+ const row = backupDb.query("SELECT COUNT(*) as count FROM memories").get();
69882
+ return { memories: row?.count ?? 0 };
69883
+ } finally {
69884
+ backupDb.close();
69885
+ }
69886
+ }
68548
69887
  function registerRestoreCommand(program2) {
68549
69888
  const handleError = makeHandleError(program2);
68550
69889
  program2.command("restore [file]").description("Restore the database from a backup file").option("--latest", "Restore the most recent backup from the mementos backups dir").option("--force", "Skip confirmation and perform the restore").action((filePath, opts) => {
@@ -68655,14 +69994,7 @@ function registerRestoreCommand(program2) {
68655
69994
  currentCount = row?.count ?? 0;
68656
69995
  } catch {}
68657
69996
  }
68658
- let backupCount = 0;
68659
- try {
68660
- const { Database: Database3 } = __require("bun:sqlite");
68661
- const backupDb = new Database3(source, { readonly: true });
68662
- const row = backupDb.query("SELECT COUNT(*) as count FROM memories").get();
68663
- backupCount = row?.count ?? 0;
68664
- backupDb.close();
68665
- } catch {}
69997
+ const backupCount = inspectLocalRestoreSource(source).memories;
68666
69998
  if (!opts.force) {
68667
69999
  if (globalOpts.json) {
68668
70000
  outputJson({
@@ -69103,14 +70435,17 @@ function registerProjectCommands(program2) {
69103
70435
  const categoriesRaw = opts.categories || "preference,fact,knowledge";
69104
70436
  const categories = categoriesRaw.split(",").map((c) => c.trim());
69105
70437
  const agentId = opts.agent || globalOpts.agent;
69106
- const projectPath = opts.project || globalOpts.project;
70438
+ const projectPath = opts.project ?? globalOpts.project;
69107
70439
  const sessionId = opts.session || globalOpts.session;
69108
70440
  const visibleMachineId = resolveVisibleMachineId(opts.machine);
69109
70441
  let projectId;
69110
- if (projectPath) {
70442
+ if (projectPath !== undefined) {
70443
+ if (!projectPath.trim())
70444
+ throw new Error("Project not found: empty project path");
69111
70445
  const project = getProject(resolve17(projectPath));
69112
- if (project)
69113
- projectId = project.id;
70446
+ if (!project)
70447
+ throw new Error(`Project not found: ${projectPath}`);
70448
+ projectId = project.id;
69114
70449
  }
69115
70450
  const allMemories = [];
69116
70451
  const globalMems = listMemories({
@@ -69142,6 +70477,7 @@ function registerProjectCommands(program2) {
69142
70477
  min_importance: minImportance,
69143
70478
  status: "active",
69144
70479
  agent_id: agentId,
70480
+ project_id: projectId,
69145
70481
  session_id: sessionId,
69146
70482
  ...visibleToMachineFilter(visibleMachineId),
69147
70483
  limit: 50
@@ -69901,6 +71237,8 @@ init_database();
69901
71237
  init_memories();
69902
71238
  init_agents();
69903
71239
  init_projects();
71240
+ init_config();
71241
+ init_api_mode();
69904
71242
  import chalk27 from "chalk";
69905
71243
  import { join as join12 } from "path";
69906
71244
  import { homedir as homedir5 } from "os";
@@ -69911,7 +71249,6 @@ import {
69911
71249
  statSync as statSync3,
69912
71250
  constants as fsConstants
69913
71251
  } from "fs";
69914
- init_api_mode();
69915
71252
  init_helpers();
69916
71253
  async function runCommandWithTimeout(args, timeoutMs) {
69917
71254
  const proc = Bun.spawn(args, { stdout: "pipe", stderr: "pipe" });
@@ -70340,8 +71677,9 @@ function registerStatusCommand(program2) {
70340
71677
  }
70341
71678
 
70342
71679
  // src/cli/commands/system-config.ts
70343
- import chalk28 from "chalk";
71680
+ init_config();
70344
71681
  init_helpers();
71682
+ import chalk28 from "chalk";
70345
71683
  function registerConfigCommand(program2) {
70346
71684
  program2.command("config [subcommand] [args...]").description("View or modify configuration. Subcommands: get <key>, set <key> <value>, reset [key], path").action((subcommand, args) => {
70347
71685
  try {
@@ -70427,9 +71765,9 @@ function registerConfigCommand(program2) {
70427
71765
  }
70428
71766
  } else {
70429
71767
  const configPath = getConfigPath2();
70430
- const { unlinkSync: unlinkSync3, existsSync: _existsSync } = __require("fs");
71768
+ const { unlinkSync: unlinkSync4, existsSync: _existsSync } = __require("fs");
70431
71769
  if (_existsSync(configPath)) {
70432
- unlinkSync3(configPath);
71770
+ unlinkSync4(configPath);
70433
71771
  }
70434
71772
  if (useJson) {
70435
71773
  outputJson({ reset: true, all: true });
@@ -70449,10 +71787,11 @@ function registerConfigCommand(program2) {
70449
71787
  }
70450
71788
 
70451
71789
  // src/cli/commands/system-profile.ts
70452
- import chalk29 from "chalk";
70453
- import { join as join13 } from "path";
71790
+ init_config();
70454
71791
  init_paths();
70455
71792
  init_helpers();
71793
+ import chalk29 from "chalk";
71794
+ import { join as join13 } from "path";
70456
71795
  function registerProfileCommand(program2) {
70457
71796
  const profileCmd = program2.command("profile").description("Manage named profile files and active-profile metadata");
70458
71797
  profileCmd.command("list").description("List all available profiles").action(() => {
@@ -70860,10 +72199,12 @@ function registerSessionCommand(program2) {
70860
72199
  const sessionCmd = program2.command("session").description("Session auto-memory \u2014 ingest session transcripts for memory extraction");
70861
72200
  sessionCmd.command("ingest <transcriptFile>").description("Ingest a session transcript file for memory extraction").option("--session-id <id>", "Session ID (default: auto-generated)").option("--agent <id>", "Agent ID").option("--project <id>", "Project ID").option("--source <source>", "Source (claude-code, codex, manual, open-sessions)", "manual").action(async (transcriptFile, opts) => {
70862
72201
  const { readFileSync: _rfs } = await import("fs");
70863
- const { createSessionJob: createSessionJob2 } = await Promise.resolve().then(() => (init_session_jobs(), exports_session_jobs));
70864
- const { enqueueSessionJob: enqueueSessionJob2 } = await Promise.resolve().then(() => (init_session_queue(), exports_session_queue));
70865
72202
  const transcript = _rfs(transcriptFile, "utf-8");
70866
72203
  const sessionId = opts.sessionId ?? `cli-${Date.now()}`;
72204
+ const { isApiMode: isApiMode2 } = await Promise.resolve().then(() => (init_api_mode(), exports_api_mode));
72205
+ const hosted = isApiMode2();
72206
+ const { createSessionJob: createSessionJob2 } = await Promise.resolve().then(() => (init_session_jobs(), exports_session_jobs));
72207
+ const { enqueueSessionJob: enqueueSessionJob2 } = await Promise.resolve().then(() => (init_session_queue(), exports_session_queue));
70867
72208
  const job = createSessionJob2({
70868
72209
  session_id: sessionId,
70869
72210
  transcript,
@@ -70872,7 +72213,7 @@ function registerSessionCommand(program2) {
70872
72213
  project_id: opts.project
70873
72214
  });
70874
72215
  enqueueSessionJob2(job.id);
70875
- console.log(chalk33.green(`\u2713 Session queued: ${chalk33.cyan(job.id)}`));
72216
+ console.log(chalk33.green(`\u2713 Session queued${hosted ? " (hosted store)" : ""}: ${chalk33.cyan(job.id)}`));
70876
72217
  console.log(` Session: ${sessionId}`);
70877
72218
  console.log(` Length: ${transcript.length} chars`);
70878
72219
  });
@@ -70891,14 +72232,16 @@ function registerSessionCommand(program2) {
70891
72232
  if (job.error)
70892
72233
  console.log(chalk33.red(` Error: ${job.error}`));
70893
72234
  });
70894
- sessionCmd.command("list").description("List session extraction jobs").option("--agent <id>", "Filter by agent").option("--project <id>", "Filter by project").option("--status <status>", "Filter by status").option("--limit <n>", "Max results", "20").action(async (opts) => {
72235
+ sessionCmd.command("list").description("List session extraction jobs").option("--agent <id>", "Filter by agent").option("--project <id>", "Filter by project").option("--status <status>", "Filter by status").option("--session-id <id>", "Filter by session ID").option("--limit <n>", "Max results", "20").option("--offset <n>", "Result offset", "0").action(async (opts) => {
70895
72236
  const globalOpts = program2.opts();
70896
72237
  const { listSessionJobs: listSessionJobs2 } = await Promise.resolve().then(() => (init_session_jobs(), exports_session_jobs));
70897
72238
  const jobs = listSessionJobs2({
70898
72239
  agent_id: opts.agent,
70899
72240
  project_id: opts.project,
70900
72241
  status: opts.status,
70901
- limit: parseInt(opts.limit)
72242
+ session_id: opts.sessionId,
72243
+ limit: parseInt(opts.limit, 10),
72244
+ offset: parseInt(opts.offset, 10)
70902
72245
  });
70903
72246
  if (globalOpts.json) {
70904
72247
  outputJson(jobs);
@@ -71044,7 +72387,7 @@ import chalk35 from "chalk";
71044
72387
  import { resolve as resolve19 } from "path";
71045
72388
  function registerSynthesizedProfileCommand(program2) {
71046
72389
  const handleError = makeHandleError(program2);
71047
- program2.command("synthesized-profile").description("Show or refresh the synthesized agent/project profile").option("--project-id <id>", "Project ID").option("--refresh", "Force refresh the profile (re-synthesize from memories)").action(async (opts) => {
72390
+ program2.command("synthesized-profile").description("Show or refresh the synthesized agent/project profile").option("--project-id <id>", "Project ID").option("--scope <scope>", "Profile scope: agent, project, or global").option("--refresh", "Force refresh the profile (re-synthesize from memories)").action(async (opts) => {
71048
72391
  try {
71049
72392
  const globalOpts = program2.opts();
71050
72393
  const { synthesizeProfile: synthesizeProfile2 } = await Promise.resolve().then(() => (init_profile_synthesizer(), exports_profile_synthesizer));
@@ -71058,6 +72401,7 @@ function registerSynthesizedProfileCommand(program2) {
71058
72401
  const result = await synthesizeProfile2({
71059
72402
  project_id: projectId,
71060
72403
  agent_id: globalOpts.agent,
72404
+ scope: opts.scope,
71061
72405
  force_refresh: !!opts.refresh
71062
72406
  });
71063
72407
  if (!result) {
@@ -71575,6 +72919,15 @@ import chalk40 from "chalk";
71575
72919
  // src/lib/storage-sync.ts
71576
72920
  init_storage();
71577
72921
  init_database();
72922
+ init_config();
72923
+ init_local_opt_in();
72924
+ var STORAGE_SYNC_LOCAL_ONLY_MESSAGE = "REMOTE_COMMAND_UNSUPPORTED: mementos storage push/pull/sync/status operate on the on-box SQLite store, " + "which is opt-in only (" + `${MEMENTOS_LOCAL_OPT_IN_ENV_KEYS[0]}=1 or an explicit ${MEMENTOS_DB_PATH_ENV_KEYS[0]}` + ") and disabled by default. On the hosted route there is no local store to sync \u2014 refusing rather than " + "creating one. Run these verbs where the on-box store deliberately lives (the local opt-in) or in mementos-serve.";
72925
+ function openLocalSyncAdapter() {
72926
+ if (!isServerContext() && !selectsMementosLocalStore(process.env)) {
72927
+ throw new Error(STORAGE_SYNC_LOCAL_ONLY_MESSAGE);
72928
+ }
72929
+ return new SqliteAdapter(getDbPath2());
72930
+ }
71578
72931
  var MEMORY_TABLE = "memories";
71579
72932
  var MEMORY_SYNC_META_TABLE = "_mementos_storage_sync_meta";
71580
72933
  var SOURCE_MACHINE_METADATA_KEY = "last_synced_source_machine";
@@ -71953,7 +73306,7 @@ function resolveTables(local, tables) {
71953
73306
  function withManagedAdapters(options, fn) {
71954
73307
  const localOwned = !options.local;
71955
73308
  const remoteOwned = !options.remote;
71956
- const local = options.local ?? new SqliteAdapter(getDbPath2());
73309
+ const local = options.local ?? openLocalSyncAdapter();
71957
73310
  const remote = options.remote ?? new PgAdapter(getStorageConnectionString("mementos"));
71958
73311
  try {
71959
73312
  const currentMachineId = resolveCurrentMachineId(local, options.current_machine_id);
@@ -72009,7 +73362,7 @@ function pullStorageChanges(options = {}) {
72009
73362
  function getStorageSyncStatus(options = {}) {
72010
73363
  const backend = getStorageBackend();
72011
73364
  const localOwned = !options.local;
72012
- const local = options.local ?? new SqliteAdapter(getDbPath2());
73365
+ const local = options.local ?? openLocalSyncAdapter();
72013
73366
  try {
72014
73367
  const currentMachineId = resolveCurrentMachineId(local, options.current_machine_id);
72015
73368
  return {