@hasna/mementos 0.15.2 → 0.17.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 (130) hide show
  1. package/README.md +83 -22
  2. package/dist/audit-contract.d.ts +87 -0
  3. package/dist/audit-contract.d.ts.map +1 -0
  4. package/dist/cli/commands/agent.d.ts.map +1 -1
  5. package/dist/cli/commands/consolidation.d.ts.map +1 -1
  6. package/dist/cli/commands/info-context.d.ts.map +1 -1
  7. package/dist/cli/commands/info-history.d.ts.map +1 -1
  8. package/dist/cli/commands/io-backup.d.ts.map +1 -1
  9. package/dist/cli/commands/io-export.d.ts.map +1 -1
  10. package/dist/cli/commands/io-restore.d.ts.map +1 -1
  11. package/dist/cli/commands/memory-cmd-crud.d.ts.map +1 -1
  12. package/dist/cli/commands/memory-cmd-list.d.ts +25 -0
  13. package/dist/cli/commands/memory-cmd-list.d.ts.map +1 -1
  14. package/dist/cli/commands/memory-cmd-recall.d.ts.map +1 -1
  15. package/dist/cli/commands/memory-cmd-remove.d.ts.map +1 -1
  16. package/dist/cli/commands/memory-cmd-search.d.ts.map +1 -1
  17. package/dist/cli/commands/memory-cmd-view.d.ts.map +1 -1
  18. package/dist/cli/commands/memory-cmd-when-to-use.d.ts.map +1 -1
  19. package/dist/cli/commands/project.d.ts.map +1 -1
  20. package/dist/cli/commands/system-session.d.ts.map +1 -1
  21. package/dist/cli/commands/system-status.d.ts.map +1 -1
  22. package/dist/cli/commands/system-synthesized-profile.d.ts.map +1 -1
  23. package/dist/cli/helpers.d.ts +3 -1
  24. package/dist/cli/helpers.d.ts.map +1 -1
  25. package/dist/cli/index.js +2482 -823
  26. package/dist/cli/structured-json.d.ts +54 -0
  27. package/dist/cli/structured-json.d.ts.map +1 -0
  28. package/dist/db/api-mode.d.ts +10 -0
  29. package/dist/db/api-mode.d.ts.map +1 -1
  30. package/dist/db/api-response-contract.d.ts +23 -0
  31. package/dist/db/api-response-contract.d.ts.map +1 -0
  32. package/dist/db/audit.d.ts +34 -45
  33. package/dist/db/audit.d.ts.map +1 -1
  34. package/dist/db/database.d.ts +1 -0
  35. package/dist/db/database.d.ts.map +1 -1
  36. package/dist/db/locks.d.ts.map +1 -1
  37. package/dist/db/machines.d.ts +50 -13
  38. package/dist/db/machines.d.ts.map +1 -1
  39. package/dist/db/memories.d.ts.map +1 -1
  40. package/dist/db/migrations.d.ts.map +1 -1
  41. package/dist/db/pg-migrate.d.ts.map +1 -1
  42. package/dist/db/pg-migrations.d.ts.map +1 -1
  43. package/dist/db/session-jobs.d.ts +2 -0
  44. package/dist/db/session-jobs.d.ts.map +1 -1
  45. package/dist/diagnostics/historical-project-registration-receipt.js +215 -1
  46. package/dist/index.d.ts +4 -2
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +1523 -206
  49. package/dist/lib/built-in-hooks.d.ts.map +1 -1
  50. package/dist/lib/conversations-transport.d.ts +21 -0
  51. package/dist/lib/conversations-transport.d.ts.map +1 -0
  52. package/dist/lib/export-v1.d.ts +2 -0
  53. package/dist/lib/export-v1.d.ts.map +1 -1
  54. package/dist/lib/injection-project.d.ts +10 -0
  55. package/dist/lib/injection-project.d.ts.map +1 -0
  56. package/dist/lib/injector.d.ts +1 -0
  57. package/dist/lib/injector.d.ts.map +1 -1
  58. package/dist/lib/machine-visibility.d.ts.map +1 -1
  59. package/dist/lib/memory-lock.d.ts.map +1 -1
  60. package/dist/lib/profile-synthesizer.d.ts +13 -2
  61. package/dist/lib/profile-synthesizer.d.ts.map +1 -1
  62. package/dist/lib/session-queue.d.ts.map +1 -1
  63. package/dist/lib/session-registry.d.ts +38 -1
  64. package/dist/lib/session-registry.d.ts.map +1 -1
  65. package/dist/lib/storage-sync.d.ts +5 -0
  66. package/dist/lib/storage-sync.d.ts.map +1 -1
  67. package/dist/lib/synthesis/index.d.ts.map +1 -1
  68. package/dist/mcp/index.d.ts +1 -1
  69. package/dist/mcp/index.d.ts.map +1 -1
  70. package/dist/mcp/index.js +3857 -1600
  71. package/dist/mcp/memory-broadcast.d.ts +6 -8
  72. package/dist/mcp/memory-broadcast.d.ts.map +1 -1
  73. package/dist/mcp/profile.d.ts +23 -0
  74. package/dist/mcp/profile.d.ts.map +1 -0
  75. package/dist/mcp/tools/bounded-output.d.ts +27 -0
  76. package/dist/mcp/tools/bounded-output.d.ts.map +1 -0
  77. package/dist/mcp/tools/lock-tools.d.ts.map +1 -1
  78. package/dist/mcp/tools/memory-audit.d.ts +1 -1
  79. package/dist/mcp/tools/memory-audit.d.ts.map +1 -1
  80. package/dist/mcp/tools/memory-crud.d.ts.map +1 -1
  81. package/dist/mcp/tools/memory-inject.d.ts.map +1 -1
  82. package/dist/mcp/tools/memory-io.d.ts.map +1 -1
  83. package/dist/mcp/tools/project-tools.d.ts.map +1 -1
  84. package/dist/mcp/tools/session-tools.d.ts.map +1 -1
  85. package/dist/mcp/tools/tool-registry.d.ts +22 -19
  86. package/dist/mcp/tools/tool-registry.d.ts.map +1 -1
  87. package/dist/mcp/tools/utility-tools.d.ts +2 -1
  88. package/dist/mcp/tools/utility-tools.d.ts.map +1 -1
  89. package/dist/project-registration.js +276 -20
  90. package/dist/sdk/index.d.ts +110 -7
  91. package/dist/sdk/index.d.ts.map +1 -1
  92. package/dist/sdk/index.js +746 -28
  93. package/dist/server/index.d.ts +2 -0
  94. package/dist/server/index.d.ts.map +1 -1
  95. package/dist/server/index.js +3195 -1134
  96. package/dist/server/openapi.d.ts.map +1 -1
  97. package/dist/server/routes/audit.d.ts +2 -0
  98. package/dist/server/routes/audit.d.ts.map +1 -0
  99. package/dist/server/routes/machines.d.ts +2 -0
  100. package/dist/server/routes/machines.d.ts.map +1 -0
  101. package/dist/server/routes/system-sessions.d.ts.map +1 -1
  102. package/dist/server/routes/system-synthesis.d.ts.map +1 -1
  103. package/dist/types/index.d.ts +3 -0
  104. package/dist/types/index.d.ts.map +1 -1
  105. package/hasna.contract.json +2 -2
  106. package/package.json +8 -5
  107. package/dist/cli/__fixtures__/clean-fallback-stub-server.d.ts +0 -2
  108. package/dist/cli/__fixtures__/clean-fallback-stub-server.d.ts.map +0 -1
  109. package/dist/cli/__fixtures__/io-restore-stub-server.d.ts +0 -2
  110. package/dist/cli/__fixtures__/io-restore-stub-server.d.ts.map +0 -1
  111. package/dist/db/__fixtures__/fail-closed-stub-server.d.ts +0 -2
  112. package/dist/db/__fixtures__/fail-closed-stub-server.d.ts.map +0 -1
  113. package/dist/db/__fixtures__/list-filter-capture-server.d.ts +0 -2
  114. package/dist/db/__fixtures__/list-filter-capture-server.d.ts.map +0 -1
  115. package/dist/db/__fixtures__/list-filter-client-runner.d.ts +0 -2
  116. package/dist/db/__fixtures__/list-filter-client-runner.d.ts.map +0 -1
  117. package/dist/test-support/memories-page-stub-server.d.ts +0 -2
  118. package/dist/test-support/memories-page-stub-server.d.ts.map +0 -1
  119. package/dist/test-support/memories-page-stub.d.ts +0 -28
  120. package/dist/test-support/memories-page-stub.d.ts.map +0 -1
  121. package/dist/test-support/pg-sync-stub-worker.d.ts +0 -2
  122. package/dist/test-support/pg-sync-stub-worker.d.ts.map +0 -1
  123. package/dist/test-support/preload-local-store.d.ts +0 -2
  124. package/dist/test-support/preload-local-store.d.ts.map +0 -1
  125. package/dist/test-support/project-authority-identity.d.ts +0 -8
  126. package/dist/test-support/project-authority-identity.d.ts.map +0 -1
  127. package/dist/test-support/store-isolation.d.ts +0 -147
  128. package/dist/test-support/store-isolation.d.ts.map +0 -1
  129. package/dist/test-support/strip-ansi.d.ts +0 -3
  130. 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);
@@ -7871,6 +8218,7 @@ __export(exports_helpers, {
7871
8218
  positiveIntOrDefault: () => positiveIntOrDefault,
7872
8219
  parseConfigValue: () => parseConfigValue,
7873
8220
  outputYaml: () => outputYaml,
8221
+ outputJsonAndExit: () => outputJsonAndExit,
7874
8222
  outputJson: () => outputJson,
7875
8223
  makeHandleError: () => makeHandleError,
7876
8224
  importanceColor: () => importanceColor,
@@ -7951,7 +8299,30 @@ function resolveEntityArg(nameOrId, type) {
7951
8299
  process.exit(1);
7952
8300
  }
7953
8301
  function outputJson(data) {
7954
- console.log(JSON.stringify(data, null, 2));
8302
+ process.stdout.write(`${JSON.stringify(data, null, 2)}
8303
+ `);
8304
+ }
8305
+ function outputJsonAndExit(data, code) {
8306
+ const payload = `${JSON.stringify(data, null, 2)}
8307
+ `;
8308
+ return new Promise((resolve4) => {
8309
+ let settled = false;
8310
+ const finish = (exitCode) => {
8311
+ if (settled)
8312
+ return;
8313
+ settled = true;
8314
+ process.stdout.off("error", onError);
8315
+ process.exit(exitCode);
8316
+ resolve4(undefined);
8317
+ };
8318
+ const onError = () => finish(1);
8319
+ process.stdout.once("error", onError);
8320
+ try {
8321
+ process.stdout.write(payload, () => finish(code));
8322
+ } catch {
8323
+ finish(1);
8324
+ }
8325
+ });
7955
8326
  }
7956
8327
  function positiveIntOrDefault(value, fallback) {
7957
8328
  const parsed = typeof value === "number" ? value : Number(value);
@@ -8146,9 +8517,9 @@ function makeHandleError(program2) {
8146
8517
  return function handleError(e) {
8147
8518
  const globalOpts = program2.opts();
8148
8519
  if (globalOpts.json || globalOpts.format === "json") {
8149
- outputJson({
8520
+ return outputJsonAndExit({
8150
8521
  error: e instanceof Error ? e.message : String(e)
8151
- });
8522
+ }, 1);
8152
8523
  } else {
8153
8524
  console.error(chalk.red(e instanceof Error ? e.message : String(e)));
8154
8525
  }
@@ -9293,6 +9664,245 @@ var init_search = __esm(() => {
9293
9664
  ]);
9294
9665
  });
9295
9666
 
9667
+ // src/lib/config.ts
9668
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync6, readdirSync, writeFileSync as writeFileSync3, unlinkSync as unlinkSync2, cpSync as cpSync2 } from "fs";
9669
+ import { homedir as homedir4 } from "os";
9670
+ import { basename, dirname as dirname4, join as join9, resolve as resolve14 } from "path";
9671
+ function isInMemoryDb2(path) {
9672
+ return path === ":memory:" || path.startsWith("file::memory:");
9673
+ }
9674
+ function homeDir() {
9675
+ return process.env["HOME"] || process.env["USERPROFILE"] || homedir4();
9676
+ }
9677
+ function deepMerge(target, source) {
9678
+ const result = { ...target };
9679
+ for (const key of Object.keys(source)) {
9680
+ const sourceVal = source[key];
9681
+ const targetVal = result[key];
9682
+ if (sourceVal !== null && typeof sourceVal === "object" && !Array.isArray(sourceVal) && targetVal !== null && typeof targetVal === "object" && !Array.isArray(targetVal)) {
9683
+ result[key] = deepMerge(targetVal, sourceVal);
9684
+ } else {
9685
+ result[key] = sourceVal;
9686
+ }
9687
+ }
9688
+ return result;
9689
+ }
9690
+ function isValidScope(value) {
9691
+ return VALID_SCOPES2.includes(value);
9692
+ }
9693
+ function isValidCategory(value) {
9694
+ return VALID_CATEGORIES2.includes(value);
9695
+ }
9696
+ function loadConfig() {
9697
+ const configPath = join9(getDataRoot(), "config.json");
9698
+ let fileConfig = {};
9699
+ if (existsSync6(configPath)) {
9700
+ try {
9701
+ const raw = readFileSync6(configPath, "utf-8");
9702
+ fileConfig = JSON.parse(raw);
9703
+ } catch {}
9704
+ }
9705
+ const merged = deepMerge(DEFAULT_CONFIG, fileConfig);
9706
+ const envScope = env.defaultScope();
9707
+ if (envScope && isValidScope(envScope)) {
9708
+ merged.default_scope = envScope;
9709
+ }
9710
+ const envCategory = env.defaultCategory();
9711
+ if (envCategory && isValidCategory(envCategory)) {
9712
+ merged.default_category = envCategory;
9713
+ }
9714
+ const envImportance = env.defaultImportance();
9715
+ if (envImportance) {
9716
+ const parsed = parseInt(envImportance, 10);
9717
+ if (!Number.isNaN(parsed) && parsed >= 1 && parsed <= 10) {
9718
+ merged.default_importance = parsed;
9719
+ }
9720
+ }
9721
+ return merged;
9722
+ }
9723
+ function findFileWalkingUp(filename) {
9724
+ let dir = process.cwd();
9725
+ const legacyHomeMementosDb = resolve14(homeDir(), ".mementos", "mementos.db");
9726
+ while (true) {
9727
+ const candidate = join9(dir, filename);
9728
+ if (existsSync6(candidate) && resolve14(candidate) !== legacyHomeMementosDb) {
9729
+ return candidate;
9730
+ }
9731
+ const parent = dirname4(dir);
9732
+ if (parent === dir) {
9733
+ return null;
9734
+ }
9735
+ dir = parent;
9736
+ }
9737
+ }
9738
+ function findGitRoot2() {
9739
+ let dir = process.cwd();
9740
+ while (true) {
9741
+ if (existsSync6(join9(dir, ".git"))) {
9742
+ return dir;
9743
+ }
9744
+ const parent = dirname4(dir);
9745
+ if (parent === dir) {
9746
+ return null;
9747
+ }
9748
+ dir = parent;
9749
+ }
9750
+ }
9751
+ function profilesDir() {
9752
+ return join9(getDataRoot(), "profiles");
9753
+ }
9754
+ function globalConfigPath() {
9755
+ return join9(getDataRoot(), "config.json");
9756
+ }
9757
+ function readGlobalConfig() {
9758
+ const p = globalConfigPath();
9759
+ if (!existsSync6(p))
9760
+ return {};
9761
+ try {
9762
+ return JSON.parse(readFileSync6(p, "utf-8"));
9763
+ } catch {
9764
+ return {};
9765
+ }
9766
+ }
9767
+ function readGlobalConfigForWrite() {
9768
+ const p = globalConfigPath();
9769
+ if (!existsSync6(p))
9770
+ return {};
9771
+ try {
9772
+ const data = JSON.parse(readFileSync6(p, "utf-8"));
9773
+ if (data === null || typeof data !== "object" || Array.isArray(data)) {
9774
+ throw new Error("expected a JSON object");
9775
+ }
9776
+ return data;
9777
+ } catch (error) {
9778
+ const detail = error instanceof Error ? error.message : String(error);
9779
+ throw new Error(`Cannot update global config at ${p}: ${detail}`);
9780
+ }
9781
+ }
9782
+ function writeGlobalConfig(data) {
9783
+ const p = globalConfigPath();
9784
+ ensureDir2(dirname4(p));
9785
+ writeFileSync3(p, JSON.stringify(data, null, 2), "utf-8");
9786
+ }
9787
+ function getActiveProfile() {
9788
+ const envProfile = process.env["MEMENTOS_PROFILE"];
9789
+ if (envProfile)
9790
+ return envProfile.trim();
9791
+ const cfg = readGlobalConfig();
9792
+ return cfg["active_profile"] || null;
9793
+ }
9794
+ function setActiveProfile(name) {
9795
+ const cfg = readGlobalConfigForWrite();
9796
+ if (name === null) {
9797
+ delete cfg["active_profile"];
9798
+ } else {
9799
+ cfg["active_profile"] = name;
9800
+ }
9801
+ writeGlobalConfig(cfg);
9802
+ }
9803
+ function listProfiles() {
9804
+ const dir = profilesDir();
9805
+ if (!existsSync6(dir))
9806
+ return [];
9807
+ return readdirSync(dir).filter((f) => f.endsWith(".db")).map((f) => basename(f, ".db")).sort();
9808
+ }
9809
+ function deleteProfile(name) {
9810
+ const dbPath = join9(profilesDir(), `${name}.db`);
9811
+ if (!existsSync6(dbPath))
9812
+ return false;
9813
+ unlinkSync2(dbPath);
9814
+ if (getActiveProfile() === name)
9815
+ setActiveProfile(null);
9816
+ return true;
9817
+ }
9818
+ function getDbPath2() {
9819
+ const _home = homeDir();
9820
+ const _newDir = getDataRoot();
9821
+ const _oldDir = join9(_home, ".mementos");
9822
+ if (selectsMementosLocalStore() && !existsSync6(_newDir) && existsSync6(_oldDir)) {
9823
+ mkdirSync3(join9(_home, ".hasna"), { recursive: true });
9824
+ cpSync2(_oldDir, _newDir, { recursive: true });
9825
+ }
9826
+ const envDbPath = process.env["HASNA_MEMENTOS_DB_PATH"] ?? process.env["MEMENTOS_DB_PATH"];
9827
+ if (envDbPath) {
9828
+ if (isInMemoryDb2(envDbPath)) {
9829
+ return envDbPath;
9830
+ }
9831
+ const resolved = resolve14(envDbPath);
9832
+ ensureDir2(dirname4(resolved));
9833
+ return resolved;
9834
+ }
9835
+ const profile = getActiveProfile();
9836
+ if (profile) {
9837
+ const profilePath = join9(profilesDir(), `${profile}.db`);
9838
+ ensureDir2(dirname4(profilePath));
9839
+ return profilePath;
9840
+ }
9841
+ const dbScope = process.env["MEMENTOS_DB_SCOPE"];
9842
+ if (dbScope === "project") {
9843
+ const gitRoot = findGitRoot2();
9844
+ if (gitRoot) {
9845
+ const dbPath = join9(gitRoot, ".mementos", "mementos.db");
9846
+ ensureDir2(dirname4(dbPath));
9847
+ return dbPath;
9848
+ }
9849
+ }
9850
+ const found = findFileWalkingUp(join9(".mementos", "mementos.db"));
9851
+ if (found) {
9852
+ return found;
9853
+ }
9854
+ const fallback = join9(getDataRoot(), "mementos.db");
9855
+ ensureDir2(dirname4(fallback));
9856
+ return fallback;
9857
+ }
9858
+ function ensureDir2(dir) {
9859
+ if (!existsSync6(dir)) {
9860
+ mkdirSync3(dir, { recursive: true });
9861
+ }
9862
+ }
9863
+ var DEFAULT_CONFIG, VALID_SCOPES2, VALID_CATEGORIES2;
9864
+ var init_config = __esm(() => {
9865
+ init_paths();
9866
+ init_env();
9867
+ init_local_opt_in();
9868
+ DEFAULT_CONFIG = {
9869
+ default_scope: "private",
9870
+ default_category: "knowledge",
9871
+ default_importance: 5,
9872
+ max_entries: 1000,
9873
+ max_entries_per_scope: {
9874
+ global: 500,
9875
+ shared: 300,
9876
+ private: 200,
9877
+ working: 100
9878
+ },
9879
+ injection: {
9880
+ max_tokens: 500,
9881
+ min_importance: 5,
9882
+ categories: ["preference", "fact"],
9883
+ refresh_interval: 5
9884
+ },
9885
+ extraction: {
9886
+ enabled: true,
9887
+ min_confidence: 0.5
9888
+ },
9889
+ sync_agents: ["claude", "codex", "gemini"],
9890
+ auto_cleanup: {
9891
+ enabled: true,
9892
+ expired_check_interval: 3600,
9893
+ unused_archive_days: 7,
9894
+ stale_deprioritize_days: 14
9895
+ }
9896
+ };
9897
+ VALID_SCOPES2 = ["global", "shared", "private", "working"];
9898
+ VALID_CATEGORIES2 = [
9899
+ "preference",
9900
+ "fact",
9901
+ "knowledge",
9902
+ "history"
9903
+ ];
9904
+ });
9905
+
9296
9906
  // src/db/relations.ts
9297
9907
  function parseRelationRow(row) {
9298
9908
  return {
@@ -10958,17 +11568,80 @@ var exports_profile_synthesizer = {};
10958
11568
  __export(exports_profile_synthesizer, {
10959
11569
  synthesizeProfile: () => synthesizeProfile,
10960
11570
  markProfileStale: () => markProfileStale,
10961
- getProfileKey: () => getProfileKey
11571
+ getProfileKey: () => getProfileKey,
11572
+ ProfileSynthesisError: () => ProfileSynthesisError,
11573
+ ProfileScopeError: () => ProfileScopeError,
11574
+ PROFILE_SYNTHESIS_CONTRACT: () => PROFILE_SYNTHESIS_CONTRACT
10962
11575
  });
11576
+ function resolveProfileTarget(options) {
11577
+ const scope = options.scope ?? (options.project_id ? "project" : options.agent_id ? "agent" : "global");
11578
+ if (scope === "project") {
11579
+ if (!options.project_id)
11580
+ throw new ProfileScopeError("project scope requires project_id");
11581
+ return { scope, id: options.project_id, memory_scope: "shared", project_id: options.project_id };
11582
+ }
11583
+ if (scope === "agent") {
11584
+ if (!options.agent_id)
11585
+ throw new ProfileScopeError("agent scope requires agent_id");
11586
+ return { scope, id: options.agent_id, memory_scope: "private", agent_id: options.agent_id };
11587
+ }
11588
+ return { scope: "global", id: "global", memory_scope: "global" };
11589
+ }
10963
11590
  function getProfileKey(scope, id) {
10964
11591
  return `_profile_${scope}_${id}`;
10965
11592
  }
11593
+ function listProfileMemories(category, target) {
11594
+ if (target.scope !== "global") {
11595
+ return listMemories({
11596
+ category,
11597
+ scope: target.memory_scope,
11598
+ project_id: target.project_id,
11599
+ agent_id: target.agent_id,
11600
+ machine_id: null,
11601
+ status: "active",
11602
+ limit: 30
11603
+ });
11604
+ }
11605
+ return [
11606
+ ...listMemories({ category, scope: "global", machine_id: null, status: "active", limit: 30 }),
11607
+ ...listMemories({ category, scope: "shared", machine_id: null, status: "active", limit: 1000 })
11608
+ ].filter((memory) => memory.agent_id === null && memory.project_id === null).slice(0, 30);
11609
+ }
10966
11610
  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);
11611
+ if (!isServerContext() && isApiMode()) {
11612
+ const operation = "POST /profile/synthesize";
11613
+ const { data } = apiJson("POST", "/profile/synthesize", {
11614
+ project_id: options.project_id,
11615
+ agent_id: options.agent_id,
11616
+ scope: options.scope,
11617
+ force_refresh: options.force_refresh === true
11618
+ });
11619
+ const response = expectObject(data, operation);
11620
+ if (response["contract"] !== PROFILE_SYNTHESIS_CONTRACT) {
11621
+ throw new MementosApiProtocolError(operation, `expected contract '${PROFILE_SYNTHESIS_CONTRACT}'`);
11622
+ }
11623
+ if (!("profile" in response)) {
11624
+ throw new MementosApiProtocolError(operation, "missing 'profile'");
11625
+ }
11626
+ if (response["profile"] === null) {
11627
+ if (response["reason"] !== "no_memories") {
11628
+ throw new MementosApiProtocolError(operation, "null profile requires reason 'no_memories'");
11629
+ }
11630
+ return null;
11631
+ }
11632
+ if (typeof response["profile"] !== "string" || response["profile"].length === 0) {
11633
+ throw new MementosApiProtocolError(operation, "expected 'profile' to be a non-empty string or null");
11634
+ }
11635
+ return {
11636
+ profile: response["profile"],
11637
+ memory_count: expectNonNegativeInteger(response, "memory_count", operation),
11638
+ from_cache: expectBoolean(response, "from_cache", operation)
11639
+ };
11640
+ }
11641
+ const target = resolveProfileTarget(options);
11642
+ const profileKey = getProfileKey(target.scope, target.id);
10970
11643
  if (!options.force_refresh) {
10971
- const cached = getMemoryByKey(profileKey, "shared", undefined, options.project_id);
11644
+ const cached = getMemoryByKey(profileKey, "shared", target.agent_id, target.project_id);
10972
11645
  if (cached) {
10973
11646
  const age = Date.now() - new Date(cached.updated_at).getTime();
10974
11647
  const maxAge = 24 * 60 * 60 * 1000;
@@ -10978,20 +11651,8 @@ async function synthesizeProfile(options) {
10978
11651
  }
10979
11652
  }
10980
11653
  }
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
- });
11654
+ const prefMemories = listProfileMemories("preference", target);
11655
+ const factMemories = listProfileMemories("fact", target);
10995
11656
  const allMemories = [...prefMemories, ...factMemories];
10996
11657
  if (allMemories.length === 0)
10997
11658
  return null;
@@ -11001,7 +11662,7 @@ async function synthesizeProfile(options) {
11001
11662
  `);
11002
11663
  const fallbackProfile = `## Profile
11003
11664
  ${lines}`;
11004
- saveProfile(profileKey, fallbackProfile, allMemories.length, options);
11665
+ saveProfile(profileKey, fallbackProfile, allMemories.length, target);
11005
11666
  return { profile: fallbackProfile, memory_count: allMemories.length, from_cache: false };
11006
11667
  }
11007
11668
  try {
@@ -11023,41 +11684,63 @@ ${lines}`;
11023
11684
  ${memoryList}` }]
11024
11685
  })
11025
11686
  });
11026
- if (!response.ok)
11687
+ if (!response.ok) {
11688
+ if (options.fail_on_provider_error)
11689
+ throw new ProfileSynthesisError;
11027
11690
  return null;
11691
+ }
11028
11692
  const data = await response.json();
11029
11693
  const profile = data.content?.[0]?.text?.trim();
11030
- if (!profile)
11694
+ if (!profile) {
11695
+ if (options.fail_on_provider_error)
11696
+ throw new ProfileSynthesisError;
11031
11697
  return null;
11032
- saveProfile(profileKey, profile, allMemories.length, options);
11698
+ }
11699
+ saveProfile(profileKey, profile, allMemories.length, target);
11033
11700
  return { profile, memory_count: allMemories.length, from_cache: false };
11034
- } catch {
11701
+ } catch (error) {
11702
+ if (options.fail_on_provider_error) {
11703
+ if (error instanceof ProfileSynthesisError)
11704
+ throw error;
11705
+ throw new ProfileSynthesisError;
11706
+ }
11035
11707
  return null;
11036
11708
  }
11037
11709
  }
11038
- function saveProfile(key, value, memoryCount, options) {
11710
+ function saveProfile(key, value, memoryCount, target) {
11039
11711
  try {
11040
11712
  createMemory({
11041
11713
  key,
11042
11714
  value,
11043
- category: "fact",
11715
+ category: "resource",
11044
11716
  scope: "shared",
11045
11717
  importance: 10,
11046
11718
  source: "auto",
11047
11719
  tags: ["profile", "synthesized"],
11048
11720
  when_to_use: "When needing to understand this agent's or project's preferences, style, and conventions",
11049
11721
  metadata: { memory_count: memoryCount, synthesized_at: new Date().toISOString(), stale: false },
11050
- agent_id: options.agent_id,
11051
- project_id: options.project_id
11722
+ agent_id: target.agent_id,
11723
+ project_id: target.project_id
11052
11724
  });
11053
11725
  } catch {}
11054
11726
  }
11055
- function markProfileStale(projectId, _agentId) {
11727
+ function markProfileStale(projectId, agentId) {
11728
+ const targets = [{ scope: "global", id: "global", memory_scope: "global" }];
11729
+ if (projectId)
11730
+ targets.push({ scope: "project", id: projectId, memory_scope: "shared", project_id: projectId });
11731
+ if (agentId)
11732
+ targets.push({ scope: "agent", id: agentId, memory_scope: "private", agent_id: agentId });
11056
11733
  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 || ""]);
11734
+ const db = !isServerContext() && isApiMode() ? undefined : (init_database(), __toCommonJS(exports_database)).getDatabase();
11735
+ for (const target of targets) {
11736
+ const cached = getMemoryByKey(getProfileKey(target.scope, target.id), "shared", target.agent_id, target.project_id, undefined, db);
11737
+ if (!cached)
11738
+ continue;
11739
+ updateMemory(cached.id, {
11740
+ metadata: { ...cached.metadata ?? {}, stale: true },
11741
+ version: cached.version
11742
+ }, db);
11743
+ }
11061
11744
  } catch {}
11062
11745
  }
11063
11746
  var PROFILE_PROMPT = `You synthesize a coherent agent/project profile from individual preference and fact memories.
@@ -11070,9 +11753,26 @@ Output a concise profile (200-300 words max) organized by:
11070
11753
  - **Key Facts**: Architecture decisions, constraints, team conventions
11071
11754
 
11072
11755
  Only include sections that have relevant data. Be specific and actionable.
11073
- Output in markdown format.`;
11756
+ Output in markdown format.`, PROFILE_SYNTHESIS_CONTRACT = "mementos.profile.synthesize.v2", ProfileScopeError, ProfileSynthesisError;
11074
11757
  var init_profile_synthesizer = __esm(() => {
11075
11758
  init_memories();
11759
+ init_api_mode();
11760
+ init_api_response_contract();
11761
+ init_storage();
11762
+ ProfileScopeError = class ProfileScopeError extends Error {
11763
+ code = "MEMENTOS_PROFILE_SCOPE";
11764
+ constructor(message) {
11765
+ super(message);
11766
+ this.name = "ProfileScopeError";
11767
+ }
11768
+ };
11769
+ ProfileSynthesisError = class ProfileSynthesisError extends Error {
11770
+ code = "MEMENTOS_PROFILE_SYNTHESIS";
11771
+ constructor() {
11772
+ super("Profile synthesis provider failed");
11773
+ this.name = "ProfileSynthesisError";
11774
+ }
11775
+ };
11076
11776
  });
11077
11777
 
11078
11778
  // src/lib/contradiction.ts
@@ -11356,6 +12056,22 @@ var init_built_in_hooks = __esm(() => {
11356
12056
  } catch {}
11357
12057
  }
11358
12058
  });
12059
+ hookRegistry.register({
12060
+ type: "PostMemoryUpdate",
12061
+ blocking: false,
12062
+ builtin: true,
12063
+ priority: 65,
12064
+ description: "Mark synthesized profile as stale when a preference or fact memory is updated",
12065
+ handler: async (ctx) => {
12066
+ const category = ctx.memory?.category;
12067
+ if (category !== "preference" && category !== "fact")
12068
+ return;
12069
+ try {
12070
+ const { markProfileStale: markProfileStale2 } = await Promise.resolve().then(() => (init_profile_synthesizer(), exports_profile_synthesizer));
12071
+ markProfileStale2(ctx.projectId, ctx.agentId);
12072
+ } catch {}
12073
+ }
12074
+ });
11359
12075
  hookRegistry.register({
11360
12076
  type: "PostMemorySave",
11361
12077
  blocking: false,
@@ -12213,10 +12929,20 @@ __export(exports_synthesis2, {
12213
12929
  getSynthesisStatus: () => getSynthesisStatus
12214
12930
  });
12215
12931
  async function runSynthesis(options = {}) {
12216
- const d = options.db || getDatabase();
12217
12932
  const projectId = options.projectId ?? null;
12218
12933
  const agentId = options.agentId ?? null;
12219
12934
  const dryRun = options.dryRun ?? false;
12935
+ if (!options.db && isApiMode()) {
12936
+ const { data } = apiJson("POST", "/synthesis/run", {
12937
+ project_id: projectId,
12938
+ agent_id: agentId,
12939
+ dry_run: dryRun,
12940
+ max_proposals: options.maxProposals,
12941
+ provider: options.provider
12942
+ });
12943
+ return data;
12944
+ }
12945
+ const d = options.db || getDatabase();
12220
12946
  const run = createSynthesisRun({
12221
12947
  triggered_by: "manual",
12222
12948
  project_id: projectId,
@@ -12305,6 +13031,10 @@ async function runSynthesis(options = {}) {
12305
13031
  }
12306
13032
  }
12307
13033
  async function rollbackSynthesis(runId, db) {
13034
+ if (!db && isApiMode()) {
13035
+ const { data } = apiJson("POST", `/synthesis/rollback/${encodeURIComponent(runId)}`);
13036
+ return data;
13037
+ }
12308
13038
  const d = db || getDatabase();
12309
13039
  const result = await rollbackRun(runId, d);
12310
13040
  if (result.errors.length === 0) {
@@ -12353,8 +13083,37 @@ __export(exports_session_jobs, {
12353
13083
  getSessionJob: () => getSessionJob,
12354
13084
  getNextPendingJob: () => getNextPendingJob,
12355
13085
  createSessionJob: () => createSessionJob,
12356
- claimSessionJob: () => claimSessionJob
13086
+ claimSessionJob: () => claimSessionJob,
13087
+ SESSION_JOBS_PAGE_CONTRACT: () => SESSION_JOBS_PAGE_CONTRACT,
13088
+ SESSION_INGEST_CONTRACT: () => SESSION_INGEST_CONTRACT
12357
13089
  });
13090
+ function parseHostedSessionJob(value, operation) {
13091
+ const row = expectObject(value, operation);
13092
+ const source = expectString(row, "source", operation);
13093
+ const status = expectString(row, "status", operation);
13094
+ if (!SESSION_JOB_SOURCES.has(source)) {
13095
+ throw new MementosApiProtocolError(operation, "unsupported 'source'");
13096
+ }
13097
+ if (!SESSION_JOB_STATUSES.has(status)) {
13098
+ throw new MementosApiProtocolError(operation, "unsupported 'status'");
13099
+ }
13100
+ return {
13101
+ id: expectString(row, "id", operation),
13102
+ session_id: expectString(row, "session_id", operation),
13103
+ agent_id: expectNullableString(row, "agent_id", operation),
13104
+ project_id: expectNullableString(row, "project_id", operation),
13105
+ source,
13106
+ status,
13107
+ transcript: expectString(row, "transcript", operation, { allowEmpty: true }),
13108
+ chunk_count: expectNonNegativeInteger(row, "chunk_count", operation),
13109
+ memories_extracted: expectNonNegativeInteger(row, "memories_extracted", operation),
13110
+ error: expectNullableString(row, "error", operation),
13111
+ metadata: expectRecord(row, "metadata", operation),
13112
+ created_at: expectString(row, "created_at", operation),
13113
+ started_at: expectNullableString(row, "started_at", operation),
13114
+ completed_at: expectNullableString(row, "completed_at", operation)
13115
+ };
13116
+ }
12358
13117
  function parseJobRow(row) {
12359
13118
  return {
12360
13119
  id: row["id"],
@@ -12374,6 +13133,28 @@ function parseJobRow(row) {
12374
13133
  };
12375
13134
  }
12376
13135
  function createSessionJob(input, db) {
13136
+ if (!db && isApiMode()) {
13137
+ const operation = "POST /sessions/ingest";
13138
+ const { data } = apiJson("POST", "/sessions/ingest", {
13139
+ session_id: input.session_id,
13140
+ transcript: input.transcript,
13141
+ source: input.source ?? "manual",
13142
+ agent_id: input.agent_id,
13143
+ project_id: input.project_id,
13144
+ metadata: input.metadata ?? {}
13145
+ });
13146
+ const response = expectObject(data, operation);
13147
+ if (response["contract"] !== SESSION_INGEST_CONTRACT) {
13148
+ throw new MementosApiProtocolError(operation, `expected contract '${SESSION_INGEST_CONTRACT}'`);
13149
+ }
13150
+ const jobId = expectString(response, "job_id", operation);
13151
+ const jobObject = expectObject(response["job"], operation);
13152
+ const job = parseHostedSessionJob({ ...jobObject, transcript: input.transcript }, `${operation} job`);
13153
+ if (job.id !== jobId || job.session_id !== input.session_id) {
13154
+ throw new MementosApiProtocolError(operation, "job receipt identity does not match the request");
13155
+ }
13156
+ return job;
13157
+ }
12377
13158
  const d = db || getDatabase();
12378
13159
  const id = uuid();
12379
13160
  const timestamp2 = now();
@@ -12395,10 +13176,11 @@ function createSessionJob(input, db) {
12395
13176
  }
12396
13177
  function getSessionJob(id, db) {
12397
13178
  if (!db && isApiMode()) {
13179
+ const operation = `GET /sessions/jobs/${encodeURIComponent(id)}`;
12398
13180
  const { status, data } = apiJson("GET", `/sessions/jobs/${encodeURIComponent(id)}`, undefined, { allow404: true });
12399
- if (status === 404 || !data)
13181
+ if (status === 404)
12400
13182
  return null;
12401
- return data;
13183
+ return parseHostedSessionJob(data, operation);
12402
13184
  }
12403
13185
  const d = db || getDatabase();
12404
13186
  const row = d.query("SELECT * FROM session_memory_jobs WHERE id = ?").get(id);
@@ -12412,10 +13194,58 @@ function listSessionJobs(filter, db) {
12412
13194
  agent_id: filter?.agent_id,
12413
13195
  project_id: filter?.project_id,
12414
13196
  status: filter?.status,
12415
- limit: filter?.limit
13197
+ session_id: filter?.session_id,
13198
+ limit: filter?.limit,
13199
+ offset: filter?.offset
12416
13200
  });
13201
+ const operation = "GET /sessions/jobs";
12417
13202
  const { data } = apiJson("GET", `/sessions/jobs${q}`);
12418
- return data?.jobs ?? [];
13203
+ const response = expectObject(data, operation);
13204
+ if (response["contract"] !== SESSION_JOBS_PAGE_CONTRACT) {
13205
+ throw new MementosApiProtocolError(operation, `expected contract '${SESSION_JOBS_PAGE_CONTRACT}'`);
13206
+ }
13207
+ const jobs = expectArray(response["jobs"], operation, "jobs").map((job, index) => parseHostedSessionJob(job, `${operation} item ${index}`));
13208
+ const count = expectNonNegativeInteger(response, "count", operation);
13209
+ const responseLimit = expectNonNegativeInteger(response, "limit", operation);
13210
+ const responseOffset = expectNonNegativeInteger(response, "offset", operation);
13211
+ const hasMore = expectBoolean(response, "has_more", operation);
13212
+ const nextOffset = response["next_offset"];
13213
+ if (responseLimit < 1) {
13214
+ throw new MementosApiProtocolError(operation, "expected 'limit' to be positive");
13215
+ }
13216
+ if (count !== jobs.length || count > responseLimit) {
13217
+ throw new MementosApiProtocolError(operation, "page count is inconsistent with jobs/limit");
13218
+ }
13219
+ if (nextOffset !== null && (!Number.isSafeInteger(nextOffset) || nextOffset < 0)) {
13220
+ throw new MementosApiProtocolError(operation, "expected 'next_offset' to be a non-negative safe integer or null");
13221
+ }
13222
+ if (hasMore && nextOffset !== responseOffset + jobs.length) {
13223
+ throw new MementosApiProtocolError(operation, "'has_more' requires the exact next offset");
13224
+ }
13225
+ if (!hasMore && nextOffset !== null) {
13226
+ throw new MementosApiProtocolError(operation, "terminal page must set 'next_offset' to null");
13227
+ }
13228
+ if (filter?.limit !== undefined && responseLimit !== filter.limit) {
13229
+ throw new MementosApiProtocolError(operation, "server did not preserve requested 'limit'");
13230
+ }
13231
+ if (filter?.offset !== undefined && responseOffset !== filter.offset) {
13232
+ throw new MementosApiProtocolError(operation, "server did not preserve requested 'offset'");
13233
+ }
13234
+ for (const job of jobs) {
13235
+ if (filter?.agent_id !== undefined && job.agent_id !== filter.agent_id) {
13236
+ throw new MementosApiProtocolError(operation, "server did not preserve requested 'agent_id'");
13237
+ }
13238
+ if (filter?.project_id !== undefined && job.project_id !== filter.project_id) {
13239
+ throw new MementosApiProtocolError(operation, "server did not preserve requested 'project_id'");
13240
+ }
13241
+ if (filter?.session_id !== undefined && job.session_id !== filter.session_id) {
13242
+ throw new MementosApiProtocolError(operation, "server did not preserve requested 'session_id'");
13243
+ }
13244
+ if (filter?.status !== undefined && job.status !== filter.status) {
13245
+ throw new MementosApiProtocolError(operation, "server did not preserve requested 'status'");
13246
+ }
13247
+ }
13248
+ return jobs;
12419
13249
  }
12420
13250
  const d = db || getDatabase();
12421
13251
  const conditions = [];
@@ -12495,9 +13325,13 @@ function recoverStaleProcessingJobs(maxAgeMs, db) {
12495
13325
  const result = d.run("UPDATE session_memory_jobs SET status = 'pending', started_at = NULL WHERE status = 'processing' AND started_at < ?", [cutoff]);
12496
13326
  return result.changes;
12497
13327
  }
13328
+ var SESSION_JOBS_PAGE_CONTRACT = "mementos.sessions.jobs.v2", SESSION_INGEST_CONTRACT = "mementos.sessions.ingest.v2", SESSION_JOB_SOURCES, SESSION_JOB_STATUSES;
12498
13329
  var init_session_jobs = __esm(() => {
12499
13330
  init_database();
12500
13331
  init_api_mode();
13332
+ init_api_response_contract();
13333
+ SESSION_JOB_SOURCES = new Set(["claude-code", "codex", "manual", "open-sessions"]);
13334
+ SESSION_JOB_STATUSES = new Set(["pending", "processing", "completed", "failed"]);
12501
13335
  });
12502
13336
 
12503
13337
  // src/db/tool-events.ts
@@ -13120,12 +13954,25 @@ __export(exports_session_queue, {
13120
13954
  enqueueSessionJob: () => enqueueSessionJob
13121
13955
  });
13122
13956
  function enqueueSessionJob(jobId) {
13957
+ if (!isServerContext() && isApiMode())
13958
+ return;
13123
13959
  _pendingQueue.add(jobId);
13124
13960
  if (!_isProcessing) {
13125
13961
  _processNext();
13126
13962
  }
13127
13963
  }
13128
13964
  function getSessionQueueStats() {
13965
+ if (!isServerContext() && isApiMode()) {
13966
+ const operation = "GET /sessions/queue/stats";
13967
+ const { data } = apiJson("GET", "/sessions/queue/stats");
13968
+ const response = expectObject(data, operation);
13969
+ return {
13970
+ pending: expectNonNegativeInteger(response, "pending", operation),
13971
+ processing: expectNonNegativeInteger(response, "processing", operation),
13972
+ completed: expectNonNegativeInteger(response, "completed", operation),
13973
+ failed: expectNonNegativeInteger(response, "failed", operation)
13974
+ };
13975
+ }
13129
13976
  try {
13130
13977
  const db = getDatabase();
13131
13978
  const rows = db.query("SELECT status, COUNT(*) as count FROM session_memory_jobs GROUP BY status").all();
@@ -13146,7 +13993,9 @@ function getSessionQueueStats() {
13146
13993
  stats.failed = row.count;
13147
13994
  }
13148
13995
  return stats;
13149
- } catch {
13996
+ } catch (error) {
13997
+ if (isServerContext())
13998
+ throw error;
13150
13999
  return {
13151
14000
  pending: _pendingQueue.size,
13152
14001
  processing: _isProcessing ? 1 : 0,
@@ -13158,6 +14007,8 @@ function getSessionQueueStats() {
13158
14007
  function startSessionQueueWorker() {
13159
14008
  if (_workerStarted)
13160
14009
  return;
14010
+ if (!isServerContext() && isApiMode())
14011
+ return;
13161
14012
  _workerStarted = true;
13162
14013
  setInterval(() => {
13163
14014
  try {
@@ -13199,6 +14050,9 @@ var init_session_queue = __esm(() => {
13199
14050
  init_database();
13200
14051
  init_session_jobs();
13201
14052
  init_session_processor();
14053
+ init_api_mode();
14054
+ init_api_response_contract();
14055
+ init_storage();
13202
14056
  _pendingQueue = new Set;
13203
14057
  });
13204
14058
 
@@ -13207,6 +14061,8 @@ var exports_session_registry = {};
13207
14061
  __export(exports_session_registry, {
13208
14062
  updateSessionAgent: () => updateSessionAgent,
13209
14063
  unregisterSession: () => unregisterSession,
14064
+ sessionRegistryUsesLocalStore: () => sessionRegistryUsesLocalStore,
14065
+ sessionRegistryPath: () => sessionRegistryPath,
13210
14066
  registerSession: () => registerSession,
13211
14067
  listSessions: () => listSessions,
13212
14068
  heartbeatSession: () => heartbeatSession,
@@ -13214,17 +14070,36 @@ __export(exports_session_registry, {
13214
14070
  getSessionByAgent: () => getSessionByAgent,
13215
14071
  getSession: () => getSession,
13216
14072
  closeRegistry: () => closeRegistry,
13217
- cleanStaleSessions: () => cleanStaleSessions
14073
+ cleanStaleSessions: () => cleanStaleSessions,
14074
+ __resetProcessLocalRegistry: () => __resetProcessLocalRegistry,
14075
+ SESSION_REGISTRY_FILE: () => SESSION_REGISTRY_FILE
13218
14076
  });
13219
14077
  import { existsSync as existsSync10, mkdirSync as mkdirSync5 } from "fs";
13220
14078
  import { dirname as dirname6, join as join14 } from "path";
14079
+ function sessionRegistryUsesLocalStore(env2 = process.env) {
14080
+ return isServerContext() || selectsMementosLocalStore(env2);
14081
+ }
14082
+ function sessionRegistryPath() {
14083
+ const store = getDbPath2();
14084
+ if (store === ":memory:")
14085
+ return ":memory:";
14086
+ return join14(dirname6(store), SESSION_REGISTRY_FILE);
14087
+ }
13221
14088
  function getDb() {
13222
- if (_db2)
14089
+ const path = sessionRegistryPath();
14090
+ if (_db2 && _dbPath2 === path)
13223
14091
  return _db2;
13224
- const dir = dirname6(DB_PATH);
13225
- if (!existsSync10(dir))
13226
- mkdirSync5(dir, { recursive: true });
13227
- _db2 = new SqliteAdapter(DB_PATH);
14092
+ if (_db2) {
14093
+ _db2.close();
14094
+ _db2 = null;
14095
+ }
14096
+ if (path !== ":memory:") {
14097
+ const dir = dirname6(path);
14098
+ if (!existsSync10(dir))
14099
+ mkdirSync5(dir, { recursive: true });
14100
+ }
14101
+ _db2 = new SqliteAdapter(path);
14102
+ _dbPath2 = path;
13228
14103
  _db2.run("PRAGMA journal_mode = WAL");
13229
14104
  _db2.run("PRAGMA busy_timeout = 3000");
13230
14105
  _db2.exec(`
@@ -13281,12 +14156,45 @@ function isProcessAlive(pid) {
13281
14156
  return false;
13282
14157
  }
13283
14158
  }
14159
+ function matchesFilter(s, filter) {
14160
+ if (!filter)
14161
+ return true;
14162
+ if (filter.project_name && s.project_name !== filter.project_name)
14163
+ return false;
14164
+ if (filter.git_root && s.git_root !== filter.git_root)
14165
+ return false;
14166
+ if (filter.mcp_server && s.mcp_server !== filter.mcp_server)
14167
+ return false;
14168
+ if (filter.agent_name && s.agent_name !== filter.agent_name)
14169
+ return false;
14170
+ if (filter.exclude_pid && s.pid === filter.exclude_pid)
14171
+ return false;
14172
+ return true;
14173
+ }
13284
14174
  function registerSession(opts) {
13285
- const db = getDb();
13286
14175
  const pid = process.pid;
13287
14176
  const cwd = opts.cwd || process.cwd();
13288
- const id = generateId();
13289
14177
  const timestamp2 = now3();
14178
+ if (!sessionRegistryUsesLocalStore()) {
14179
+ const existing2 = [..._memory.values()].find((s) => s.pid === pid && s.mcp_server === opts.mcp_server);
14180
+ const record = {
14181
+ id: existing2?.id ?? generateId(),
14182
+ pid,
14183
+ cwd,
14184
+ git_root: opts.git_root || null,
14185
+ agent_name: opts.agent_name || null,
14186
+ project_name: opts.project_name || null,
14187
+ tty: opts.tty || null,
14188
+ mcp_server: opts.mcp_server,
14189
+ metadata: { ...opts.metadata || {} },
14190
+ registered_at: existing2?.registered_at ?? timestamp2,
14191
+ last_seen_at: timestamp2
14192
+ };
14193
+ _memory.set(record.id, record);
14194
+ return { ...record, metadata: { ...record.metadata } };
14195
+ }
14196
+ const db = getDb();
14197
+ const id = generateId();
13290
14198
  const existing = db.query("SELECT id FROM sessions WHERE pid = ? AND mcp_server = ?").get(pid, opts.mcp_server);
13291
14199
  if (existing) {
13292
14200
  db.run(`UPDATE sessions SET agent_name = ?, project_name = ?, cwd = ?,
@@ -13319,19 +14227,41 @@ function registerSession(opts) {
13319
14227
  return getSession(id);
13320
14228
  }
13321
14229
  function heartbeatSession(id) {
14230
+ if (!sessionRegistryUsesLocalStore()) {
14231
+ const s = _memory.get(id);
14232
+ if (s)
14233
+ s.last_seen_at = now3();
14234
+ return;
14235
+ }
13322
14236
  const db = getDb();
13323
14237
  db.run("UPDATE sessions SET last_seen_at = ? WHERE id = ?", [now3(), id]);
13324
14238
  }
13325
14239
  function unregisterSession(id) {
14240
+ if (!sessionRegistryUsesLocalStore()) {
14241
+ _memory.delete(id);
14242
+ return;
14243
+ }
13326
14244
  const db = getDb();
13327
14245
  db.run("DELETE FROM sessions WHERE id = ?", [id]);
13328
14246
  }
13329
14247
  function getSession(id) {
14248
+ if (!sessionRegistryUsesLocalStore()) {
14249
+ const s = _memory.get(id);
14250
+ return s ? { ...s, metadata: { ...s.metadata } } : null;
14251
+ }
13330
14252
  const db = getDb();
13331
14253
  const row = db.query("SELECT * FROM sessions WHERE id = ?").get(id);
13332
14254
  return row ? parseRow2(row) : null;
13333
14255
  }
13334
14256
  function listSessions(filter) {
14257
+ if (!sessionRegistryUsesLocalStore()) {
14258
+ return [..._memory.values()].filter((s) => matchesFilter(s, filter)).filter((s) => {
14259
+ if (isProcessAlive(s.pid))
14260
+ return true;
14261
+ _memory.delete(s.id);
14262
+ return false;
14263
+ }).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 } }));
14264
+ }
13335
14265
  const db = getDb();
13336
14266
  const conditions = [];
13337
14267
  const params = [];
@@ -13372,6 +14302,16 @@ function getSessionsByProject(projectName) {
13372
14302
  return listSessions({ project_name: projectName });
13373
14303
  }
13374
14304
  function cleanStaleSessions() {
14305
+ if (!sessionRegistryUsesLocalStore()) {
14306
+ let cleaned2 = 0;
14307
+ for (const s of [..._memory.values()]) {
14308
+ if (!isProcessAlive(s.pid)) {
14309
+ _memory.delete(s.id);
14310
+ cleaned2++;
14311
+ }
14312
+ }
14313
+ return cleaned2;
14314
+ }
13375
14315
  const db = getDb();
13376
14316
  const rows = db.query("SELECT id, pid FROM sessions").all();
13377
14317
  let cleaned = 0;
@@ -13384,20 +14324,35 @@ function cleanStaleSessions() {
13384
14324
  return cleaned;
13385
14325
  }
13386
14326
  function updateSessionAgent(mcpServer, agentName) {
13387
- const db = getDb();
13388
14327
  const pid = process.pid;
14328
+ if (!sessionRegistryUsesLocalStore()) {
14329
+ for (const s of _memory.values()) {
14330
+ if (s.pid === pid && s.mcp_server === mcpServer) {
14331
+ s.agent_name = agentName;
14332
+ s.last_seen_at = now3();
14333
+ }
14334
+ }
14335
+ return;
14336
+ }
14337
+ const db = getDb();
13389
14338
  db.run("UPDATE sessions SET agent_name = ?, last_seen_at = ? WHERE pid = ? AND mcp_server = ?", [agentName, now3(), pid, mcpServer]);
13390
14339
  }
13391
14340
  function closeRegistry() {
13392
14341
  if (_db2) {
13393
14342
  _db2.close();
13394
14343
  _db2 = null;
14344
+ _dbPath2 = null;
13395
14345
  }
13396
14346
  }
13397
- var DB_PATH, _db2 = null;
14347
+ function __resetProcessLocalRegistry() {
14348
+ _memory.clear();
14349
+ }
14350
+ var SESSION_REGISTRY_FILE = "sessions-registry.db", _db2 = null, _dbPath2 = null, _memory;
13398
14351
  var init_session_registry = __esm(() => {
13399
14352
  init_storage();
13400
- DB_PATH = join14(process.env["HOME"] || process.env["USERPROFILE"] || "~", ".open-sessions-registry.db");
14353
+ init_config();
14354
+ init_local_opt_in();
14355
+ _memory = new Map;
13401
14356
  });
13402
14357
 
13403
14358
  // src/db/pg-migrations.ts
@@ -14177,6 +15132,79 @@ var init_pg_migrations = __esm(() => {
14177
15132
  `
14178
15133
  ${postgresMementosMemoryProjectLinkSchemaSql()}
14179
15134
  INSERT INTO _migrations (id) VALUES (40) ON CONFLICT DO NOTHING;
15135
+ `,
15136
+ `
15137
+ DROP TABLE IF EXISTS pg_temp.mementos_m41_machine_preflight;
15138
+ CREATE TEMP TABLE mementos_m41_machine_preflight (
15139
+ id TEXT PRIMARY KEY,
15140
+ name TEXT NOT NULL CHECK(
15141
+ length(name) BETWEEN 1 AND 128
15142
+ AND name !~ '[[:cntrl:]]'
15143
+ ),
15144
+ hostname TEXT NOT NULL UNIQUE CHECK(
15145
+ length(hostname) BETWEEN 1 AND 253
15146
+ AND hostname = lower(regexp_replace(btrim(hostname), '\\.+$', ''))
15147
+ AND hostname !~ '[[:space:]/\\\\]'
15148
+ ),
15149
+ platform TEXT NOT NULL CHECK(
15150
+ length(platform) BETWEEN 1 AND 64
15151
+ AND platform = lower(btrim(platform))
15152
+ AND platform ~ '^[a-z0-9._-]+$'
15153
+ ),
15154
+ is_primary BOOLEAN NOT NULL,
15155
+ created_at TIMESTAMPTZ NOT NULL,
15156
+ last_seen_at TIMESTAMPTZ NOT NULL CHECK(last_seen_at >= created_at)
15157
+ ) ON COMMIT DROP;
15158
+
15159
+ INSERT INTO mementos_m41_machine_preflight
15160
+ (id, name, hostname, platform, is_primary, created_at, last_seen_at)
15161
+ SELECT
15162
+ id,
15163
+ name,
15164
+ lower(regexp_replace(btrim(hostname), '\\.+$', '')),
15165
+ platform,
15166
+ is_primary,
15167
+ created_at,
15168
+ last_seen_at
15169
+ FROM machines;
15170
+
15171
+ UPDATE machines machine
15172
+ SET hostname = preflight.hostname
15173
+ FROM mementos_m41_machine_preflight preflight
15174
+ WHERE preflight.id = machine.id;
15175
+
15176
+ DROP TRIGGER IF EXISTS machines_single_primary_insert ON machines;
15177
+ DROP TRIGGER IF EXISTS machines_single_primary_update ON machines;
15178
+ DROP INDEX IF EXISTS idx_machines_hostname;
15179
+ CREATE UNIQUE INDEX idx_machines_hostname ON machines(hostname);
15180
+ DROP INDEX IF EXISTS idx_machines_single_primary;
15181
+ CREATE UNIQUE INDEX idx_machines_single_primary
15182
+ ON machines ((1)) WHERE is_primary = TRUE;
15183
+
15184
+ ALTER TABLE machines DROP CONSTRAINT IF EXISTS machines_hostname_canonical;
15185
+ ALTER TABLE machines ADD CONSTRAINT machines_hostname_canonical CHECK(
15186
+ length(hostname) BETWEEN 1 AND 253
15187
+ AND hostname = lower(regexp_replace(btrim(hostname), '\\.+$', ''))
15188
+ AND hostname !~ '[[:space:]/\\\\]'
15189
+ );
15190
+
15191
+ ALTER TABLE machines DROP CONSTRAINT IF EXISTS machines_name_runtime;
15192
+ ALTER TABLE machines ADD CONSTRAINT machines_name_runtime CHECK(
15193
+ length(name) BETWEEN 1 AND 128
15194
+ AND name !~ '[[:cntrl:]]'
15195
+ );
15196
+ ALTER TABLE machines DROP CONSTRAINT IF EXISTS machines_platform_runtime;
15197
+ ALTER TABLE machines ADD CONSTRAINT machines_platform_runtime CHECK(
15198
+ length(platform) BETWEEN 1 AND 64
15199
+ AND platform = lower(btrim(platform))
15200
+ AND platform ~ '^[a-z0-9._-]+$'
15201
+ );
15202
+ ALTER TABLE machines DROP CONSTRAINT IF EXISTS machines_liveness_runtime;
15203
+ ALTER TABLE machines ADD CONSTRAINT machines_liveness_runtime CHECK(
15204
+ last_seen_at >= created_at
15205
+ );
15206
+
15207
+ INSERT INTO _migrations (id) VALUES (41) ON CONFLICT DO NOTHING;
14180
15208
  `
14181
15209
  ];
14182
15210
  });
@@ -14232,24 +15260,32 @@ async function applyPgMigrations(connectionString) {
14232
15260
  totalMigrations: PG_MIGRATIONS.length
14233
15261
  };
14234
15262
  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));
15263
+ const migrationLockKey = 21760041;
15264
+ await pg2.transaction(async (client) => {
15265
+ await client.query("SELECT pg_advisory_xact_lock($1)", [migrationLockKey]);
15266
+ await client.query(`CREATE TABLE IF NOT EXISTS _pg_migrations (
15267
+ id SERIAL PRIMARY KEY,
15268
+ version INT UNIQUE NOT NULL,
15269
+ applied_at TIMESTAMPTZ DEFAULT NOW()
15270
+ )`);
15271
+ });
14242
15272
  for (let i = 0;i < PG_MIGRATIONS.length; i++) {
14243
- if (appliedSet.has(i)) {
14244
- result.alreadyApplied.push(i);
14245
- continue;
14246
- }
14247
15273
  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)}`);
15274
+ const outcome = await pg2.transaction(async (client) => {
15275
+ await client.query("SELECT pg_advisory_xact_lock($1)", [migrationLockKey]);
15276
+ const already = await client.query("SELECT 1 FROM _pg_migrations WHERE version = $1", [i]);
15277
+ if (already.rowCount)
15278
+ return "already";
15279
+ await client.query(PG_MIGRATIONS[i]);
15280
+ await client.query("INSERT INTO _pg_migrations (version) VALUES ($1)", [i]);
15281
+ return "applied";
15282
+ });
15283
+ if (outcome === "already")
15284
+ result.alreadyApplied.push(i);
15285
+ else
15286
+ result.applied.push(i);
15287
+ } catch (error) {
15288
+ result.errors.push(`Migration ${i}: ${error instanceof Error ? error.message : String(error)}`);
14253
15289
  break;
14254
15290
  }
14255
15291
  }
@@ -46679,7 +47715,7 @@ var require_token_io = __commonJS((exports, module) => {
46679
47715
  module.exports = __toCommonJS2(token_io_exports);
46680
47716
  var import_path2 = __toESM2(__require("path"));
46681
47717
  var import_fs2 = __toESM2(__require("fs"));
46682
- var import_os3 = __toESM2(__require("os"));
47718
+ var import_os2 = __toESM2(__require("os"));
46683
47719
  var import_token_error = require_token_error();
46684
47720
  function findRootDir() {
46685
47721
  try {
@@ -46700,11 +47736,11 @@ var require_token_io = __commonJS((exports, module) => {
46700
47736
  if (process.env.XDG_DATA_HOME) {
46701
47737
  return process.env.XDG_DATA_HOME;
46702
47738
  }
46703
- switch (import_os3.default.platform()) {
47739
+ switch (import_os2.default.platform()) {
46704
47740
  case "darwin":
46705
- return import_path2.default.join(import_os3.default.homedir(), "Library/Application Support");
47741
+ return import_path2.default.join(import_os2.default.homedir(), "Library/Application Support");
46706
47742
  case "linux":
46707
- return import_path2.default.join(import_os3.default.homedir(), ".local/share");
47743
+ return import_path2.default.join(import_os2.default.homedir(), ".local/share");
46708
47744
  case "win32":
46709
47745
  if (process.env.LOCALAPPDATA) {
46710
47746
  return process.env.LOCALAPPDATA;
@@ -46814,10 +47850,10 @@ var require_oauth = __commonJS((exports, module) => {
46814
47850
  refreshTokenRequest: () => refreshTokenRequest
46815
47851
  });
46816
47852
  module.exports = __toCommonJS2(oauth_exports);
46817
- var import_os3 = __require("os");
47853
+ var import_os2 = __require("os");
46818
47854
  var VERCEL_ISSUER = "https://vercel.com";
46819
47855
  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)()}`;
47856
+ var userAgent = `@vercel/oidc node-${process.version} ${(0, import_os2.platform)()} (${(0, import_os2.arch)()}) ${(0, import_os2.hostname)()}`;
46821
47857
  var _tokenEndpoint = null;
46822
47858
  async function getTokenEndpoint() {
46823
47859
  if (_tokenEndpoint) {
@@ -64449,65 +65485,283 @@ import { fileURLToPath as fileURLToPath5 } from "url";
64449
65485
 
64450
65486
  // src/db/machines.ts
64451
65487
  init_database();
65488
+ init_api_mode();
65489
+ init_api_response_contract();
65490
+ import { createHash } from "crypto";
64452
65491
  import { hostname, platform } from "os";
64453
- function parseMachine(row) {
65492
+ var MACHINE_REGISTRATION_CONTRACT = "mementos.machine-registration.v1";
65493
+ var MACHINE_LIST_CONTRACT = "mementos.machines.v1";
65494
+ class MachineRegistryError extends Error {
65495
+ code;
65496
+ constructor(code, message) {
65497
+ super(message);
65498
+ this.code = code;
65499
+ this.name = "MachineRegistryError";
65500
+ }
65501
+ }
65502
+ var ISO_UTC_TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
65503
+ function canonicalTimestamp(value, operation, field) {
65504
+ const stringValue = value instanceof Date ? value.toISOString() : value;
65505
+ if (typeof stringValue !== "string" || !ISO_UTC_TIMESTAMP.test(stringValue)) {
65506
+ throw new MementosApiProtocolError(operation, `expected '${field}' to be a canonical UTC ISO-8601 timestamp`);
65507
+ }
65508
+ const parsed = new Date(stringValue);
65509
+ if (!Number.isFinite(parsed.getTime()) || parsed.toISOString() !== stringValue) {
65510
+ throw new MementosApiProtocolError(operation, `expected '${field}' to be a real calendar timestamp`);
65511
+ }
65512
+ return stringValue;
65513
+ }
65514
+ function machineFromDb(row, operation = "machine database row") {
64454
65515
  if (!row)
64455
65516
  return null;
65517
+ 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) {
65518
+ throw new Error(`${operation} is missing or violates required machine identity fields`);
65519
+ }
65520
+ const canonicalHostname = normalizeMachineHostname(row.hostname);
65521
+ const canonicalPlatform = normalizeMachinePlatform(row.platform);
65522
+ if (canonicalHostname !== row.hostname || canonicalPlatform !== row.platform) {
65523
+ throw new Error(`${operation} contains noncanonical hostname or platform data`);
65524
+ }
65525
+ if (!["boolean", "number"].includes(typeof row.is_primary)) {
65526
+ throw new Error(`${operation} has an invalid is_primary value`);
65527
+ }
65528
+ const createdValue = row.created_at instanceof Date ? row.created_at : new Date(String(row.created_at ?? ""));
65529
+ const lastSeenValue = row.last_seen_at instanceof Date ? row.last_seen_at : new Date(String(row.last_seen_at ?? ""));
65530
+ if (!Number.isFinite(createdValue.getTime()) || !Number.isFinite(lastSeenValue.getTime())) {
65531
+ throw new Error(`${operation} has an invalid timestamp`);
65532
+ }
65533
+ const createdAt = createdValue.toISOString();
65534
+ const lastSeenAt = lastSeenValue.toISOString();
65535
+ if (lastSeenAt < createdAt)
65536
+ throw new Error(`${operation} has last_seen_at before created_at`);
64456
65537
  return {
64457
- ...row,
64458
- is_primary: Boolean(row.is_primary)
64459
- };
65538
+ id: row.id,
65539
+ name: row.name,
65540
+ hostname: canonicalHostname,
65541
+ platform: canonicalPlatform,
65542
+ is_primary: Boolean(row.is_primary),
65543
+ created_at: createdAt,
65544
+ last_seen_at: lastSeenAt
65545
+ };
65546
+ }
65547
+ function decodeMachine(value, operation) {
65548
+ const object = expectObject(value, operation);
65549
+ const name = expectString(object, "name", operation);
65550
+ if (name.length > 128 || /[\u0000-\u001f\u007f]/.test(name)) {
65551
+ throw new MementosApiProtocolError(operation, "machine.name is outside the public contract");
65552
+ }
65553
+ const rawHost = expectString(object, "hostname", operation);
65554
+ const rawPlatform = expectString(object, "platform", operation);
65555
+ let machineHostname;
65556
+ let machinePlatform;
65557
+ try {
65558
+ machineHostname = normalizeMachineHostname(rawHost);
65559
+ machinePlatform = normalizeMachinePlatform(rawPlatform);
65560
+ } catch {
65561
+ throw new MementosApiProtocolError(operation, "machine hostname or platform is not canonical");
65562
+ }
65563
+ if (machineHostname !== rawHost || machinePlatform !== rawPlatform) {
65564
+ throw new MementosApiProtocolError(operation, "machine hostname or platform is not canonical");
65565
+ }
65566
+ const createdAt = canonicalTimestamp(object["created_at"], operation, "created_at");
65567
+ const lastSeenAt = canonicalTimestamp(object["last_seen_at"], operation, "last_seen_at");
65568
+ if (lastSeenAt < createdAt) {
65569
+ throw new MementosApiProtocolError(operation, "last_seen_at precedes created_at");
65570
+ }
65571
+ return {
65572
+ id: expectString(object, "id", operation),
65573
+ name,
65574
+ hostname: machineHostname,
65575
+ platform: machinePlatform,
65576
+ is_primary: expectBoolean(object, "is_primary", operation),
65577
+ created_at: createdAt,
65578
+ last_seen_at: lastSeenAt
65579
+ };
65580
+ }
65581
+ function expectContract(object, contract, operation) {
65582
+ if (object["contract"] !== contract) {
65583
+ throw new MementosApiProtocolError(operation, `expected contract '${contract}'`);
65584
+ }
65585
+ }
65586
+ function decodeRegistration(value, expected) {
65587
+ const operation = "POST /v1/machines";
65588
+ const object = expectObject(value, operation);
65589
+ expectContract(object, MACHINE_REGISTRATION_CONTRACT, operation);
65590
+ const machine = decodeMachine(object["machine"], operation);
65591
+ const created = expectBoolean(object, "created", operation);
65592
+ const identity = expectObject(object["identity"], operation);
65593
+ if (identity["idempotency_key"] !== "normalized_hostname") {
65594
+ throw new MementosApiProtocolError(operation, "expected normalized-hostname identity semantics");
65595
+ }
65596
+ if (expectString(identity, "stable_id", operation) !== machine.id) {
65597
+ throw new MementosApiProtocolError(operation, "identity stable_id does not match machine.id");
65598
+ }
65599
+ const expectedHostname = normalizeMachineHostname(expected.hostname);
65600
+ const expectedPlatform = normalizeMachinePlatform(expected.platform);
65601
+ if (machine.hostname !== expectedHostname || machine.platform !== expectedPlatform) {
65602
+ throw new MementosApiProtocolError(operation, "registration receipt does not match the requested hostname/platform");
65603
+ }
65604
+ if (created && machine.name !== normalizeMachineName(expected.name, expectedHostname)) {
65605
+ throw new MementosApiProtocolError(operation, "created registration receipt does not match the requested name");
65606
+ }
65607
+ return { machine, created };
65608
+ }
65609
+ function decodeMachineList(value) {
65610
+ const operation = "GET /v1/machines";
65611
+ const object = expectObject(value, operation);
65612
+ expectContract(object, MACHINE_LIST_CONTRACT, operation);
65613
+ const values = expectArray(object["machines"], operation, "machines");
65614
+ const count = expectNonNegativeInteger(object, "count", operation);
65615
+ if (count !== values.length) {
65616
+ throw new MementosApiProtocolError(operation, "count does not match machines.length");
65617
+ }
65618
+ if (object["complete"] !== true) {
65619
+ throw new MementosApiProtocolError(operation, "expected complete=true");
65620
+ }
65621
+ const machines = values.map((entry, index) => decodeMachine(entry, `${operation} machines[${index}]`));
65622
+ const ids = new Set;
65623
+ const hosts = new Set;
65624
+ const names = new Set;
65625
+ let primaryCount = 0;
65626
+ for (const machine of machines) {
65627
+ if (ids.has(machine.id))
65628
+ throw new MementosApiProtocolError(operation, `duplicate machine id '${machine.id}'`);
65629
+ if (hosts.has(machine.hostname))
65630
+ throw new MementosApiProtocolError(operation, `duplicate machine hostname '${machine.hostname}'`);
65631
+ if (names.has(machine.name))
65632
+ throw new MementosApiProtocolError(operation, `duplicate machine name '${machine.name}'`);
65633
+ ids.add(machine.id);
65634
+ hosts.add(machine.hostname);
65635
+ names.add(machine.name);
65636
+ if (machine.is_primary)
65637
+ primaryCount += 1;
65638
+ }
65639
+ if (primaryCount > 1)
65640
+ throw new MementosApiProtocolError(operation, "more than one machine is primary");
65641
+ return machines;
65642
+ }
65643
+ function normalizeMachineHostname(host) {
65644
+ const normalized = host.trim().replace(/\.+$/, "").toLowerCase();
65645
+ if (!normalized || normalized.length > 253 || /[\u0000-\u001f\u007f/\\\s]/.test(normalized)) {
65646
+ throw new MachineRegistryError("MACHINE_INVALID_INPUT", "Machine hostname must be a non-empty hostname of at most 253 characters");
65647
+ }
65648
+ return normalized;
64460
65649
  }
64461
- function normalizeHostname(host) {
64462
- return host.replace(/\.(local|lan|home|internal)$/i, "");
65650
+ function normalizeMachinePlatform(value) {
65651
+ const normalized = value.trim().toLowerCase();
65652
+ if (!normalized || normalized.length > 64 || !/^[a-z0-9._-]+$/.test(normalized)) {
65653
+ throw new MachineRegistryError("MACHINE_INVALID_INPUT", "Machine platform must contain 1-64 lowercase letters, digits, dots, underscores, or hyphens");
65654
+ }
65655
+ return normalized;
64463
65656
  }
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));
65657
+ function normalizeMachineName(value, fallback) {
65658
+ const normalized = value?.trim() || fallback;
65659
+ if (!normalized || normalized.length > 128 || /[\u0000-\u001f\u007f]/.test(normalized)) {
65660
+ throw new MachineRegistryError("MACHINE_INVALID_INPUT", "Machine name must contain 1-128 printable characters");
64473
65661
  }
64474
- let finalName = machineName;
64475
- let suffix = 2;
64476
- while (db.query("SELECT id FROM machines WHERE name = ?").get(finalName)) {
64477
- finalName = `${machineName}-${suffix++}`;
65662
+ return normalized;
65663
+ }
65664
+ function registerMachineRecord(input, db) {
65665
+ const d = db ?? getDatabase();
65666
+ const host = normalizeMachineHostname(input.hostname);
65667
+ const machineName = normalizeMachineName(input.name, host);
65668
+ const machinePlatform = normalizeMachinePlatform(input.platform);
65669
+ const proposedId = uuid();
65670
+ const seenAt = now();
65671
+ try {
65672
+ const row = d.query(`INSERT INTO machines (id, name, hostname, platform, created_at, last_seen_at)
65673
+ VALUES (?, ?, ?, ?, ?, ?)
65674
+ ON CONFLICT(hostname) DO UPDATE SET
65675
+ platform = excluded.platform,
65676
+ last_seen_at = CASE
65677
+ WHEN machines.last_seen_at > excluded.last_seen_at THEN machines.last_seen_at
65678
+ ELSE excluded.last_seen_at
65679
+ END
65680
+ RETURNING *, id = ? AS registration_created`).get(proposedId, machineName, host, machinePlatform, seenAt, seenAt, proposedId);
65681
+ const machine = machineFromDb(row, "machine registration upsert");
65682
+ if (!machine || typeof row?.registration_created !== "boolean" && typeof row?.registration_created !== "number") {
65683
+ throw new MachineRegistryError("MACHINE_REGISTRATION_CONFLICT", "Machine registration returned no stable identity receipt");
65684
+ }
65685
+ return { machine, created: Boolean(row.registration_created) };
65686
+ } catch (error) {
65687
+ if (error instanceof MachineRegistryError)
65688
+ throw error;
65689
+ const details = error && typeof error === "object" ? error : {};
65690
+ const diagnostic = `${String(details.constraint ?? "")} ${String(details.message ?? "")}`;
65691
+ if (/machines(?:_|\.)name|machines_name_key/i.test(diagnostic)) {
65692
+ throw new MachineRegistryError("MACHINE_NAME_CONFLICT", `Machine name already taken: ${machineName}`);
65693
+ }
65694
+ throw error;
64478
65695
  }
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));
64482
65696
  }
64483
- function getPrimaryMachine(db = getDatabase()) {
64484
- return parseMachine(db.query("SELECT * FROM machines WHERE is_primary = 1 LIMIT 1").get());
65697
+ function registerMachine(name, db) {
65698
+ const host = normalizeMachineHostname(hostname());
65699
+ const plat = normalizeMachinePlatform(platform());
65700
+ if (!db && isApiMode()) {
65701
+ const input = { name: name?.trim() || undefined, hostname: host, platform: plat };
65702
+ const { data } = apiJson("POST", "/machines", input);
65703
+ return decodeRegistration(data, input).machine;
65704
+ }
65705
+ return registerMachineRecord({ name, hostname: host, platform: plat }, db).machine;
64485
65706
  }
64486
- function getPrimaryMachineCandidate(db = getDatabase()) {
64487
- if (getPrimaryMachine(db))
65707
+ function listMachines(db) {
65708
+ if (!db && isApiMode()) {
65709
+ const { data } = apiJson("GET", "/machines");
65710
+ return decodeMachineList(data);
65711
+ }
65712
+ const d = db ?? getDatabase();
65713
+ const rows = d.query("SELECT * FROM machines ORDER BY is_primary DESC, last_seen_at DESC, created_at ASC, id ASC").all();
65714
+ return rows.map((row) => machineFromDb(row));
65715
+ }
65716
+ function getPrimaryMachine(db) {
65717
+ if (!db && isApiMode())
65718
+ return listMachines().find((machine) => machine.is_primary) ?? null;
65719
+ const d = db ?? getDatabase();
65720
+ return machineFromDb(d.query("SELECT * FROM machines WHERE is_primary = 1 LIMIT 1").get());
65721
+ }
65722
+ function getPrimaryMachineCandidate(db) {
65723
+ if (!db && isApiMode()) {
65724
+ const machines = listMachines();
65725
+ if (machines.some((machine) => machine.is_primary))
65726
+ return null;
65727
+ return [...machines].sort((a, b) => a.created_at.localeCompare(b.created_at) || a.id.localeCompare(b.id))[0] ?? null;
65728
+ }
65729
+ const d = db ?? getDatabase();
65730
+ if (getPrimaryMachine(d))
64488
65731
  return null;
64489
- return parseMachine(db.query("SELECT * FROM machines ORDER BY created_at ASC, id ASC LIMIT 1").get());
65732
+ return machineFromDb(d.query("SELECT * FROM machines ORDER BY created_at ASC, id ASC LIMIT 1").get());
64490
65733
  }
64491
- function getPrimaryMachineStartupWarning(db = getDatabase()) {
65734
+ function getPrimaryMachineStartupWarning(db) {
64492
65735
  if (getPrimaryMachine(db))
64493
65736
  return null;
64494
65737
  const candidate = getPrimaryMachineCandidate(db);
64495
- if (!candidate) {
65738
+ if (!candidate)
64496
65739
  return "No primary machine configured. Fallback sync target is unset because no machines are registered yet.";
64497
- }
64498
65740
  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
65741
  }
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;
65742
+ var hostedMachineCache = null;
65743
+ function getCurrentMachineId(db) {
65744
+ if (!db && isApiMode()) {
65745
+ const host = normalizeMachineHostname(hostname());
65746
+ const plat = normalizeMachinePlatform(platform());
65747
+ const configBefore = getApiConfig();
65748
+ if (!configBefore)
65749
+ throw new Error("mementos hosted machine identity requires a resolved API authority");
65750
+ const credentialFingerprint = createHash("sha256").update(configBefore.apiKey).digest("hex");
65751
+ const key = `${configBefore.baseUrl}\x00${credentialFingerprint}\x00${host}\x00${plat}`;
65752
+ if (hostedMachineCache?.key === key)
65753
+ return hostedMachineCache.id;
65754
+ const id = registerMachine().id;
65755
+ const configAfter = getApiConfig();
65756
+ const fingerprintAfter = configAfter ? createHash("sha256").update(configAfter.apiKey).digest("hex") : null;
65757
+ if (!configAfter || configAfter.baseUrl !== configBefore.baseUrl || fingerprintAfter !== credentialFingerprint) {
65758
+ throw new Error("MEMENTOS_AUTHORITY_CHANGED: the machine registry authority or credential changed while resolving the stable machine identity");
65759
+ }
65760
+ hostedMachineCache = { key, id };
65761
+ return id;
64509
65762
  }
64510
- return registerMachine(undefined, db).id;
65763
+ const d = db ?? getDatabase();
65764
+ return registerMachine(undefined, d).id;
64511
65765
  }
64512
65766
 
64513
65767
  // src/cli/startup-side-effects.ts
@@ -65256,7 +66510,7 @@ init_memories();
65256
66510
  init_package_version();
65257
66511
  init_schema();
65258
66512
  init_identity();
65259
- import { createHash as createHash2 } from "crypto";
66513
+ import { createHash as createHash3 } from "crypto";
65260
66514
 
65261
66515
  class MemoryProjectLinkError extends Error {
65262
66516
  code;
@@ -65296,7 +66550,7 @@ function canonicalJson(value) {
65296
66550
  return JSON.stringify(canonicalize(value));
65297
66551
  }
65298
66552
  function digest(value) {
65299
- return createHash2("sha256").update(canonicalJson(value)).digest("hex");
66553
+ return createHash3("sha256").update(canonicalJson(value)).digest("hex");
65300
66554
  }
65301
66555
  function timestampString2(value) {
65302
66556
  return value instanceof Date ? value.toISOString() : String(value);
@@ -65881,7 +67135,7 @@ function getMemoryProjectLinkReceipt(memoryId, receiptId, identity = linkAuthori
65881
67135
  }
65882
67136
 
65883
67137
  // src/project-registration/authority.ts
65884
- import { createHash as createHash3 } from "crypto";
67138
+ import { createHash as createHash4 } from "crypto";
65885
67139
  init_projects();
65886
67140
  init_types2();
65887
67141
  init_identity();
@@ -65903,7 +67157,7 @@ function canonicalize2(value) {
65903
67157
  return out;
65904
67158
  }
65905
67159
  function digestMementosProjectRegistrationValue(value) {
65906
- return createHash3("sha256").update(canonicalMementosProjectRegistrationJson(value)).digest("hex");
67160
+ return createHash4("sha256").update(canonicalMementosProjectRegistrationJson(value)).digest("hex");
65907
67161
  }
65908
67162
  // src/project-registration/http.ts
65909
67163
  init_types2();
@@ -66518,18 +67772,20 @@ function registerCrudCommands(program2) {
66518
67772
  handleError(e);
66519
67773
  }
66520
67774
  });
66521
- program2.command("update <id>").description("Update a memory by ID").option("--value <text>", "New value").option("--importance <n>", "New importance 1-10", parseInt).option("--tags <tags>", "New comma-separated tags").option("--summary <text>", "New summary").option("--pin", "Pin the memory").option("--unpin", "Unpin the memory").option("-c, --category <cat>", "New category").option("--scope <scope>", "New scope").option("--status <status>", "New status: active, archived, expired").action((id, opts) => {
67775
+ program2.command("update <id>").description("Update a memory by ID").option("--value <text>", "New value").option("--importance <n>", "New importance 1-10", parseInt).option("--tags <tags>", "New comma-separated tags").option("--summary <text>", "New summary").option("--pin", "Pin the memory").option("--unpin", "Unpin the memory").option("-c, --category <cat>", "New category").option("--scope <scope>", "New scope").option("--status <status>", "New status: active, archived, expired").action(async (id, opts) => {
66522
67776
  try {
66523
67777
  const globalOpts = program2.opts();
66524
67778
  const resolvedId = resolveMemoryId(id);
66525
67779
  const existing = getMemory(resolvedId);
66526
67780
  if (!existing) {
66527
67781
  if (globalOpts.json) {
66528
- outputJson({ error: `Memory not found: ${id}` });
67782
+ await outputJsonAndExit({ error: `Memory not found: ${id}` }, 1);
66529
67783
  } else {
66530
67784
  console.error(chalk2.red(`Memory not found: ${id}`));
66531
67785
  }
66532
- process.exit(1);
67786
+ if (!globalOpts.json)
67787
+ process.exit(1);
67788
+ return;
66533
67789
  }
66534
67790
  const updateInput = {
66535
67791
  version: existing.version
@@ -66581,7 +67837,7 @@ function registerCrudCommands(program2) {
66581
67837
  handleError(e);
66582
67838
  }
66583
67839
  });
66584
- program2.command("forget <keyOrId>").description("Delete a memory by key or ID").option("--scope <scope>", "Filter by scope (global, shared, private)").option("--agent <agent>", "Filter by agent ID").option("--project <project>", "Filter by project ID").option("--all", "Delete ALL matching memories (no disambiguation needed)").action((keyOrId, opts) => {
67840
+ program2.command("forget <keyOrId>").description("Delete a memory by key or ID").option("--scope <scope>", "Filter by scope (global, shared, private)").option("--agent <agent>", "Filter by agent ID").option("--project <project>", "Filter by project ID").option("--all", "Delete ALL matching memories (no disambiguation needed)").action(async (keyOrId, opts) => {
66585
67841
  try {
66586
67842
  const globalOpts = program2.opts();
66587
67843
  const idMatch = isApiMode() ? null : resolvePartialId(getDatabase(), "memories", keyOrId);
@@ -66605,11 +67861,13 @@ function registerCrudCommands(program2) {
66605
67861
  return;
66606
67862
  }
66607
67863
  if (globalOpts.json) {
66608
- outputJson({ error: `No memory found: ${keyOrId}` });
67864
+ await outputJsonAndExit({ error: `No memory found: ${keyOrId}` }, 1);
66609
67865
  } else {
66610
67866
  console.error(chalk2.red(`No memory found: ${keyOrId}`));
66611
67867
  }
66612
- process.exit(1);
67868
+ if (!globalOpts.json)
67869
+ process.exit(1);
67870
+ return;
66613
67871
  }
66614
67872
  if (matches.length === 1) {
66615
67873
  deleteMemory(matches[0].id);
@@ -66632,10 +67890,10 @@ function registerCrudCommands(program2) {
66632
67890
  return;
66633
67891
  }
66634
67892
  if (globalOpts.json) {
66635
- outputJson({
67893
+ await outputJsonAndExit({
66636
67894
  error: `Ambiguous key "${keyOrId}" \u2014 ${matches.length} memories found. Use --all to delete all, or specify an ID.`,
66637
67895
  matches: matches.map((m) => ({ id: m.id, key: redactCredentialKey(m.key), scope: m.scope, category: m.category, agent_id: m.agent_id }))
66638
- });
67896
+ }, 1);
66639
67897
  } else {
66640
67898
  console.log(chalk2.yellow(`Ambiguous key "${keyOrId}" \u2014 ${matches.length} memories found:`));
66641
67899
  for (const m of matches) {
@@ -66644,7 +67902,8 @@ function registerCrudCommands(program2) {
66644
67902
  console.log(chalk2.dim(`
66645
67903
  Use --all to delete all, or specify an ID.`));
66646
67904
  }
66647
- process.exit(1);
67905
+ if (!globalOpts.json)
67906
+ process.exit(1);
66648
67907
  } catch (e) {
66649
67908
  handleError(e);
66650
67909
  }
@@ -66658,18 +67917,20 @@ init_redact();
66658
67917
  import chalk3 from "chalk";
66659
67918
  function registerViewCommands(program2) {
66660
67919
  const handleError = makeHandleError(program2);
66661
- program2.command("show <id>").description("Show full detail of a memory by ID (supports partial IDs)").action((id) => {
67920
+ program2.command("show <id>").description("Show full detail of a memory by ID (supports partial IDs)").action(async (id) => {
66662
67921
  try {
66663
67922
  const globalOpts = program2.opts();
66664
67923
  const resolvedId = resolveMemoryId(id);
66665
67924
  const memory = getMemory(resolvedId);
66666
67925
  if (!memory) {
66667
67926
  if (globalOpts.json) {
66668
- outputJson({ error: `Memory not found: ${id}` });
67927
+ await outputJsonAndExit({ error: `Memory not found: ${id}` }, 1);
66669
67928
  } else {
66670
67929
  console.error(chalk3.red(`Memory not found: ${id}`));
66671
67930
  }
66672
- process.exit(1);
67931
+ if (!globalOpts.json)
67932
+ process.exit(1);
67933
+ return;
66673
67934
  }
66674
67935
  touchMemory(memory.id);
66675
67936
  const safe = redactMemoryForOutput(memory);
@@ -66682,17 +67943,19 @@ function registerViewCommands(program2) {
66682
67943
  handleError(e);
66683
67944
  }
66684
67945
  });
66685
- program2.command("pin <keyOrId>").description("Pin a memory by key or partial ID").option("--scope <scope>", "Scope filter for key lookup").option("--agent <name>", "Agent filter for key lookup").option("--project <path>", "Project filter for key lookup").action((keyOrId, opts) => {
67946
+ program2.command("pin <keyOrId>").description("Pin a memory by key or partial ID").option("--scope <scope>", "Scope filter for key lookup").option("--agent <name>", "Agent filter for key lookup").option("--project <path>", "Project filter for key lookup").action(async (keyOrId, opts) => {
66686
67947
  try {
66687
67948
  const globalOpts = program2.opts();
66688
67949
  const memory = resolveKeyOrId(keyOrId, opts, globalOpts);
66689
67950
  if (!memory) {
66690
67951
  if (globalOpts.json) {
66691
- outputJson({ error: `No memory found: ${keyOrId}` });
67952
+ await outputJsonAndExit({ error: `No memory found: ${keyOrId}` }, 1);
66692
67953
  } else {
66693
67954
  console.error(chalk3.red(`No memory found: ${keyOrId}`));
66694
67955
  }
66695
- process.exit(1);
67956
+ if (!globalOpts.json)
67957
+ process.exit(1);
67958
+ return;
66696
67959
  }
66697
67960
  const updated = updateMemory(memory.id, {
66698
67961
  version: memory.version,
@@ -66707,17 +67970,19 @@ function registerViewCommands(program2) {
66707
67970
  handleError(e);
66708
67971
  }
66709
67972
  });
66710
- program2.command("unpin <keyOrId>").description("Unpin a memory by key or partial ID").option("--scope <scope>", "Scope filter for key lookup").option("--agent <name>", "Agent filter for key lookup").option("--project <path>", "Project filter for key lookup").action((keyOrId, opts) => {
67973
+ program2.command("unpin <keyOrId>").description("Unpin a memory by key or partial ID").option("--scope <scope>", "Scope filter for key lookup").option("--agent <name>", "Agent filter for key lookup").option("--project <path>", "Project filter for key lookup").action(async (keyOrId, opts) => {
66711
67974
  try {
66712
67975
  const globalOpts = program2.opts();
66713
67976
  const memory = resolveKeyOrId(keyOrId, opts, globalOpts);
66714
67977
  if (!memory) {
66715
67978
  if (globalOpts.json) {
66716
- outputJson({ error: `No memory found: ${keyOrId}` });
67979
+ await outputJsonAndExit({ error: `No memory found: ${keyOrId}` }, 1);
66717
67980
  } else {
66718
67981
  console.error(chalk3.red(`No memory found: ${keyOrId}`));
66719
67982
  }
66720
- process.exit(1);
67983
+ if (!globalOpts.json)
67984
+ process.exit(1);
67985
+ return;
66721
67986
  }
66722
67987
  const updated = updateMemory(memory.id, {
66723
67988
  version: memory.version,
@@ -66910,14 +68175,167 @@ init_helpers();
66910
68175
  init_redact();
66911
68176
  import chalk6 from "chalk";
66912
68177
  import { resolve as resolve6 } from "path";
68178
+
68179
+ // src/cli/structured-json.ts
68180
+ init_helpers();
68181
+ var STRUCTURED_PAGE_MAX_ROWS = 1000;
68182
+ var STRUCTURED_ALL_MAX_ROWS = 1e5;
68183
+ var STRUCTURED_DEFAULT_MAX_BYTES = 32 * 1024;
68184
+ var STRUCTURED_FULL_MAX_BYTES = 64 * 1024;
68185
+ var STRUCTURED_ALL_MAX_BYTES = 64 * 1024 * 1024;
68186
+ var STRUCTURED_MIN_MAX_BYTES = 1024;
68187
+ function structuredMaxBytes(value, opts) {
68188
+ const fallback = opts.all ? STRUCTURED_ALL_MAX_BYTES : opts.detail === "full" ? STRUCTURED_FULL_MAX_BYTES : STRUCTURED_DEFAULT_MAX_BYTES;
68189
+ if (value === undefined)
68190
+ return fallback;
68191
+ const parsed = typeof value === "number" ? value : Number(value);
68192
+ if (!Number.isInteger(parsed) || parsed < STRUCTURED_MIN_MAX_BYTES || parsed > STRUCTURED_ALL_MAX_BYTES) {
68193
+ throw new Error(`--max-bytes must be an integer from ${STRUCTURED_MIN_MAX_BYTES} to ${STRUCTURED_ALL_MAX_BYTES}`);
68194
+ }
68195
+ return parsed;
68196
+ }
68197
+ function structuredPageLimit(value, fallback) {
68198
+ const parsed = value === undefined ? fallback : Number(value);
68199
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > STRUCTURED_PAGE_MAX_ROWS) {
68200
+ throw new Error(`--limit must be an integer from 1 to ${STRUCTURED_PAGE_MAX_ROWS}`);
68201
+ }
68202
+ return parsed;
68203
+ }
68204
+ function compactMemory(memory, opts = {}) {
68205
+ const value = truncateText(memory.summary || memory.value, 240);
68206
+ return {
68207
+ id: memory.id,
68208
+ key: memory.key,
68209
+ value,
68210
+ scope: memory.scope,
68211
+ category: memory.category,
68212
+ importance: memory.importance,
68213
+ status: memory.status,
68214
+ pinned: memory.pinned,
68215
+ ...Array.isArray(memory.tags) && memory.tags.length ? { tags: memory.tags.slice(0, 10) } : {},
68216
+ ...memory.agent_id ? { agent_id: memory.agent_id } : {},
68217
+ ...memory.project_id ? { project_id: memory.project_id } : {},
68218
+ ...memory.session_id ? { session_id: memory.session_id } : {},
68219
+ ...opts.history && memory.accessed_at ? { accessed_at: memory.accessed_at } : {},
68220
+ updated_at: memory.updated_at
68221
+ };
68222
+ }
68223
+ function compactProject(project) {
68224
+ return {
68225
+ id: project.id,
68226
+ name: project.name,
68227
+ path: truncateText(project.path, 240),
68228
+ ...project.description ? { description: truncateText(project.description, 240) } : {},
68229
+ ...project.memory_prefix ? { memory_prefix: project.memory_prefix } : {},
68230
+ updated_at: project.updated_at
68231
+ };
68232
+ }
68233
+ function compactAgent(agent) {
68234
+ return {
68235
+ id: agent.id,
68236
+ name: agent.name,
68237
+ role: agent.role || "agent",
68238
+ ...agent.description ? { description: truncateText(agent.description, 240) } : {},
68239
+ ...agent.active_project_id ? { active_project_id: agent.active_project_id } : {},
68240
+ last_seen_at: agent.last_seen_at
68241
+ };
68242
+ }
68243
+ function compactSearchResult(result) {
68244
+ return {
68245
+ memory: compactMemory(result.memory),
68246
+ score: result.score,
68247
+ match_type: result.match_type,
68248
+ ...result.confidence !== undefined ? { confidence: result.confidence } : {},
68249
+ ...result.highlights?.length ? { highlights: result.highlights.slice(0, 3).map((highlight) => ({
68250
+ field: highlight.field,
68251
+ snippet: truncateText(highlight.snippet, 240)
68252
+ })) } : {}
68253
+ };
68254
+ }
68255
+ function makeEnvelope(args, items, byteTruncated, omittedFromPage) {
68256
+ const hasMore = byteTruncated || args.sourceHasMore;
68257
+ const complete = args.offset === 0 && !hasMore;
68258
+ const nextCursor = hasMore ? args.offset + items.length : null;
68259
+ const envelope = {
68260
+ [args.collection]: items,
68261
+ _meta: {
68262
+ receipt: args.receipt,
68263
+ count: items.length,
68264
+ limit: args.all ? null : args.limit,
68265
+ offset: args.offset,
68266
+ next_cursor: nextCursor,
68267
+ has_more: hasMore,
68268
+ complete,
68269
+ all: args.all,
68270
+ detail: args.detail,
68271
+ max_rows: args.all ? STRUCTURED_ALL_MAX_ROWS : STRUCTURED_PAGE_MAX_ROWS,
68272
+ max_bytes: args.maxBytes,
68273
+ response_bytes: 0,
68274
+ truncated: hasMore,
68275
+ truncation_reason: byteTruncated ? "max_bytes" : args.sourceHasMore ? "limit" : null,
68276
+ omitted_from_page: omittedFromPage,
68277
+ next_arguments: nextCursor === null ? null : {
68278
+ cursor: nextCursor,
68279
+ limit: args.limit,
68280
+ ...args.includeDetailInNextArguments !== false && args.detail === "full" ? { full: true } : {},
68281
+ max_bytes: args.maxBytes,
68282
+ ...args.nextArguments
68283
+ },
68284
+ continuation_scope: "unchanged_snapshot"
68285
+ }
68286
+ };
68287
+ for (let attempt = 0;attempt < 8; attempt += 1) {
68288
+ const bytes = Buffer.byteLength(`${JSON.stringify(envelope)}
68289
+ `);
68290
+ const meta = envelope["_meta"];
68291
+ if (bytes === meta.response_bytes)
68292
+ break;
68293
+ meta.response_bytes = bytes;
68294
+ }
68295
+ return envelope;
68296
+ }
68297
+ function structuredCollectionOutput(args) {
68298
+ const complete = makeEnvelope(args, args.items, false, 0);
68299
+ const completeText = `${JSON.stringify(complete)}
68300
+ `;
68301
+ if (Buffer.byteLength(completeText) <= args.maxBytes)
68302
+ return completeText;
68303
+ if (args.all) {
68304
+ throw new Error(`Exhaustive structured output exceeds the hard safety limit of ${args.maxBytes} bytes; use paginated JSON output or raise --max-bytes explicitly`);
68305
+ }
68306
+ for (let count = args.items.length - 1;count >= 0; count -= 1) {
68307
+ const envelope = makeEnvelope({ ...args, sourceHasMore: true }, args.items.slice(0, count), true, args.items.length - count);
68308
+ const text = `${JSON.stringify(envelope)}
68309
+ `;
68310
+ if (Buffer.byteLength(text) > args.maxBytes)
68311
+ continue;
68312
+ if (count === 0 && args.items.length > 0) {
68313
+ throw new Error(`One structured row exceeds --max-bytes=${args.maxBytes}; use compact detail or raise --max-bytes`);
68314
+ }
68315
+ return text;
68316
+ }
68317
+ throw new Error(`Structured output metadata exceeds --max-bytes=${args.maxBytes}`);
68318
+ }
68319
+
68320
+ // src/cli/commands/memory-cmd-search.ts
66913
68321
  function registerSearchCommand(program2) {
66914
68322
  const handleError = makeHandleError(program2);
66915
- program2.command("search <query>").description("Full-text search across memories").option("--scope <scope>", "Scope filter").option("-c, --category <cat>", "Category filter").option("--tags <tags>", "Comma-separated tags filter").option("--project <path>", "Project filter (path or name)").option("--agent <name>", "Agent filter").option("--session <id>", "Session ID filter").option("--limit <n>", "Max results", parseInt).option("--offset <n>", "Offset for pagination", parseInt).option("--cursor <n>", "Cursor offset for the next page", parseInt).option("--format <fmt>", "Output format: compact (default), json, csv, yaml").option("--verbose", "Show match highlights and wider snippets").option("--history", "Show recent search queries instead of searching").option("--popular", "Show most popular search queries").action((query, opts) => {
68323
+ program2.command("search <query>").description("Full-text search across memories").option("--scope <scope>", "Scope filter").option("-c, --category <cat>", "Category filter").option("--tags <tags>", "Comma-separated tags filter").option("--project <path>", "Project filter (path or name)").option("--agent <name>", "Agent filter").option("--session <id>", "Session ID filter").option("--limit <n>", "Max results", parseInt).option("--offset <n>", "Offset for pagination", parseInt).option("--cursor <n>", "Cursor offset for the next page", parseInt).option("--format <fmt>", "Output format: compact (default), json, csv, yaml").option("--verbose", "Show match highlights and wider snippets").option("--all", `Exhaust matching results in one explicit JSON receipt (hard max: ${STRUCTURED_ALL_MAX_ROWS})`).option("--full", "Return full legacy search-result objects in the bounded JSON receipt").option("--max-bytes <n>", "JSON response byte ceiling", parseInt).option("--history", "Show recent search queries instead of searching").option("--popular", "Show most popular search queries").action((query, opts) => {
66916
68324
  try {
66917
68325
  const fmt = getOutputFormat(program2, opts.format);
66918
68326
  const isStructured = fmt === "json" || fmt === "csv" || fmt === "yaml";
66919
- const limit = positiveIntOrDefault(opts.limit, isStructured ? 20 : DEFAULT_SEARCH_LIMIT);
66920
- const offset = cursorOrOffset(opts.cursor, opts.offset);
68327
+ const jsonMode = fmt === "json";
68328
+ const all = Boolean(opts.all);
68329
+ const detail = opts.full ? "full" : "compact";
68330
+ if (!jsonMode && (all || opts.full || opts.maxBytes !== undefined)) {
68331
+ throw new Error("--all, --full, and --max-bytes require JSON output");
68332
+ }
68333
+ if (all && opts.limit !== undefined)
68334
+ throw new Error("--all cannot be combined with --limit");
68335
+ const limit = jsonMode ? structuredPageLimit(opts.limit, 20) : positiveIntOrDefault(opts.limit, isStructured ? 20 : DEFAULT_SEARCH_LIMIT);
68336
+ const offset = cursorOrOffset(opts.cursor, opts.offset) ?? 0;
68337
+ if (all && offset !== 0)
68338
+ throw new Error("--all requires --cursor/--offset 0");
66921
68339
  if (opts.history) {
66922
68340
  const history = getSearchHistory(limit);
66923
68341
  if (fmt === "json") {
@@ -66965,16 +68383,36 @@ function registerSearchCommand(program2) {
66965
68383
  tags: opts.tags ? opts.tags.split(",").map((t) => t.trim()) : undefined,
66966
68384
  project_id: projectId,
66967
68385
  agent_id: agentId,
66968
- session_id: opts.session || globalOpts.session,
66969
- limit: isStructured ? limit : limit + 1,
66970
- offset
68386
+ session_id: opts.session || globalOpts.session
66971
68387
  };
66972
- const fetched = searchMemories(query, filter);
68388
+ const target = all ? STRUCTURED_ALL_MAX_ROWS : limit;
68389
+ const { rows: fetched, hasMore } = collectPagedRows((cursor, pageLimit) => {
68390
+ const rows = searchMemories(query, { ...filter, limit: pageLimit, offset: cursor });
68391
+ return {
68392
+ rows,
68393
+ has_more: rows.length < pageLimit ? false : undefined,
68394
+ next_cursor: cursor + rows.length
68395
+ };
68396
+ }, target, offset);
68397
+ if (all && hasMore) {
68398
+ throw new Error(`Exhaustive search output exceeds the hard safety limit of ${STRUCTURED_ALL_MAX_ROWS} rows; use paginated JSON output`);
68399
+ }
66973
68400
  const sanitized = fetched.map(redactSearchResultForOutput);
66974
- const hasMore = !isStructured && sanitized.length > limit;
66975
68401
  const results = hasMore ? sanitized.slice(0, limit) : sanitized;
66976
- if (fmt === "json") {
66977
- outputJson(sanitized);
68402
+ if (jsonMode) {
68403
+ const items = detail === "full" ? results.map((result) => ({ ...result })) : results.map(compactSearchResult);
68404
+ process.stdout.write(structuredCollectionOutput({
68405
+ collection: "results",
68406
+ receipt: "mementos.search.page.v1",
68407
+ items,
68408
+ offset,
68409
+ limit,
68410
+ sourceHasMore: hasMore,
68411
+ all,
68412
+ detail,
68413
+ maxBytes: structuredMaxBytes(opts.maxBytes, { all, detail }),
68414
+ nextArguments: { query }
68415
+ }));
66978
68416
  return;
66979
68417
  }
66980
68418
  if (fmt === "csv") {
@@ -67027,18 +68465,20 @@ init_redact();
67027
68465
  import chalk7 from "chalk";
67028
68466
  function registerWhenToUseCommand(program2) {
67029
68467
  const handleError = makeHandleError(program2);
67030
- program2.command("when-to-use <memory_id>").description("Show the when_to_use guidance for a memory").action((memoryId) => {
68468
+ program2.command("when-to-use <memory_id>").description("Show the when_to_use guidance for a memory").action(async (memoryId) => {
67031
68469
  try {
67032
68470
  const globalOpts = program2.opts();
67033
68471
  const resolvedId = resolveMemoryId(memoryId);
67034
68472
  const memory = getMemory(resolvedId);
67035
68473
  if (!memory) {
67036
68474
  if (globalOpts.json) {
67037
- outputJson({ error: `Memory not found: ${memoryId}` });
68475
+ await outputJsonAndExit({ error: `Memory not found: ${memoryId}` }, 1);
67038
68476
  } else {
67039
68477
  console.error(chalk7.red(`Memory not found: ${memoryId}`));
67040
68478
  }
67041
- process.exit(1);
68479
+ if (!globalOpts.json)
68480
+ process.exit(1);
68481
+ return;
67042
68482
  }
67043
68483
  const safe = redactMemoryForOutput(memory);
67044
68484
  const whenToUse = safe.when_to_use ?? null;
@@ -67171,7 +68611,7 @@ init_memories();
67171
68611
  init_helpers();
67172
68612
  import chalk10 from "chalk";
67173
68613
  function registerRemoveCommand(program2) {
67174
- program2.command("remove <nameOrId>").description("Remove/delete a memory by name or ID (alias for memory forget)").option("--agent <id>", "Agent ID").option("--scope <scope>", "Filter by scope (when looking up by key)").action((nameOrId, opts) => {
68614
+ program2.command("remove <nameOrId>").description("Remove/delete a memory by name or ID (alias for memory forget)").option("--agent <id>", "Agent ID").option("--scope <scope>", "Filter by scope (when looking up by key)").action(async (nameOrId, opts) => {
67175
68615
  const globalOpts = program2.opts();
67176
68616
  const agentId = opts.agent || globalOpts.agent;
67177
68617
  let id = isApiMode() ? null : resolvePartialId(getDatabase(), "memories", nameOrId);
@@ -67185,11 +68625,13 @@ function registerRemoveCommand(program2) {
67185
68625
  }
67186
68626
  if (!id) {
67187
68627
  if (globalOpts.json) {
67188
- outputJson({ error: `Memory not found: ${nameOrId}` });
68628
+ await outputJsonAndExit({ error: `Memory not found: ${nameOrId}` }, 1);
67189
68629
  } else {
67190
68630
  console.error(chalk10.red(`Memory not found: ${nameOrId}`));
67191
68631
  }
67192
- process.exit(1);
68632
+ if (!globalOpts.json)
68633
+ process.exit(1);
68634
+ return;
67193
68635
  }
67194
68636
  const deleted = deleteMemory(id);
67195
68637
  if (deleted) {
@@ -67200,11 +68642,13 @@ function registerRemoveCommand(program2) {
67200
68642
  }
67201
68643
  } else {
67202
68644
  if (globalOpts.json) {
67203
- outputJson({ error: `Memory not found: ${nameOrId}` });
68645
+ await outputJsonAndExit({ error: `Memory not found: ${nameOrId}` }, 1);
67204
68646
  } else {
67205
68647
  console.error(chalk10.red(`Memory not found: ${nameOrId}`));
67206
68648
  }
67207
- process.exit(1);
68649
+ if (!globalOpts.json)
68650
+ process.exit(1);
68651
+ return;
67208
68652
  }
67209
68653
  });
67210
68654
  }
@@ -67235,7 +68679,7 @@ var RECALL_EXIT_FUZZY = 2;
67235
68679
  // src/cli/commands/memory-cmd-recall.ts
67236
68680
  function registerRecallCommand(program2) {
67237
68681
  const handleError = makeHandleError(program2);
67238
- program2.command("recall <key>").alias("get").description("Recall a memory by exact key (use --fuzzy to fall back to the nearest match)").option("--scope <scope>", "Scope filter").option("--agent <name>", "Agent filter").option("--project <path>", "Project filter").option("--fuzzy", "If the exact key is absent, return the nearest match instead (exits 2)").action((key, opts) => {
68682
+ program2.command("recall <key>").alias("get").description("Recall a memory by exact key (use --fuzzy to fall back to the nearest match)").option("--scope <scope>", "Scope filter").option("--agent <name>", "Agent filter").option("--project <path>", "Project filter").option("--fuzzy", "If the exact key is absent, return the nearest match instead (exits 2)").action(async (key, opts) => {
67239
68683
  try {
67240
68684
  const globalOpts = program2.opts();
67241
68685
  const agentId = resolveAgentFilter(opts.agent || globalOpts.agent);
@@ -67268,28 +68712,32 @@ function registerRecallCommand(program2) {
67268
68712
  const safeBest = redactSearchResultForOutput(results[0]);
67269
68713
  touchMemory(safeBest.memory.id);
67270
68714
  if (globalOpts.json) {
67271
- outputJson({
68715
+ await outputJsonAndExit({
67272
68716
  fuzzy_match: true,
67273
68717
  requested_key: key,
67274
68718
  returned_key: safeBest.memory.key,
67275
68719
  score: safeBest.score,
67276
68720
  match_type: safeBest.match_type,
67277
68721
  memory: safeBest.memory
67278
- });
68722
+ }, RECALL_EXIT_FUZZY);
67279
68723
  } else {
67280
68724
  console.error(chalk11.yellow(`No memory with key "${key}". Showing the nearest match "${safeBest.memory.key}" ` + `(score: ${safeBest.score.toFixed(2)}, match: ${safeBest.match_type}) \u2014 this is a DIFFERENT record.`));
67281
68725
  console.log(formatMemoryDetail(safeBest.memory));
67282
68726
  }
67283
- process.exit(RECALL_EXIT_FUZZY);
68727
+ if (!globalOpts.json)
68728
+ process.exit(RECALL_EXIT_FUZZY);
68729
+ return;
67284
68730
  }
67285
68731
  }
67286
68732
  const message = opts.fuzzy ? `No memory found for key: ${key}` : `No memory found for key: ${key} (exact match; pass --fuzzy to return the nearest record instead)`;
67287
68733
  if (globalOpts.json) {
67288
- outputJson({ error: message, requested_key: key });
68734
+ await outputJsonAndExit({ error: message, requested_key: key }, RECALL_EXIT_NOT_FOUND);
67289
68735
  } else {
67290
68736
  console.error(chalk11.yellow(message));
67291
68737
  }
67292
- process.exit(RECALL_EXIT_NOT_FOUND);
68738
+ if (!globalOpts.json)
68739
+ process.exit(RECALL_EXIT_NOT_FOUND);
68740
+ return;
67293
68741
  } catch (e) {
67294
68742
  handleError(e);
67295
68743
  }
@@ -67303,16 +68751,164 @@ init_redact();
67303
68751
  init_helpers();
67304
68752
  import chalk12 from "chalk";
67305
68753
  import { resolve as resolve8 } from "path";
68754
+ var STRUCTURED_PAGE_MAX_ROWS2 = 1000;
68755
+ var STRUCTURED_ALL_MAX_ROWS2 = 1e5;
68756
+ var STRUCTURED_DEFAULT_MAX_BYTES2 = 32 * 1024;
68757
+ var STRUCTURED_FULL_MAX_BYTES2 = 64 * 1024;
68758
+ var STRUCTURED_ALL_MAX_BYTES2 = 64 * 1024 * 1024;
68759
+ var STRUCTURED_MIN_MAX_BYTES2 = 1024;
68760
+ function structuredMaxBytes2(value, opts) {
68761
+ const fallback = opts.all ? STRUCTURED_ALL_MAX_BYTES2 : opts.detail === "full" ? STRUCTURED_FULL_MAX_BYTES2 : STRUCTURED_DEFAULT_MAX_BYTES2;
68762
+ if (value === undefined)
68763
+ return fallback;
68764
+ const parsed = typeof value === "number" ? value : Number(value);
68765
+ if (!Number.isInteger(parsed) || parsed < STRUCTURED_MIN_MAX_BYTES2 || parsed > STRUCTURED_ALL_MAX_BYTES2) {
68766
+ throw new Error(`--max-bytes must be an integer from ${STRUCTURED_MIN_MAX_BYTES2} to ${STRUCTURED_ALL_MAX_BYTES2}`);
68767
+ }
68768
+ return parsed;
68769
+ }
68770
+ function compactMemoryForStructuredOutput(memory, opts = {}) {
68771
+ const value = truncateText(memory.value, 160);
68772
+ const summary = memory.summary ? truncateText(memory.summary, 160) : null;
68773
+ return {
68774
+ id: memory.id,
68775
+ key: memory.key,
68776
+ value,
68777
+ ...summary ? { summary } : {},
68778
+ scope: memory.scope,
68779
+ category: memory.category,
68780
+ importance: memory.importance,
68781
+ status: memory.status,
68782
+ pinned: memory.pinned,
68783
+ ...Array.isArray(memory.tags) && memory.tags.length ? { tags: memory.tags.slice(0, 10) } : {},
68784
+ ...memory.agent_id ? { agent_id: memory.agent_id } : {},
68785
+ ...memory.project_id ? { project_id: memory.project_id } : {},
68786
+ ...memory.session_id ? { session_id: memory.session_id } : {},
68787
+ ...opts.history && memory.accessed_at ? { accessed_at: memory.accessed_at } : {},
68788
+ updated_at: memory.updated_at
68789
+ };
68790
+ }
68791
+ function withResponseBytes(envelope) {
68792
+ for (let attempt = 0;attempt < 8; attempt += 1) {
68793
+ const bytes = Buffer.byteLength(`${JSON.stringify(envelope)}
68794
+ `);
68795
+ if (bytes === envelope._meta.response_bytes)
68796
+ return envelope;
68797
+ envelope._meta.response_bytes = bytes;
68798
+ }
68799
+ return envelope;
68800
+ }
68801
+ function makeStructuredEnvelope(args) {
68802
+ const hasMore = args.byteTruncated || args.sourceHasMore;
68803
+ const complete = args.offset === 0 && !hasMore;
68804
+ const nextCursor = hasMore ? args.offset + args.memories.length : null;
68805
+ const truncationReason = args.byteTruncated ? "max_bytes" : args.sourceHasMore ? "limit" : args.offset > 0 ? "cursor" : null;
68806
+ return withResponseBytes({
68807
+ memories: args.memories,
68808
+ _meta: {
68809
+ receipt: args.receipt,
68810
+ count: args.memories.length,
68811
+ limit: args.all ? null : args.limit,
68812
+ offset: args.offset,
68813
+ next_cursor: nextCursor,
68814
+ has_more: hasMore,
68815
+ complete,
68816
+ all: args.all,
68817
+ detail: args.detail,
68818
+ max_rows: args.all ? STRUCTURED_ALL_MAX_ROWS2 : STRUCTURED_PAGE_MAX_ROWS2,
68819
+ max_bytes: args.maxBytes,
68820
+ response_bytes: 0,
68821
+ truncated: !complete,
68822
+ truncation_reason: truncationReason,
68823
+ omitted_from_page: args.omittedFromPage,
68824
+ next_arguments: nextCursor === null ? null : {
68825
+ cursor: nextCursor,
68826
+ limit: args.limit,
68827
+ agent_json: true,
68828
+ ...args.detail === "full" ? { full: true } : {},
68829
+ max_bytes: args.maxBytes
68830
+ },
68831
+ continuation_scope: "unchanged_snapshot"
68832
+ }
68833
+ });
68834
+ }
68835
+ function structuredMemoryOutput(args) {
68836
+ const projected = args.detail === "full" ? args.memories : args.memories.map((memory) => compactMemoryForStructuredOutput(memory, { history: args.history }));
68837
+ const completeEnvelope = makeStructuredEnvelope({
68838
+ memories: projected,
68839
+ receipt: args.receipt,
68840
+ offset: args.offset,
68841
+ limit: args.limit,
68842
+ sourceHasMore: args.sourceHasMore,
68843
+ all: args.all,
68844
+ detail: args.detail,
68845
+ maxBytes: args.maxBytes,
68846
+ byteTruncated: false,
68847
+ omittedFromPage: 0
68848
+ });
68849
+ const completeText = `${JSON.stringify(completeEnvelope)}
68850
+ `;
68851
+ if (Buffer.byteLength(completeText) <= args.maxBytes)
68852
+ return completeText;
68853
+ if (args.all) {
68854
+ throw new Error(`Exhaustive structured output exceeds the hard safety limit of ${args.maxBytes} bytes; use paginated JSON output instead`);
68855
+ }
68856
+ for (let count = projected.length - 1;count >= 0; count -= 1) {
68857
+ const envelope = makeStructuredEnvelope({
68858
+ memories: projected.slice(0, count),
68859
+ receipt: args.receipt,
68860
+ offset: args.offset,
68861
+ limit: args.limit,
68862
+ sourceHasMore: true,
68863
+ all: false,
68864
+ detail: args.detail,
68865
+ maxBytes: args.maxBytes,
68866
+ byteTruncated: true,
68867
+ omittedFromPage: projected.length - count
68868
+ });
68869
+ const text = `${JSON.stringify(envelope)}
68870
+ `;
68871
+ if (Buffer.byteLength(text) > args.maxBytes)
68872
+ continue;
68873
+ if (count === 0 && projected.length > 0) {
68874
+ throw new Error(`One structured memory row exceeds --max-bytes=${args.maxBytes}; use compact detail, a larger --max-bytes value, or mementos show <id>`);
68875
+ }
68876
+ return text;
68877
+ }
68878
+ throw new Error(`Structured output metadata exceeds --max-bytes=${args.maxBytes}`);
68879
+ }
68880
+ function assertReceiptFlags(opts, receiptMode, requestedFormat) {
68881
+ if (!receiptMode && (opts.all || opts.full || opts.maxBytes !== undefined)) {
68882
+ throw new Error("--all, --full, and --max-bytes require JSON receipt mode (--json, --format json, or --agent-json)");
68883
+ }
68884
+ if (receiptMode && requestedFormat !== undefined && requestedFormat !== "json") {
68885
+ throw new Error("--agent-json cannot be combined with a non-JSON --format");
68886
+ }
68887
+ if (opts.all && opts.limit !== undefined) {
68888
+ throw new Error("--all cannot be combined with --limit");
68889
+ }
68890
+ }
67306
68891
  function registerListCommand(program2) {
67307
68892
  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) => {
68893
+ 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_ROWS2})`, 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_ROWS2} 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_BYTES2})`, parseInt).action((opts) => {
67309
68894
  try {
67310
68895
  const globalOpts = program2.opts();
68896
+ const requestedFormat = opts.format ?? globalOpts.format;
67311
68897
  const fmt = getOutputFormat(program2, opts.format);
68898
+ const receiptMode = Boolean(opts.agentJson || fmt === "json");
67312
68899
  const isStructured = fmt === "json" || fmt === "csv" || fmt === "yaml";
68900
+ assertReceiptFlags(opts, receiptMode, requestedFormat);
67313
68901
  const requestedLimit = opts.limit;
67314
- const limit = requestedLimit === undefined ? isStructured ? undefined : DEFAULT_COMPACT_LIMIT : positiveIntOrDefault(requestedLimit, isStructured ? 50 : DEFAULT_COMPACT_LIMIT);
68902
+ const all = Boolean(opts.all);
68903
+ const detail = opts.full ? "full" : "compact";
68904
+ const limit = requestedLimit === undefined ? receiptMode ? DEFAULT_COMPACT_LIMIT : isStructured ? undefined : DEFAULT_COMPACT_LIMIT : positiveIntOrDefault(requestedLimit, receiptMode ? DEFAULT_COMPACT_LIMIT : isStructured ? 50 : DEFAULT_COMPACT_LIMIT);
68905
+ if (receiptMode && limit !== undefined && limit > STRUCTURED_PAGE_MAX_ROWS2) {
68906
+ throw new Error(`--limit cannot exceed the agent JSON page ceiling of ${STRUCTURED_PAGE_MAX_ROWS2}; use --all for a bounded exhaustive read`);
68907
+ }
67315
68908
  const offset = cursorOrOffset(opts.cursor, opts.offset) ?? 0;
68909
+ if (all && offset !== 0) {
68910
+ throw new Error("--all requires --cursor/--offset 0");
68911
+ }
67316
68912
  const agentId = resolveAgentFilter(opts.agent || globalOpts.agent);
67317
68913
  const projectPath = opts.project || globalOpts.project;
67318
68914
  let projectId;
@@ -67334,6 +68930,7 @@ function registerListCommand(program2) {
67334
68930
  status: opts.status,
67335
68931
  session_id: opts.session || globalOpts.session
67336
68932
  };
68933
+ const target = all ? STRUCTURED_ALL_MAX_ROWS2 : limit;
67337
68934
  const { rows: collected, hasMore } = collectPagedRows((cursor, pageLimit) => {
67338
68935
  const page = listMemoriesPage({
67339
68936
  ...filter,
@@ -67345,9 +68942,25 @@ function registerListCommand(program2) {
67345
68942
  has_more: page.has_more,
67346
68943
  next_cursor: page.next_cursor
67347
68944
  };
67348
- }, limit, offset);
67349
- const memories = hasMore && limit !== undefined ? collected.slice(0, limit) : collected;
68945
+ }, target, offset);
68946
+ if (all && hasMore) {
68947
+ throw new Error(`Exhaustive structured output exceeds the hard safety limit of ${STRUCTURED_ALL_MAX_ROWS2} rows; use paginated JSON output instead`);
68948
+ }
68949
+ const memories = target === undefined ? collected : collected.slice(0, target);
67350
68950
  const sanitized = memories.map(redactMemoryForOutput);
68951
+ if (receiptMode) {
68952
+ process.stdout.write(structuredMemoryOutput({
68953
+ memories: sanitized,
68954
+ receipt: "mementos.list.page.v1",
68955
+ offset,
68956
+ limit: limit ?? DEFAULT_COMPACT_LIMIT,
68957
+ sourceHasMore: hasMore,
68958
+ all,
68959
+ detail,
68960
+ maxBytes: structuredMaxBytes2(opts.maxBytes, { all, detail })
68961
+ }));
68962
+ return;
68963
+ }
67351
68964
  if (fmt === "json") {
67352
68965
  outputJson(sanitized);
67353
68966
  return;
@@ -67381,7 +68994,7 @@ function registerListCommand(program2) {
67381
68994
  offset,
67382
68995
  hasMore,
67383
68996
  command: "mementos list",
67384
- detailHint: "use mementos show <id> for full details or --json for full objects"
68997
+ detailHint: "use mementos show <id> for full details; JSON output is a bounded receipt, with --full/--all as explicit compatibility escapes"
67385
68998
  });
67386
68999
  } catch (e) {
67387
69000
  handleError(e);
@@ -67808,13 +69421,33 @@ init_helpers();
67808
69421
  import chalk16 from "chalk";
67809
69422
  function registerHistoryCommand(program2) {
67810
69423
  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) => {
69424
+ program2.command("history").description("List memories sorted by most recently accessed").option("--limit <n>", `Max results (agent JSON page max: ${STRUCTURED_PAGE_MAX_ROWS2})`, 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_ROWS2} 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_BYTES2})`, parseInt).action((opts) => {
67812
69425
  try {
67813
69426
  const globalOpts = program2.opts();
67814
- const isJson = Boolean(globalOpts.json);
69427
+ const format = getOutputFormat(program2);
69428
+ const receiptMode = Boolean(opts.agentJson);
69429
+ const isJson = format === "json";
69430
+ if (!receiptMode && (opts.all || opts.full || opts.maxBytes !== undefined)) {
69431
+ throw new Error("--all, --full, and --max-bytes require --agent-json receipt mode");
69432
+ }
69433
+ if (receiptMode && globalOpts.format !== undefined && globalOpts.format !== "json") {
69434
+ throw new Error("--agent-json cannot be combined with a non-JSON --format");
69435
+ }
69436
+ if (opts.all && opts.limit !== undefined) {
69437
+ throw new Error("--all cannot be combined with --limit");
69438
+ }
67815
69439
  const requestedLimit = opts.limit;
67816
- const limit = requestedLimit === undefined ? isJson ? undefined : DEFAULT_SEARCH_LIMIT : positiveIntOrDefault(requestedLimit, isJson ? 20 : DEFAULT_SEARCH_LIMIT);
69440
+ const all = Boolean(opts.all);
69441
+ const detail = opts.full ? "full" : "compact";
69442
+ const limit = requestedLimit === undefined ? receiptMode ? DEFAULT_SEARCH_LIMIT : isJson ? undefined : DEFAULT_SEARCH_LIMIT : positiveIntOrDefault(requestedLimit, receiptMode ? DEFAULT_SEARCH_LIMIT : isJson ? 20 : DEFAULT_SEARCH_LIMIT);
69443
+ if (receiptMode && limit !== undefined && limit > STRUCTURED_PAGE_MAX_ROWS2) {
69444
+ throw new Error(`--limit cannot exceed the agent JSON page ceiling of ${STRUCTURED_PAGE_MAX_ROWS2}; use --all for a bounded exhaustive read`);
69445
+ }
67817
69446
  const offset = cursorOrOffset(opts.cursor, opts.offset) ?? 0;
69447
+ if (all && offset !== 0) {
69448
+ throw new Error("--all requires --cursor/--offset 0");
69449
+ }
69450
+ const target = all ? STRUCTURED_ALL_MAX_ROWS2 : limit;
67818
69451
  const { rows: collected, hasMore } = collectPagedRows((cursor, pageLimit) => {
67819
69452
  const page = listMemoryHistoryPage({ limit: pageLimit, offset: cursor });
67820
69453
  return {
@@ -67822,10 +69455,27 @@ function registerHistoryCommand(program2) {
67822
69455
  has_more: page.has_more,
67823
69456
  next_cursor: page.next_cursor
67824
69457
  };
67825
- }, limit, offset);
67826
- const memories = hasMore && limit !== undefined ? collected.slice(0, limit) : collected;
69458
+ }, target, offset);
69459
+ if (all && hasMore) {
69460
+ throw new Error(`Exhaustive structured output exceeds the hard safety limit of ${STRUCTURED_ALL_MAX_ROWS2} rows; use paginated JSON output instead`);
69461
+ }
69462
+ const memories = target === undefined ? collected : collected.slice(0, target);
67827
69463
  const sanitized = memories.map(redactMemoryForOutput);
67828
- if (globalOpts.json) {
69464
+ if (receiptMode) {
69465
+ process.stdout.write(structuredMemoryOutput({
69466
+ memories: sanitized,
69467
+ receipt: "mementos.history.page.v1",
69468
+ offset,
69469
+ limit: limit ?? DEFAULT_SEARCH_LIMIT,
69470
+ sourceHasMore: hasMore,
69471
+ all,
69472
+ detail,
69473
+ maxBytes: structuredMaxBytes2(opts.maxBytes, { all, detail }),
69474
+ history: true
69475
+ }));
69476
+ return;
69477
+ }
69478
+ if (isJson) {
67829
69479
  outputJson(sanitized);
67830
69480
  return;
67831
69481
  }
@@ -67848,7 +69498,7 @@ function registerHistoryCommand(program2) {
67848
69498
  offset,
67849
69499
  hasMore,
67850
69500
  command: "mementos history",
67851
- detailHint: "use mementos show <id> for full details or --json for full objects"
69501
+ detailHint: "use mementos show <id> for full details, --json for the compatible full array, or --agent-json for a bounded receipt"
67852
69502
  });
67853
69503
  } catch (e) {
67854
69504
  handleError(e);
@@ -67863,13 +69513,16 @@ import chalk17 from "chalk";
67863
69513
  import { resolve as resolve11 } from "path";
67864
69514
 
67865
69515
  // src/lib/machine-visibility.ts
69516
+ init_api_mode();
67866
69517
  function resolveVisibleMachineId(machineId, db) {
67867
69518
  if (machineId !== undefined) {
67868
69519
  return machineId;
67869
69520
  }
67870
69521
  try {
67871
69522
  return getCurrentMachineId(db);
67872
- } catch {
69523
+ } catch (error) {
69524
+ if (!db && isApiMode())
69525
+ throw error;
67873
69526
  return null;
67874
69527
  }
67875
69528
  }
@@ -67900,13 +69553,16 @@ function registerContextCommand(program2) {
67900
69553
  const categoriesRaw = opts.categories;
67901
69554
  const categories = categoriesRaw ? categoriesRaw.split(",").map((c) => c.trim()) : undefined;
67902
69555
  const agentId = resolveAgentFilter(opts.agent || globalOpts.agent);
67903
- const projectPath = opts.project || globalOpts.project;
69556
+ const projectPath = opts.project ?? globalOpts.project;
67904
69557
  const visibleMachineId = resolveVisibleMachineId(opts.machine);
67905
69558
  let projectId;
67906
- if (projectPath) {
69559
+ if (projectPath !== undefined) {
69560
+ if (!projectPath.trim())
69561
+ throw new Error("Project not found: empty project path");
67907
69562
  const project = getProject(resolve11(projectPath));
67908
- if (project)
67909
- projectId = project.id;
69563
+ if (!project)
69564
+ throw new Error(`Project not found: ${projectPath}`);
69565
+ projectId = project.id;
67910
69566
  }
67911
69567
  let memories;
67912
69568
  if (query) {
@@ -67954,6 +69610,7 @@ function registerContextCommand(program2) {
67954
69610
  ...baseFilter,
67955
69611
  scope: "private",
67956
69612
  agent_id: agentId,
69613
+ project_id: projectId,
67957
69614
  ...visibleToMachineFilter(visibleMachineId)
67958
69615
  }));
67959
69616
  }
@@ -68022,7 +69679,7 @@ init_helpers();
68022
69679
  import { resolve as resolve12 } from "path";
68023
69680
  function registerExportCommand(program2) {
68024
69681
  const handleError = makeHandleError(program2);
68025
- program2.command("export").description("Export memories as JSON").option("--scope <scope>", "Scope filter").option("-c, --category <cat>", "Category filter").option("--agent <name>", "Agent filter").option("--project <path>", "Project filter").action((opts) => {
69682
+ program2.command("export").description("Export a truthful full-detail JSON page; use --all for the exhaustive legacy array").option("--scope <scope>", "Scope filter").option("-c, --category <cat>", "Category filter").option("--agent <name>", "Agent filter").option("--project <path>", "Project filter").option("--limit <n>", "Page size (default: 100, maximum: 1000)", parseInt).option("--cursor <n>", "Cursor offset for the next page", parseInt).option("--offset <n>", "Offset for pagination", parseInt).option("--max-bytes <n>", "Paginated response byte ceiling (default: 65536)", parseInt).option("--all", "Exhaust the complete query and emit the legacy full JSON array").action((opts) => {
68026
69683
  try {
68027
69684
  const globalOpts = program2.opts();
68028
69685
  const agentId = resolveAgentFilter(opts.agent || globalOpts.agent);
@@ -68039,9 +69696,43 @@ function registerExportCommand(program2) {
68039
69696
  agent_id: agentId,
68040
69697
  project_id: projectId
68041
69698
  };
68042
- const memories = listMemoriesBounded(filter, 1e4).rows;
68043
- const sanitized = memories.map(redactMemoryForOutput);
68044
- outputJson(sanitized);
69699
+ const exhaustive = Boolean(opts.all);
69700
+ const offset = cursorOrOffset(opts.cursor, opts.offset) ?? 0;
69701
+ if (exhaustive && opts.limit !== undefined) {
69702
+ throw new Error("--all cannot be combined with --limit");
69703
+ }
69704
+ if (exhaustive && offset !== 0) {
69705
+ throw new Error("--all requires --cursor/--offset 0");
69706
+ }
69707
+ if (exhaustive && opts.maxBytes !== undefined) {
69708
+ throw new Error("--max-bytes applies to paginated exports; exhaustive --all is an explicit unbounded compatibility escape");
69709
+ }
69710
+ if (exhaustive) {
69711
+ const complete = listMemoriesBounded(filter, undefined).rows.map(redactMemoryForOutput);
69712
+ outputJson(complete);
69713
+ return;
69714
+ }
69715
+ const limit = structuredPageLimit(opts.limit, 100);
69716
+ const page = listMemoriesBounded({ ...filter, offset }, limit);
69717
+ const sanitized = page.rows.map(redactMemoryForOutput);
69718
+ process.stdout.write(structuredCollectionOutput({
69719
+ collection: "memories",
69720
+ receipt: "mementos.export.page.v1",
69721
+ items: sanitized,
69722
+ offset,
69723
+ limit,
69724
+ sourceHasMore: page.has_more,
69725
+ all: false,
69726
+ detail: "full",
69727
+ maxBytes: structuredMaxBytes(opts.maxBytes, { all: false, detail: "full" }),
69728
+ nextArguments: {
69729
+ ...opts.scope ? { scope: opts.scope } : {},
69730
+ ...opts.category ? { category: opts.category } : {},
69731
+ ...agentId ? { agent: agentId } : {},
69732
+ ...projectPath ? { project: projectPath } : {}
69733
+ },
69734
+ includeDetailInNextArguments: false
69735
+ }));
68045
69736
  } catch (e) {
68046
69737
  handleError(e);
68047
69738
  }
@@ -68090,247 +69781,13 @@ function registerImportCommand(program2) {
68090
69781
  }
68091
69782
 
68092
69783
  // src/cli/commands/io-clean.ts
69784
+ init_config();
68093
69785
  import chalk19 from "chalk";
68094
69786
 
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
69787
  // src/lib/retention.ts
68332
69788
  init_database();
68333
69789
  init_memories();
69790
+ init_config();
68334
69791
  function enforceQuotas(config, db) {
68335
69792
  const d = db || getDatabase();
68336
69793
  let totalEvicted = 0;
@@ -68438,11 +69895,108 @@ function registerCleanCommand(program2) {
68438
69895
 
68439
69896
  // src/cli/commands/io-backup.ts
68440
69897
  init_database();
69898
+ init_api_mode();
69899
+ init_memories();
68441
69900
  init_paths();
68442
69901
  init_helpers();
68443
69902
  import chalk20 from "chalk";
68444
69903
  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";
69904
+ import { existsSync as existsSync7, statSync, copyFileSync, mkdirSync as mkdirSync4, readdirSync as readdirSync2, unlinkSync as unlinkSync3 } from "fs";
69905
+ function writeMemoriesBackupFile(dest, rows) {
69906
+ const { Database: Database3 } = __require("bun:sqlite");
69907
+ if (existsSync7(dest)) {
69908
+ unlinkSync3(dest);
69909
+ }
69910
+ const backupDb = new Database3(dest);
69911
+ try {
69912
+ backupDb.run(`
69913
+ CREATE TABLE IF NOT EXISTS _backup_carrier (
69914
+ format INTEGER NOT NULL
69915
+ )
69916
+ `);
69917
+ backupDb.run(`INSERT INTO _backup_carrier (format) VALUES (1)`);
69918
+ backupDb.run(`
69919
+ CREATE TABLE IF NOT EXISTS memories (
69920
+ id TEXT PRIMARY KEY,
69921
+ key TEXT NOT NULL,
69922
+ value TEXT NOT NULL,
69923
+ category TEXT,
69924
+ scope TEXT,
69925
+ summary TEXT,
69926
+ tags TEXT,
69927
+ importance INTEGER,
69928
+ source TEXT,
69929
+ status TEXT,
69930
+ pinned INTEGER,
69931
+ agent_id TEXT,
69932
+ project_id TEXT,
69933
+ session_id TEXT,
69934
+ machine_id TEXT,
69935
+ namespace TEXT,
69936
+ created_by_agent TEXT,
69937
+ when_to_use TEXT,
69938
+ sequence_group TEXT,
69939
+ sequence_order INTEGER,
69940
+ metadata TEXT,
69941
+ access_count INTEGER,
69942
+ version INTEGER,
69943
+ expires_at TEXT,
69944
+ valid_from TEXT,
69945
+ valid_until TEXT,
69946
+ ingested_at TEXT,
69947
+ created_at TEXT,
69948
+ updated_at TEXT,
69949
+ accessed_at TEXT
69950
+ )
69951
+ `);
69952
+ 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)
69953
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
69954
+ const bind = (v) => {
69955
+ if (v === undefined || v === null)
69956
+ return null;
69957
+ return typeof v === "string" || typeof v === "number" ? v : JSON.stringify(v);
69958
+ };
69959
+ const boolBind = (v) => v === undefined || v === null ? null : v ? 1 : 0;
69960
+ for (const row of rows) {
69961
+ const params = [
69962
+ bind(row["id"]),
69963
+ bind(row["key"]),
69964
+ bind(row["value"]),
69965
+ bind(row["category"]),
69966
+ bind(row["scope"]),
69967
+ bind(row["summary"]),
69968
+ Array.isArray(row["tags"]) ? JSON.stringify(row["tags"]) : bind(row["tags"]),
69969
+ bind(row["importance"]),
69970
+ bind(row["source"]),
69971
+ bind(row["status"]),
69972
+ boolBind(row["pinned"]),
69973
+ bind(row["agent_id"]),
69974
+ bind(row["project_id"]),
69975
+ bind(row["session_id"]),
69976
+ bind(row["machine_id"]),
69977
+ bind(row["namespace"]),
69978
+ bind(row["created_by_agent"]),
69979
+ bind(row["when_to_use"]),
69980
+ bind(row["sequence_group"]),
69981
+ bind(row["sequence_order"]),
69982
+ bind(row["metadata"]),
69983
+ bind(row["access_count"]),
69984
+ bind(row["version"]),
69985
+ bind(row["expires_at"]),
69986
+ bind(row["valid_from"]),
69987
+ bind(row["valid_until"]),
69988
+ bind(row["ingested_at"]),
69989
+ bind(row["created_at"]),
69990
+ bind(row["updated_at"]),
69991
+ bind(row["accessed_at"])
69992
+ ];
69993
+ insert.run(...params);
69994
+ }
69995
+ return rows.length;
69996
+ } finally {
69997
+ backupDb.close();
69998
+ }
69999
+ }
68446
70000
  function registerBackupCommand(program2) {
68447
70001
  const handleError = makeHandleError(program2);
68448
70002
  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 +70045,6 @@ function registerBackupCommand(program2) {
68491
70045
  }
68492
70046
  return;
68493
70047
  }
68494
- const dbPath = getDbPath();
68495
- if (!existsSync7(dbPath)) {
68496
- console.error(chalk20.red(`Database not found at ${dbPath}`));
68497
- process.exit(1);
68498
- }
68499
70048
  let dest;
68500
70049
  if (targetPath) {
68501
70050
  dest = resolve15(targetPath);
@@ -68511,6 +70060,26 @@ function registerBackupCommand(program2) {
68511
70060
  if (!existsSync7(destDir)) {
68512
70061
  mkdirSync4(destDir, { recursive: true });
68513
70062
  }
70063
+ if (isApiMode()) {
70064
+ const { rows } = listMemoriesBounded({}, undefined);
70065
+ const count = writeMemoriesBackupFile(dest, rows);
70066
+ const st2 = statSync(dest);
70067
+ const sizeMB2 = (st2.size / (1024 * 1024)).toFixed(1);
70068
+ const sizeStr2 = st2.size >= 1024 * 1024 ? `${sizeMB2} MB` : `${(st2.size / 1024).toFixed(1)} KB`;
70069
+ if (globalOpts.json) {
70070
+ outputJson({ backed_up_to: dest, size: st2.size, source: "cloud-api", memories: count });
70071
+ return;
70072
+ }
70073
+ console.log(`Backed up to: ${chalk20.green(dest)} (size: ${sizeStr2})`);
70074
+ console.log(` Source: hosted store (cloud-api)`);
70075
+ console.log(` Memories: ${chalk20.cyan(String(count))}`);
70076
+ return;
70077
+ }
70078
+ const dbPath = getDbPath();
70079
+ if (!existsSync7(dbPath)) {
70080
+ console.error(chalk20.red(`Database not found at ${dbPath}`));
70081
+ process.exit(1);
70082
+ }
68514
70083
  copyFileSync(dbPath, dest);
68515
70084
  const st = statSync(dest);
68516
70085
  const sizeMB = (st.size / (1024 * 1024)).toFixed(1);
@@ -68545,9 +70114,31 @@ function readMemoriesFromBackup(source) {
68545
70114
  backupDb.close();
68546
70115
  }
68547
70116
  }
70117
+ function inspectLocalRestoreSource(source) {
70118
+ const { Database: Database3 } = __require("bun:sqlite");
70119
+ let backupDb;
70120
+ try {
70121
+ backupDb = new Database3(source, { readonly: true });
70122
+ } catch (error) {
70123
+ throw new Error(`Backup file is not a readable SQLite database: ${source} ` + `(${error instanceof Error ? error.message : String(error)})`);
70124
+ }
70125
+ try {
70126
+ const tables = backupDb.query("SELECT name FROM sqlite_master WHERE type = 'table'").all().map((t) => t.name);
70127
+ if (tables.includes("_backup_carrier")) {
70128
+ 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\`.`);
70129
+ }
70130
+ if (!tables.includes("memories")) {
70131
+ throw new Error(`Backup file has no mementos \`memories\` table: ${source} \u2014 it is not a mementos backup.`);
70132
+ }
70133
+ const row = backupDb.query("SELECT COUNT(*) as count FROM memories").get();
70134
+ return { memories: row?.count ?? 0 };
70135
+ } finally {
70136
+ backupDb.close();
70137
+ }
70138
+ }
68548
70139
  function registerRestoreCommand(program2) {
68549
70140
  const handleError = makeHandleError(program2);
68550
- 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) => {
70141
+ 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(async (filePath, opts) => {
68551
70142
  try {
68552
70143
  const globalOpts = program2.opts();
68553
70144
  const backupsDir = join11(getDataRoot(), "backups");
@@ -68608,7 +70199,7 @@ function registerRestoreCommand(program2) {
68608
70199
  if (result.rejected > 0) {
68609
70200
  const msg = `${result.rejected} of ${result.total} memories were rejected and did not persist. See errors.`;
68610
70201
  if (globalOpts.json) {
68611
- outputJson({
70202
+ await outputJsonAndExit({
68612
70203
  action: "restore",
68613
70204
  status: "failed",
68614
70205
  source,
@@ -68618,11 +70209,12 @@ function registerRestoreCommand(program2) {
68618
70209
  rejected: result.rejected,
68619
70210
  total: result.total,
68620
70211
  error: msg
68621
- });
70212
+ }, 1);
68622
70213
  } else {
68623
70214
  console.error(chalk21.red(msg));
68624
70215
  }
68625
- process.exit(1);
70216
+ if (!globalOpts.json)
70217
+ process.exit(1);
68626
70218
  }
68627
70219
  if (globalOpts.json) {
68628
70220
  outputJson({
@@ -68655,14 +70247,7 @@ function registerRestoreCommand(program2) {
68655
70247
  currentCount = row?.count ?? 0;
68656
70248
  } catch {}
68657
70249
  }
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 {}
70250
+ const backupCount = inspectLocalRestoreSource(source).memories;
68666
70251
  if (!opts.force) {
68667
70252
  if (globalOpts.json) {
68668
70253
  outputJson({
@@ -68784,20 +70369,40 @@ function registerAgentCommands(program2) {
68784
70369
  handleError(e);
68785
70370
  }
68786
70371
  });
68787
- program2.command("agents").description("List all registered agents").option("--limit <n>", "Max results (compact default: 20)", parseInt).option("--cursor <n>", "Cursor offset for the next page", parseInt).option("--offset <n>", "Offset for pagination", parseInt).action((opts) => {
70372
+ program2.command("agents").description("List all registered agents").option("--limit <n>", "Max results (compact default: 20)", parseInt).option("--cursor <n>", "Cursor offset for the next page", parseInt).option("--offset <n>", "Offset for pagination", parseInt).option("--all", `Exhaust all agents in one explicit JSON receipt (hard max: ${STRUCTURED_ALL_MAX_ROWS})`).option("--full", "Return full agent objects instead of compact projections").option("--max-bytes <n>", "JSON response byte ceiling", parseInt).action((opts) => {
68788
70373
  try {
68789
70374
  const globalOpts = program2.opts();
68790
- const limit = positiveIntOrDefault(opts.limit, DEFAULT_COMPACT_LIMIT);
70375
+ const jsonMode = Boolean(globalOpts.json);
70376
+ const all = Boolean(opts.all);
70377
+ const detail = opts.full ? "full" : "compact";
70378
+ if (!jsonMode && (all || opts.full || opts.maxBytes !== undefined)) {
70379
+ throw new Error("--all, --full, and --max-bytes require --json");
70380
+ }
70381
+ if (all && opts.limit !== undefined)
70382
+ throw new Error("--all cannot be combined with --limit");
70383
+ const limit = jsonMode ? structuredPageLimit(opts.limit, DEFAULT_COMPACT_LIMIT) : positiveIntOrDefault(opts.limit, DEFAULT_COMPACT_LIMIT);
68791
70384
  const offset = cursorOrOffset(opts.cursor, opts.offset) ?? 0;
68792
- const explicitPagination = opts.limit !== undefined || opts.cursor !== undefined || opts.offset !== undefined;
68793
- const agents = listAgents({
68794
- limit: globalOpts.json ? explicitPagination ? limit : undefined : limit + 1,
68795
- offset
68796
- });
68797
- const hasMore = !globalOpts.json && agents.length > limit;
70385
+ if (all && offset !== 0)
70386
+ throw new Error("--all requires --cursor/--offset 0");
70387
+ const agents = all ? listAgents() : listAgents({ limit: limit + 1, offset });
70388
+ if (all && agents.length > STRUCTURED_ALL_MAX_ROWS) {
70389
+ throw new Error(`Exhaustive agent output exceeds the hard safety limit of ${STRUCTURED_ALL_MAX_ROWS} rows; use paginated JSON output`);
70390
+ }
70391
+ const hasMore = !all && agents.length > limit;
68798
70392
  const displayAgents = hasMore ? agents.slice(0, limit) : agents;
68799
- if (globalOpts.json) {
68800
- outputJson(agents);
70393
+ if (jsonMode) {
70394
+ const items = detail === "full" ? displayAgents.map((agent) => ({ ...agent })) : displayAgents.map(compactAgent);
70395
+ process.stdout.write(structuredCollectionOutput({
70396
+ collection: "agents",
70397
+ receipt: "mementos.agents.page.v1",
70398
+ items,
70399
+ offset,
70400
+ limit,
70401
+ sourceHasMore: hasMore,
70402
+ all,
70403
+ detail,
70404
+ maxBytes: structuredMaxBytes(opts.maxBytes, { all, detail })
70405
+ }));
68801
70406
  return;
68802
70407
  }
68803
70408
  if (displayAgents.length === 0) {
@@ -68821,7 +70426,7 @@ function registerAgentCommands(program2) {
68821
70426
  handleError(e);
68822
70427
  }
68823
70428
  });
68824
- program2.command("agent-update <id>").description("Update an agent's name, description, or role").option("--name <name>", "New agent name").option("-d, --description <text>", "New description").option("-r, --role <role>", "New role").action((id, opts) => {
70429
+ program2.command("agent-update <id>").description("Update an agent's name, description, or role").option("--name <name>", "New agent name").option("-d, --description <text>", "New description").option("-r, --role <role>", "New role").action(async (id, opts) => {
68825
70430
  try {
68826
70431
  const globalOpts = program2.opts();
68827
70432
  const updates = {};
@@ -68833,20 +70438,24 @@ function registerAgentCommands(program2) {
68833
70438
  updates.role = opts.role;
68834
70439
  if (Object.keys(updates).length === 0) {
68835
70440
  if (globalOpts.json) {
68836
- outputJson({ error: "No updates provided. Use --name, --description, or --role." });
70441
+ await outputJsonAndExit({ error: "No updates provided. Use --name, --description, or --role." }, 1);
68837
70442
  } else {
68838
70443
  console.error(chalk22.red("No updates provided. Use --name, --description, or --role."));
68839
70444
  }
68840
- process.exit(1);
70445
+ if (!globalOpts.json)
70446
+ process.exit(1);
70447
+ return;
68841
70448
  }
68842
70449
  const agent = updateAgent(id, updates);
68843
70450
  if (!agent) {
68844
70451
  if (globalOpts.json) {
68845
- outputJson({ error: `Agent not found: ${id}` });
70452
+ await outputJsonAndExit({ error: `Agent not found: ${id}` }, 1);
68846
70453
  } else {
68847
70454
  console.error(chalk22.red(`Agent not found: ${id}`));
68848
70455
  }
68849
- process.exit(1);
70456
+ if (!globalOpts.json)
70457
+ process.exit(1);
70458
+ return;
68850
70459
  }
68851
70460
  if (globalOpts.json) {
68852
70461
  outputJson(agent);
@@ -68934,7 +70543,7 @@ import { resolve as resolve17 } from "path";
68934
70543
  init_helpers();
68935
70544
  function registerProjectCommands(program2) {
68936
70545
  const handleError = makeHandleError(program2);
68937
- program2.command("projects").description("Manage projects").option("--add", "Add a new project").option("--update <id>", "Update a project by its exact stable ID").option("--name <name>", "Project name").option("--path <path>", "Project path").option("--description <text>", "Project description").option("--memory-prefix <prefix>", "Project memory prefix").option("--expected-revision <revision>", "Exact updated_at revision required for compare-and-swap").option("--idempotency-key <key>", "Caller-owned key for one guarded mutation").option("--operation-id <id>", "Operation identifier (defaults to the idempotency key)").option("--step-id <id>", "Step identifier (defaults to mementos_project_update)").option("--dry-run", "Validate and preview the guarded update without writing").option("--rollback-receipt <id>", "Restore the exact before snapshot from an accepted update receipt").option("--limit <n>", "Max results (compact default: 20)", parseInt).option("--cursor <n>", "Cursor offset for the next page", parseInt).option("--offset <n>", "Offset for pagination", parseInt).action((opts) => {
70546
+ program2.command("projects").description("Manage projects").option("--add", "Add a new project").option("--update <id>", "Update a project by its exact stable ID").option("--name <name>", "Project name").option("--path <path>", "Project path").option("--description <text>", "Project description").option("--memory-prefix <prefix>", "Project memory prefix").option("--expected-revision <revision>", "Exact updated_at revision required for compare-and-swap").option("--idempotency-key <key>", "Caller-owned key for one guarded mutation").option("--operation-id <id>", "Operation identifier (defaults to the idempotency key)").option("--step-id <id>", "Step identifier (defaults to mementos_project_update)").option("--dry-run", "Validate and preview the guarded update without writing").option("--rollback-receipt <id>", "Restore the exact before snapshot from an accepted update receipt").option("--limit <n>", "Max results (compact default: 20)", parseInt).option("--cursor <n>", "Cursor offset for the next page", parseInt).option("--offset <n>", "Offset for pagination", parseInt).option("--all", `Exhaust all projects in one explicit JSON receipt (hard max: ${STRUCTURED_ALL_MAX_ROWS})`).option("--full", "Return full project objects instead of compact projections").option("--max-bytes <n>", "JSON response byte ceiling", parseInt).action((opts) => {
68938
70547
  try {
68939
70548
  const globalOpts = program2.opts();
68940
70549
  if (opts.add && opts.update) {
@@ -69015,15 +70624,39 @@ function registerProjectCommands(program2) {
69015
70624
  }
69016
70625
  return;
69017
70626
  }
69018
- const allProjects = listProjects();
69019
- const limit = positiveIntOrDefault(opts.limit, DEFAULT_COMPACT_LIMIT);
70627
+ const jsonMode = Boolean(globalOpts.json);
70628
+ const all = Boolean(opts.all);
70629
+ const detail = opts.full ? "full" : "compact";
70630
+ if (!jsonMode && (all || opts.full || opts.maxBytes !== undefined)) {
70631
+ throw new Error("--all, --full, and --max-bytes require --json");
70632
+ }
70633
+ if (all && opts.limit !== undefined) {
70634
+ throw new Error("--all cannot be combined with --limit");
70635
+ }
70636
+ const limit = jsonMode ? structuredPageLimit(opts.limit, DEFAULT_COMPACT_LIMIT) : positiveIntOrDefault(opts.limit, DEFAULT_COMPACT_LIMIT);
69020
70637
  const offset = cursorOrOffset(opts.cursor, opts.offset) ?? 0;
69021
- const explicitPagination = opts.limit !== undefined || opts.cursor !== undefined || opts.offset !== undefined;
69022
- const projects = globalOpts.json ? explicitPagination ? allProjects.slice(offset, offset + limit) : allProjects : allProjects.slice(offset, offset + limit + 1);
69023
- const hasMore = !globalOpts.json && projects.length > limit;
69024
- const displayProjects = hasMore ? projects.slice(0, limit) : projects;
69025
- if (globalOpts.json) {
69026
- outputJson(projects);
70638
+ if (all && offset !== 0)
70639
+ throw new Error("--all requires --cursor/--offset 0");
70640
+ const allProjects = listProjects();
70641
+ if (all && allProjects.length > STRUCTURED_ALL_MAX_ROWS) {
70642
+ throw new Error(`Exhaustive project output exceeds the hard safety limit of ${STRUCTURED_ALL_MAX_ROWS} rows; use paginated JSON output`);
70643
+ }
70644
+ const page = all ? allProjects : allProjects.slice(offset, offset + limit + 1);
70645
+ const hasMore = !all && page.length > limit;
70646
+ const displayProjects = hasMore ? page.slice(0, limit) : page;
70647
+ if (jsonMode) {
70648
+ const items = detail === "full" ? displayProjects.map((project) => ({ ...project })) : displayProjects.map(compactProject);
70649
+ process.stdout.write(structuredCollectionOutput({
70650
+ collection: "projects",
70651
+ receipt: "mementos.projects.page.v1",
70652
+ items,
70653
+ offset,
70654
+ limit,
70655
+ sourceHasMore: hasMore,
70656
+ all,
70657
+ detail,
70658
+ maxBytes: structuredMaxBytes(opts.maxBytes, { all, detail })
70659
+ }));
69027
70660
  return;
69028
70661
  }
69029
70662
  if (displayProjects.length === 0) {
@@ -69103,14 +70736,17 @@ function registerProjectCommands(program2) {
69103
70736
  const categoriesRaw = opts.categories || "preference,fact,knowledge";
69104
70737
  const categories = categoriesRaw.split(",").map((c) => c.trim());
69105
70738
  const agentId = opts.agent || globalOpts.agent;
69106
- const projectPath = opts.project || globalOpts.project;
70739
+ const projectPath = opts.project ?? globalOpts.project;
69107
70740
  const sessionId = opts.session || globalOpts.session;
69108
70741
  const visibleMachineId = resolveVisibleMachineId(opts.machine);
69109
70742
  let projectId;
69110
- if (projectPath) {
70743
+ if (projectPath !== undefined) {
70744
+ if (!projectPath.trim())
70745
+ throw new Error("Project not found: empty project path");
69111
70746
  const project = getProject(resolve17(projectPath));
69112
- if (project)
69113
- projectId = project.id;
70747
+ if (!project)
70748
+ throw new Error(`Project not found: ${projectPath}`);
70749
+ projectId = project.id;
69114
70750
  }
69115
70751
  const allMemories = [];
69116
70752
  const globalMems = listMemories({
@@ -69142,6 +70778,7 @@ function registerProjectCommands(program2) {
69142
70778
  min_importance: minImportance,
69143
70779
  status: "active",
69144
70780
  agent_id: agentId,
70781
+ project_id: projectId,
69145
70782
  session_id: sessionId,
69146
70783
  ...visibleToMachineFilter(visibleMachineId),
69147
70784
  limit: 50
@@ -69901,6 +71538,8 @@ init_database();
69901
71538
  init_memories();
69902
71539
  init_agents();
69903
71540
  init_projects();
71541
+ init_config();
71542
+ init_api_mode();
69904
71543
  import chalk27 from "chalk";
69905
71544
  import { join as join12 } from "path";
69906
71545
  import { homedir as homedir5 } from "os";
@@ -69911,7 +71550,6 @@ import {
69911
71550
  statSync as statSync3,
69912
71551
  constants as fsConstants
69913
71552
  } from "fs";
69914
- init_api_mode();
69915
71553
  init_helpers();
69916
71554
  async function runCommandWithTimeout(args, timeoutMs) {
69917
71555
  const proc = Bun.spawn(args, { stdout: "pipe", stderr: "pipe" });
@@ -70292,7 +71930,8 @@ function resolveApiStatus(version = getPackageVersion()) {
70292
71930
  error = err instanceof Error ? err.message : String(err);
70293
71931
  }
70294
71932
  }
70295
- const validBase = error ? null : apiBase;
71933
+ const resolvedApiUrl = error ? null : apiUrl ?? resolved?.baseUrl ?? null;
71934
+ const validBase = error ? null : apiBase ?? resolved?.baseUrl.replace(/\/v1$/, "") ?? null;
70296
71935
  const apiKeyConfigured = configured?.apiKeyPresent ?? false;
70297
71936
  let transport;
70298
71937
  if (hasExplicitLocalDbPath()) {
@@ -70309,7 +71948,7 @@ function resolveApiStatus(version = getPackageVersion()) {
70309
71948
  app: "mementos",
70310
71949
  version,
70311
71950
  transport,
70312
- api_url: apiUrl,
71951
+ api_url: resolvedApiUrl,
70313
71952
  api_base: validBase,
70314
71953
  api_key_present: apiKeyConfigured || resolved !== null
70315
71954
  },
@@ -70340,8 +71979,9 @@ function registerStatusCommand(program2) {
70340
71979
  }
70341
71980
 
70342
71981
  // src/cli/commands/system-config.ts
70343
- import chalk28 from "chalk";
71982
+ init_config();
70344
71983
  init_helpers();
71984
+ import chalk28 from "chalk";
70345
71985
  function registerConfigCommand(program2) {
70346
71986
  program2.command("config [subcommand] [args...]").description("View or modify configuration. Subcommands: get <key>, set <key> <value>, reset [key], path").action((subcommand, args) => {
70347
71987
  try {
@@ -70427,9 +72067,9 @@ function registerConfigCommand(program2) {
70427
72067
  }
70428
72068
  } else {
70429
72069
  const configPath = getConfigPath2();
70430
- const { unlinkSync: unlinkSync3, existsSync: _existsSync } = __require("fs");
72070
+ const { unlinkSync: unlinkSync4, existsSync: _existsSync } = __require("fs");
70431
72071
  if (_existsSync(configPath)) {
70432
- unlinkSync3(configPath);
72072
+ unlinkSync4(configPath);
70433
72073
  }
70434
72074
  if (useJson) {
70435
72075
  outputJson({ reset: true, all: true });
@@ -70449,10 +72089,11 @@ function registerConfigCommand(program2) {
70449
72089
  }
70450
72090
 
70451
72091
  // src/cli/commands/system-profile.ts
70452
- import chalk29 from "chalk";
70453
- import { join as join13 } from "path";
72092
+ init_config();
70454
72093
  init_paths();
70455
72094
  init_helpers();
72095
+ import chalk29 from "chalk";
72096
+ import { join as join13 } from "path";
70456
72097
  function registerProfileCommand(program2) {
70457
72098
  const profileCmd = program2.command("profile").description("Manage named profile files and active-profile metadata");
70458
72099
  profileCmd.command("list").description("List all available profiles").action(() => {
@@ -70860,10 +72501,12 @@ function registerSessionCommand(program2) {
70860
72501
  const sessionCmd = program2.command("session").description("Session auto-memory \u2014 ingest session transcripts for memory extraction");
70861
72502
  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
72503
  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
72504
  const transcript = _rfs(transcriptFile, "utf-8");
70866
72505
  const sessionId = opts.sessionId ?? `cli-${Date.now()}`;
72506
+ const { isApiMode: isApiMode2 } = await Promise.resolve().then(() => (init_api_mode(), exports_api_mode));
72507
+ const hosted = isApiMode2();
72508
+ const { createSessionJob: createSessionJob2 } = await Promise.resolve().then(() => (init_session_jobs(), exports_session_jobs));
72509
+ const { enqueueSessionJob: enqueueSessionJob2 } = await Promise.resolve().then(() => (init_session_queue(), exports_session_queue));
70867
72510
  const job = createSessionJob2({
70868
72511
  session_id: sessionId,
70869
72512
  transcript,
@@ -70872,7 +72515,7 @@ function registerSessionCommand(program2) {
70872
72515
  project_id: opts.project
70873
72516
  });
70874
72517
  enqueueSessionJob2(job.id);
70875
- console.log(chalk33.green(`\u2713 Session queued: ${chalk33.cyan(job.id)}`));
72518
+ console.log(chalk33.green(`\u2713 Session queued${hosted ? " (hosted store)" : ""}: ${chalk33.cyan(job.id)}`));
70876
72519
  console.log(` Session: ${sessionId}`);
70877
72520
  console.log(` Length: ${transcript.length} chars`);
70878
72521
  });
@@ -70891,14 +72534,16 @@ function registerSessionCommand(program2) {
70891
72534
  if (job.error)
70892
72535
  console.log(chalk33.red(` Error: ${job.error}`));
70893
72536
  });
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) => {
72537
+ 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
72538
  const globalOpts = program2.opts();
70896
72539
  const { listSessionJobs: listSessionJobs2 } = await Promise.resolve().then(() => (init_session_jobs(), exports_session_jobs));
70897
72540
  const jobs = listSessionJobs2({
70898
72541
  agent_id: opts.agent,
70899
72542
  project_id: opts.project,
70900
72543
  status: opts.status,
70901
- limit: parseInt(opts.limit)
72544
+ session_id: opts.sessionId,
72545
+ limit: parseInt(opts.limit, 10),
72546
+ offset: parseInt(opts.offset, 10)
70902
72547
  });
70903
72548
  if (globalOpts.json) {
70904
72549
  outputJson(jobs);
@@ -71044,7 +72689,7 @@ import chalk35 from "chalk";
71044
72689
  import { resolve as resolve19 } from "path";
71045
72690
  function registerSynthesizedProfileCommand(program2) {
71046
72691
  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) => {
72692
+ 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
72693
  try {
71049
72694
  const globalOpts = program2.opts();
71050
72695
  const { synthesizeProfile: synthesizeProfile2 } = await Promise.resolve().then(() => (init_profile_synthesizer(), exports_profile_synthesizer));
@@ -71058,6 +72703,7 @@ function registerSynthesizedProfileCommand(program2) {
71058
72703
  const result = await synthesizeProfile2({
71059
72704
  project_id: projectId,
71060
72705
  agent_id: globalOpts.agent,
72706
+ scope: opts.scope,
71061
72707
  force_refresh: !!opts.refresh
71062
72708
  });
71063
72709
  if (!result) {
@@ -71575,6 +73221,15 @@ import chalk40 from "chalk";
71575
73221
  // src/lib/storage-sync.ts
71576
73222
  init_storage();
71577
73223
  init_database();
73224
+ init_config();
73225
+ init_local_opt_in();
73226
+ 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.";
73227
+ function openLocalSyncAdapter() {
73228
+ if (!isServerContext() && !selectsMementosLocalStore(process.env)) {
73229
+ throw new Error(STORAGE_SYNC_LOCAL_ONLY_MESSAGE);
73230
+ }
73231
+ return new SqliteAdapter(getDbPath2());
73232
+ }
71578
73233
  var MEMORY_TABLE = "memories";
71579
73234
  var MEMORY_SYNC_META_TABLE = "_mementos_storage_sync_meta";
71580
73235
  var SOURCE_MACHINE_METADATA_KEY = "last_synced_source_machine";
@@ -71953,7 +73608,7 @@ function resolveTables(local, tables) {
71953
73608
  function withManagedAdapters(options, fn) {
71954
73609
  const localOwned = !options.local;
71955
73610
  const remoteOwned = !options.remote;
71956
- const local = options.local ?? new SqliteAdapter(getDbPath2());
73611
+ const local = options.local ?? openLocalSyncAdapter();
71957
73612
  const remote = options.remote ?? new PgAdapter(getStorageConnectionString("mementos"));
71958
73613
  try {
71959
73614
  const currentMachineId = resolveCurrentMachineId(local, options.current_machine_id);
@@ -72009,7 +73664,7 @@ function pullStorageChanges(options = {}) {
72009
73664
  function getStorageSyncStatus(options = {}) {
72010
73665
  const backend = getStorageBackend();
72011
73666
  const localOwned = !options.local;
72012
- const local = options.local ?? new SqliteAdapter(getDbPath2());
73667
+ const local = options.local ?? openLocalSyncAdapter();
72013
73668
  try {
72014
73669
  const currentMachineId = resolveCurrentMachineId(local, options.current_machine_id);
72015
73670
  return {
@@ -73581,11 +75236,13 @@ function registerConsolidationCommands(program2) {
73581
75236
  }
73582
75237
  } catch (error40) {
73583
75238
  if (program2.opts().json) {
73584
- outputJson({ error: error40 instanceof Error ? error40.message : String(error40) });
75239
+ await outputJsonAndExit({ error: error40 instanceof Error ? error40.message : String(error40) }, 1);
73585
75240
  } else {
73586
75241
  console.error(chalk42.red(error40 instanceof Error ? error40.message : String(error40)));
73587
75242
  }
73588
- process.exit(1);
75243
+ if (!program2.opts().json)
75244
+ process.exit(1);
75245
+ return;
73589
75246
  }
73590
75247
  });
73591
75248
  program2.command("reflect").description("Reflect on a session, task, or range and save structured lessons").requiredOption("--on <target>", "Trajectory target: session, task, range").option("--source <idOrRange>", "Session ID, task ID, or range as since..until").option("--dry-run", "Critique without writing lesson memories").option("--project <idOrPath>", "Project ID, name, or path").option("--agent <nameOrId>", "Agent name or ID").option("--since <iso>", "Range start timestamp").option("--until <iso>", "Range end timestamp").option("--provider <name>", "Critic provider: anthropic, openai, cerebras, grok").option("--model <name>", "Critic model").option("--max-tokens <n>", "Maximum critic output tokens", parseNumber2).option("--format <fmt>", "Output format: compact, json").action(async (opts) => {
@@ -73624,11 +75281,13 @@ function registerConsolidationCommands(program2) {
73624
75281
  }
73625
75282
  } catch (error40) {
73626
75283
  if (program2.opts().json) {
73627
- outputJson({ error: error40 instanceof Error ? error40.message : String(error40) });
75284
+ await outputJsonAndExit({ error: error40 instanceof Error ? error40.message : String(error40) }, 1);
73628
75285
  } else {
73629
75286
  console.error(chalk42.red(error40 instanceof Error ? error40.message : String(error40)));
73630
75287
  }
73631
- process.exit(1);
75288
+ if (!program2.opts().json)
75289
+ process.exit(1);
75290
+ return;
73632
75291
  }
73633
75292
  });
73634
75293
  }