@hasna/mementos 0.14.85 → 0.14.87

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 (55) hide show
  1. package/bun.lock +63 -4
  2. package/dist/cli/__fixtures__/io-restore-stub-server.d.ts +2 -0
  3. package/dist/cli/__fixtures__/io-restore-stub-server.d.ts.map +1 -0
  4. package/dist/cli/commands/info-stale.d.ts.map +1 -1
  5. package/dist/cli/commands/io-restore.d.ts.map +1 -1
  6. package/dist/cli/commands/memory-cmd-crud.d.ts.map +1 -1
  7. package/dist/cli/commands/memory-cmd-list.d.ts.map +1 -1
  8. package/dist/cli/commands/memory-cmd-remove.d.ts.map +1 -1
  9. package/dist/cli/index.js +623 -208
  10. package/dist/db/__fixtures__/list-filter-capture-server.d.ts +2 -0
  11. package/dist/db/__fixtures__/list-filter-capture-server.d.ts.map +1 -0
  12. package/dist/db/__fixtures__/list-filter-client-runner.d.ts +2 -0
  13. package/dist/db/__fixtures__/list-filter-client-runner.d.ts.map +1 -0
  14. package/dist/db/agents.d.ts +14 -0
  15. package/dist/db/agents.d.ts.map +1 -1
  16. package/dist/db/analytics.d.ts +4 -0
  17. package/dist/db/analytics.d.ts.map +1 -1
  18. package/dist/db/memories.d.ts +6 -0
  19. package/dist/db/memories.d.ts.map +1 -1
  20. package/dist/db/session-jobs.d.ts +18 -0
  21. package/dist/db/session-jobs.d.ts.map +1 -1
  22. package/dist/db/webhook_hooks.d.ts +24 -1
  23. package/dist/db/webhook_hooks.d.ts.map +1 -1
  24. package/dist/diagnostics/historical-project-registration-receipt.js +36 -17
  25. package/dist/index.js +141 -52
  26. package/dist/lib/built-in-hooks.d.ts +22 -2
  27. package/dist/lib/built-in-hooks.d.ts.map +1 -1
  28. package/dist/lib/file-deps.d.ts +1 -1
  29. package/dist/lib/open-sessions-connector.d.ts +6 -6
  30. package/dist/lib/open-sessions-connector.d.ts.map +1 -1
  31. package/dist/lib/redact.d.ts +16 -0
  32. package/dist/lib/redact.d.ts.map +1 -1
  33. package/dist/lib/session-processor.d.ts.map +1 -1
  34. package/dist/lib/session-queue.d.ts.map +1 -1
  35. package/dist/lib/storage-sync.d.ts.map +1 -1
  36. package/dist/mcp/index.js +348 -70
  37. package/dist/mcp/tools/memory-lifecycle.d.ts.map +1 -1
  38. package/dist/pg-sync-worker.js +7 -5
  39. package/dist/project-registration.js +108 -36
  40. package/dist/sdk/index.d.ts +9 -0
  41. package/dist/sdk/index.d.ts.map +1 -1
  42. package/dist/sdk/index.js +1 -0
  43. package/dist/server/index.d.ts.map +1 -1
  44. package/dist/server/index.js +563 -299
  45. package/dist/server/routes/system-hooks.d.ts.map +1 -1
  46. package/dist/storage.d.ts +16 -2
  47. package/dist/storage.d.ts.map +1 -1
  48. package/dist/storage.js +63 -26
  49. package/dist/test-support/pg-sync-stub-worker.d.ts +2 -0
  50. package/dist/test-support/pg-sync-stub-worker.d.ts.map +1 -0
  51. package/dist/types/hooks.d.ts +1 -1
  52. package/dist/types/index.d.ts +10 -0
  53. package/dist/types/index.d.ts.map +1 -1
  54. package/hasna.contract.json +5 -4
  55. package/package.json +4 -3
package/dist/cli/index.js CHANGED
@@ -1011,7 +1011,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
1011
1011
  this._exitCallback = (err) => {
1012
1012
  if (err.code !== "commander.executeSubCommandAsync") {
1013
1013
  throw err;
1014
- }
1014
+ } else {}
1015
1015
  };
1016
1016
  }
1017
1017
  return this;
@@ -2274,6 +2274,7 @@ function translateSql(sql) {
2274
2274
  let translated = sql.replace(/\?/g, () => `$${++parameterIndex}`);
2275
2275
  const ISO_FMT = `'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'`;
2276
2276
  translated = translated.replace(/datetime\s*\(\s*'now'\s*\)/gi, `to_char(now() AT TIME ZONE 'UTC', ${ISO_FMT})`);
2277
+ translated = translated.replace(/strftime\s*\(\s*'%Y-%m-%dT%H:%M:%fZ'\s*,\s*'now'\s*\)/gi, `to_char(now() AT TIME ZONE 'UTC', ${ISO_FMT})`);
2277
2278
  translated = translated.replace(/datetime\s*\(\s*'now'\s*,\s*'(-?\d+)\s+(minutes?|hours?|days?|seconds?)'\s*\)/gi, (_match, amount, unit) => {
2278
2279
  const parsed = parseInt(String(amount), 10);
2279
2280
  const absolute = Math.abs(parsed);
@@ -2758,16 +2759,24 @@ function transferRows(target, table, rows, options) {
2758
2759
  const conflictColumn = options.conflictColumn ?? "updated_at";
2759
2760
  let written = 0;
2760
2761
  let skipped = 0;
2762
+ let maxSyncedAt = null;
2761
2763
  const errors = [];
2764
+ const bumpMaxSyncedAt = (row) => {
2765
+ const value = row[conflictColumn];
2766
+ if (typeof value === "string" && (maxSyncedAt === null || value > maxSyncedAt)) {
2767
+ maxSyncedAt = value;
2768
+ }
2769
+ };
2762
2770
  if (rows.length === 0) {
2763
- return { written, skipped, errors };
2771
+ return { written, skipped, errors, maxSyncedAt };
2764
2772
  }
2765
2773
  const columns = Object.keys(rows[0] ?? {});
2766
2774
  if (!columns.includes(primaryKey)) {
2767
2775
  return {
2768
2776
  written,
2769
2777
  skipped,
2770
- errors: [`Table "${table}" has no "${primaryKey}" column; skipping`]
2778
+ errors: [`Table "${table}" has no "${primaryKey}" column; skipping`],
2779
+ maxSyncedAt
2771
2780
  };
2772
2781
  }
2773
2782
  const hasConflictColumn = columns.includes(conflictColumn);
@@ -2780,6 +2789,7 @@ function transferRows(target, table, rows, options) {
2780
2789
  const incomingTime = Date.parse(String(row[conflictColumn]));
2781
2790
  if (Number.isFinite(existingTime) && Number.isFinite(incomingTime) && existingTime >= incomingTime) {
2782
2791
  skipped++;
2792
+ bumpMaxSyncedAt(row);
2783
2793
  continue;
2784
2794
  }
2785
2795
  }
@@ -2792,11 +2802,12 @@ function transferRows(target, table, rows, options) {
2792
2802
  target.run(`INSERT INTO "${table}" (${columnList}) VALUES (${placeholders})`, ...columns.map((column) => row[column]));
2793
2803
  }
2794
2804
  written++;
2805
+ bumpMaxSyncedAt(row);
2795
2806
  } catch (error) {
2796
2807
  errors.push(`Row ${String(row[primaryKey] ?? "unknown")}: ${error instanceof Error ? error.message : String(error)}`);
2797
2808
  }
2798
2809
  }
2799
- return { written, skipped, errors };
2810
+ return { written, skipped, errors, maxSyncedAt };
2800
2811
  }
2801
2812
  function incrementalSyncPush(local, remote, tables, options = {}) {
2802
2813
  return runIncrementalSync("push", local, remote, local, tables, options);
@@ -2834,22 +2845,29 @@ function runIncrementalSync(direction, source, target, metaDb, tables, options)
2834
2845
  rows = source.all(`SELECT * FROM "${table}"`);
2835
2846
  stat.first_sync = true;
2836
2847
  }
2848
+ let maxSyncedAt = null;
2837
2849
  for (let offset = 0;offset < rows.length; offset += batchSize) {
2838
2850
  const batch = rows.slice(offset, offset + batchSize);
2839
2851
  const result = transferRows(target, table, batch, options);
2840
2852
  stat.synced_rows += result.written;
2841
2853
  stat.skipped_rows += result.skipped;
2842
2854
  stat.errors.push(...result.errors);
2855
+ if (result.maxSyncedAt !== null && (maxSyncedAt === null || result.maxSyncedAt > maxSyncedAt)) {
2856
+ maxSyncedAt = result.maxSyncedAt;
2857
+ }
2843
2858
  }
2844
2859
  if (rows.length === 0) {
2845
2860
  stat.skipped_rows = stat.total_rows;
2846
2861
  }
2847
- upsertSyncMeta(metaDb, {
2848
- table_name: table,
2849
- last_synced_at: new Date().toISOString(),
2850
- last_synced_row_count: stat.synced_rows,
2851
- direction
2852
- });
2862
+ const nextCursor = maxSyncedAt ?? meta?.last_synced_at ?? null;
2863
+ if (rows.length > 0 && stat.errors.length === 0 && nextCursor !== null) {
2864
+ upsertSyncMeta(metaDb, {
2865
+ table_name: table,
2866
+ last_synced_at: nextCursor,
2867
+ last_synced_row_count: stat.synced_rows,
2868
+ direction
2869
+ });
2870
+ }
2853
2871
  } catch (error) {
2854
2872
  stat.errors.push(`Table "${table}": ${error instanceof Error ? error.message : String(error)}`);
2855
2873
  }
@@ -2877,8 +2895,13 @@ var init_storage = __esm(() => {
2877
2895
  data;
2878
2896
  closed = false;
2879
2897
  lastError = null;
2898
+ generation = 0;
2880
2899
  static DATA_BYTES = 128 * 1024 * 1024;
2881
- static QUERY_TIMEOUT_MS = 60000;
2900
+ static queryTimeoutMs() {
2901
+ const raw = process.env["MEMENTOS_PGSYNC_QUERY_TIMEOUT_MS"]?.trim();
2902
+ const parsed = raw ? Number(raw) : Number.NaN;
2903
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 60000;
2904
+ }
2882
2905
  static resolveWorkerPath() {
2883
2906
  const ext = import.meta.url.endsWith(".ts") ? ".ts" : ".js";
2884
2907
  const here = fileURLToPath(new URL(".", import.meta.url));
@@ -2893,12 +2916,12 @@ var init_storage = __esm(() => {
2893
2916
  }
2894
2917
  return candidates[0];
2895
2918
  }
2896
- constructor(connectionString) {
2897
- const control = new SharedArrayBuffer(8);
2919
+ constructor(connectionString, workerPath) {
2920
+ const control = new SharedArrayBuffer(12);
2898
2921
  const dataSab = new SharedArrayBuffer(PgSyncPool.DATA_BYTES);
2899
2922
  this.status = new Int32Array(control);
2900
2923
  this.data = new Uint8Array(dataSab);
2901
- this.worker = new Worker(PgSyncPool.resolveWorkerPath(), {
2924
+ this.worker = new Worker(workerPath ?? PgSyncPool.resolveWorkerPath(), {
2902
2925
  workerData: {
2903
2926
  dsn: stripSslParams(connectionString),
2904
2927
  ssl: sslConfigFor(connectionString),
@@ -2916,21 +2939,35 @@ var init_storage = __esm(() => {
2916
2939
  throw new Error("PgSyncPool is closed");
2917
2940
  if (this.lastError)
2918
2941
  throw this.lastError;
2942
+ const timeoutMs = PgSyncPool.queryTimeoutMs();
2943
+ const gen = ++this.generation;
2919
2944
  Atomics.store(this.status, 0, 0);
2920
- this.worker.postMessage({ sql, params });
2921
- const waitResult = Atomics.wait(this.status, 0, 0, PgSyncPool.QUERY_TIMEOUT_MS);
2922
- const code = Atomics.load(this.status, 0);
2923
- if (code === 0 || waitResult === "timed-out") {
2924
- if (this.lastError)
2925
- throw this.lastError;
2926
- throw new Error("PostgreSQL query timed out after 60s");
2927
- }
2928
- const len = Atomics.load(this.status, 1);
2929
- const payload = JSON.parse(new TextDecoder().decode(this.data.subarray(0, len)));
2930
- if (code === 2) {
2931
- throw new Error(payload.message ?? "PostgreSQL error");
2945
+ Atomics.store(this.status, 2, 0);
2946
+ this.worker.postMessage({ sql, params, gen });
2947
+ const deadline = Date.now() + timeoutMs;
2948
+ for (;; ) {
2949
+ const remaining = deadline - Date.now();
2950
+ const responding = Atomics.load(this.status, 0);
2951
+ if (responding === gen) {
2952
+ const code = Atomics.load(this.status, 2);
2953
+ const len = Atomics.load(this.status, 1);
2954
+ const payload = JSON.parse(new TextDecoder().decode(this.data.subarray(0, len)));
2955
+ if (code === 2) {
2956
+ throw new Error(payload.message ?? "PostgreSQL error");
2957
+ }
2958
+ return payload;
2959
+ }
2960
+ if (responding !== 0) {
2961
+ Atomics.compareExchange(this.status, 0, responding, 0);
2962
+ continue;
2963
+ }
2964
+ if (remaining <= 0) {
2965
+ if (this.lastError)
2966
+ throw this.lastError;
2967
+ throw new Error(`PostgreSQL query timed out after ${timeoutMs}ms`);
2968
+ }
2969
+ Atomics.wait(this.status, 0, 0, remaining);
2932
2970
  }
2933
- return payload;
2934
2971
  }
2935
2972
  end() {
2936
2973
  if (this.closed)
@@ -4982,6 +5019,30 @@ function redactSecrets(text) {
4982
5019
  }
4983
5020
  return result;
4984
5021
  }
5022
+ function redactValueTree(value) {
5023
+ if (typeof value === "string")
5024
+ return redactSecrets(value);
5025
+ if (Array.isArray(value))
5026
+ return value.map(redactValueTree);
5027
+ if (value !== null && typeof value === "object") {
5028
+ const out = {};
5029
+ for (const [k, v] of Object.entries(value)) {
5030
+ out[k] = redactValueTree(v);
5031
+ }
5032
+ return out;
5033
+ }
5034
+ return value;
5035
+ }
5036
+ function redactMemoryForOutput(memory) {
5037
+ return {
5038
+ ...memory,
5039
+ key: redactSecrets(memory.key),
5040
+ value: redactSecrets(memory.value),
5041
+ summary: memory.summary ? redactSecrets(memory.summary) : null,
5042
+ when_to_use: memory.when_to_use ? redactSecrets(memory.when_to_use) : null,
5043
+ metadata: redactValueTree(memory.metadata)
5044
+ };
5045
+ }
4985
5046
  var REDACTED = "[REDACTED]", SECRET_PATTERNS;
4986
5047
  var init_redact = __esm(() => {
4987
5048
  SECRET_PATTERNS = [
@@ -5267,6 +5328,7 @@ __export(exports_memories, {
5267
5328
  updateMemory: () => updateMemory,
5268
5329
  touchMemory: () => touchMemory,
5269
5330
  semanticSearch: () => semanticSearch,
5331
+ reservedAgentIdViolation: () => reservedAgentIdViolation,
5270
5332
  parseMemoryRow: () => parseMemoryRow,
5271
5333
  listMemoryHistoryPage: () => listMemoryHistoryPage,
5272
5334
  listMemoryHistory: () => listMemoryHistory,
@@ -5338,7 +5400,20 @@ function parseMemoryRow(row) {
5338
5400
  accessed_at: row["accessed_at"] || null
5339
5401
  };
5340
5402
  }
5403
+ function reservedAgentIdViolation(agentId) {
5404
+ if (!agentId)
5405
+ return null;
5406
+ const normalized = agentId.trim().toLowerCase();
5407
+ if (RESERVED_AGENT_IDS.has(normalized)) {
5408
+ return `Reserved placeholder agent id "${agentId}" cannot own a memory. ` + `Refusing to write: test harnesses must not write memories under placeholder agent ` + `identities. Register a real agent (mementos register-agent <name>) and pass its id.`;
5409
+ }
5410
+ return null;
5411
+ }
5341
5412
  function createMemory(input, dedupeMode = "merge", db) {
5413
+ const reservedViolation = reservedAgentIdViolation(input.agent_id);
5414
+ if (reservedViolation) {
5415
+ throw new Error(reservedViolation);
5416
+ }
5342
5417
  if (!db && isApiMode()) {
5343
5418
  const { status, data } = apiJson("POST", "/memories", { ...input, dedupe: dedupeMode });
5344
5419
  if (!data || !data.id) {
@@ -5388,7 +5463,8 @@ function createMemory(input, dedupeMode = "merge", db) {
5388
5463
  importance = ?, metadata = ?, expires_at = ?,
5389
5464
  when_to_use = ?,
5390
5465
  pinned = COALESCE(pinned, 0),
5391
- version = version + 1, updated_at = ?
5466
+ version = version + 1, updated_at = ?,
5467
+ updated_by_agent = ?
5392
5468
  WHERE id = ?`, [
5393
5469
  safeValue,
5394
5470
  input.category || "knowledge",
@@ -5399,6 +5475,7 @@ function createMemory(input, dedupeMode = "merge", db) {
5399
5475
  expiresAt,
5400
5476
  input.when_to_use || null,
5401
5477
  timestamp,
5478
+ input.agent_id || null,
5402
5479
  existing.id
5403
5480
  ]);
5404
5481
  d.run("DELETE FROM memory_tags WHERE memory_id = ?", [existing.id]);
@@ -5503,6 +5580,12 @@ function bulkUpsertMemories(memories, db) {
5503
5580
  errors.push(`Rejected "${key}": ${formatEnumViolation(violation)}`);
5504
5581
  continue;
5505
5582
  }
5583
+ const agentViolation = reservedAgentIdViolation(typeof mem["agent_id"] === "string" ? mem["agent_id"] : undefined);
5584
+ if (agentViolation) {
5585
+ rejected++;
5586
+ errors.push(`Rejected "${key}": ${agentViolation}`);
5587
+ continue;
5588
+ }
5506
5589
  const timestamp = now();
5507
5590
  let tags = [];
5508
5591
  const rawTags = mem["tags"];
@@ -5781,6 +5864,11 @@ function listMemoriesPage(filter, db) {
5781
5864
  agent_id: f.agent_id,
5782
5865
  project_id: f.project_id,
5783
5866
  session_id: f.session_id,
5867
+ machine_id: f.machine_id,
5868
+ visible_to_machine_id: f.visible_to_machine_id,
5869
+ search: f.search,
5870
+ source: f.source,
5871
+ flag: f.flag,
5784
5872
  namespace: f.namespace,
5785
5873
  as_of: f.as_of,
5786
5874
  limit: f.limit,
@@ -6060,20 +6148,29 @@ function updateMemory(id, input, db) {
6060
6148
  sets.push("when_to_use = ?");
6061
6149
  params.push(input.when_to_use ?? null);
6062
6150
  }
6151
+ if (input.updated_by_agent !== undefined) {
6152
+ sets.push("updated_by_agent = ?");
6153
+ params.push(input.updated_by_agent ?? null);
6154
+ }
6063
6155
  if (input.tags !== undefined) {
6064
6156
  sets.push("tags = ?");
6065
6157
  params.push(JSON.stringify(input.tags));
6066
- d.run("DELETE FROM memory_tags WHERE memory_id = ?", [memoryId]);
6067
- const insertTag = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
6068
- for (const tag of input.tags) {
6069
- insertTag.run(memoryId, tag);
6070
- }
6071
6158
  }
6072
6159
  params.push(memoryId);
6073
- const result = d.run(`UPDATE memories SET ${sets.join(", ")} WHERE id = ?`, params);
6074
- if (result.changes === 0) {
6075
- throw new Error(`Update affected no rows for memory ${memoryId}: the record was read but not written. ` + `This is a bug in @hasna/mementos, not a bad argument \u2014 please report it.`);
6076
- }
6160
+ d.transaction(() => {
6161
+ const res = d.run(`UPDATE memories SET ${sets.join(", ")} WHERE id = ?`, params);
6162
+ if (res.changes === 0) {
6163
+ throw new Error(`Update affected no rows for memory ${memoryId}: the record was read but not written. ` + `This is a bug in @hasna/mementos, not a bad argument \u2014 please report it.`);
6164
+ }
6165
+ if (input.tags !== undefined) {
6166
+ d.run("DELETE FROM memory_tags WHERE memory_id = ?", [memoryId]);
6167
+ const insertTag = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
6168
+ for (const tag of input.tags) {
6169
+ insertTag.run(memoryId, tag);
6170
+ }
6171
+ }
6172
+ return res;
6173
+ });
6077
6174
  const updated = getMemory(memoryId, d);
6078
6175
  if (input.value !== undefined) {
6079
6176
  try {
@@ -6244,7 +6341,7 @@ async function semanticSearch(queryText, options = {}, db) {
6244
6341
  scored.sort((a, b) => b.score - a.score);
6245
6342
  return scored.slice(0, limit);
6246
6343
  }
6247
- var RECALL_PROMOTE_THRESHOLD = 3;
6344
+ var RESERVED_AGENT_IDS, RECALL_PROMOTE_THRESHOLD = 3;
6248
6345
  var init_memories = __esm(() => {
6249
6346
  init_types();
6250
6347
  init_database();
@@ -6254,9 +6351,32 @@ var init_memories = __esm(() => {
6254
6351
  init_poisoning();
6255
6352
  init_entity_memories();
6256
6353
  init_api_mode();
6354
+ RESERVED_AGENT_IDS = new Set([
6355
+ "agent-a",
6356
+ "agent-x",
6357
+ "agent-z",
6358
+ "nonexistent-agent"
6359
+ ]);
6257
6360
  });
6258
6361
 
6259
6362
  // src/db/agents.ts
6363
+ import { homedir as homedir3 } from "os";
6364
+ import { join as join5 } from "path";
6365
+ import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
6366
+ function resolveWritingAgentName() {
6367
+ const envName = process.env["MEMENTOS_AGENT"]?.trim();
6368
+ if (envName)
6369
+ return envName;
6370
+ try {
6371
+ const path = join5(homedir3(), ".hasna", "conversations", "agent-id");
6372
+ if (existsSync4(path)) {
6373
+ const fileAgent = readFileSync2(path, "utf8").trim();
6374
+ if (fileAgent)
6375
+ return fileAgent;
6376
+ }
6377
+ } catch {}
6378
+ return null;
6379
+ }
6260
6380
  function parseAgentRow(row) {
6261
6381
  return {
6262
6382
  id: row["id"],
@@ -6459,17 +6579,17 @@ var init_agents = __esm(() => {
6459
6579
  });
6460
6580
 
6461
6581
  // src/lib/package-version.ts
6462
- import { readFileSync as readFileSync2 } from "fs";
6463
- import { dirname as dirname2, join as join5 } from "path";
6582
+ import { readFileSync as readFileSync3 } from "fs";
6583
+ import { dirname as dirname2, join as join6 } from "path";
6464
6584
  import { fileURLToPath as fileURLToPath2 } from "url";
6465
6585
  function getMementosPackageVersion() {
6466
6586
  const here = dirname2(fileURLToPath2(import.meta.url));
6467
6587
  for (const candidate of [
6468
- join5(here, "..", "..", "package.json"),
6469
- join5(here, "..", "package.json")
6588
+ join6(here, "..", "..", "package.json"),
6589
+ join6(here, "..", "package.json")
6470
6590
  ]) {
6471
6591
  try {
6472
- const parsed = JSON.parse(readFileSync2(candidate, "utf8"));
6592
+ const parsed = JSON.parse(readFileSync3(candidate, "utf8"));
6473
6593
  if (typeof parsed.version === "string" && parsed.version.trim())
6474
6594
  return parsed.version;
6475
6595
  } catch {}
@@ -7472,13 +7592,13 @@ __export(exports_helpers, {
7472
7592
  DEFAULT_COMPACT_LIMIT: () => DEFAULT_COMPACT_LIMIT
7473
7593
  });
7474
7594
  import chalk from "chalk";
7475
- import { readFileSync as readFileSync3 } from "fs";
7476
- import { dirname as dirname3, join as join6, resolve as resolve2 } from "path";
7595
+ import { readFileSync as readFileSync4 } from "fs";
7596
+ import { dirname as dirname3, join as join7, resolve as resolve2 } from "path";
7477
7597
  import { fileURLToPath as fileURLToPath3 } from "url";
7478
7598
  function getPackageVersion() {
7479
7599
  try {
7480
- const pkgPath = join6(dirname3(fileURLToPath3(import.meta.url)), "..", "..", "package.json");
7481
- const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
7600
+ const pkgPath = join7(dirname3(fileURLToPath3(import.meta.url)), "..", "..", "package.json");
7601
+ const pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
7482
7602
  return pkg.version || "0.0.0";
7483
7603
  } catch {
7484
7604
  return "0.0.0";
@@ -8037,16 +8157,16 @@ function validateConfigKeyValue(key, value, DEFAULT_CONFIG) {
8037
8157
  return null;
8038
8158
  }
8039
8159
  function getConfigPath() {
8040
- const { homedir: homedir3 } = __require("os");
8041
- return join6(homedir3(), ".hasna", "mementos", "config.json");
8160
+ const { homedir: homedir4 } = __require("os");
8161
+ return join7(homedir4(), ".hasna", "mementos", "config.json");
8042
8162
  }
8043
8163
  function readFileConfig() {
8044
- const { existsSync: existsSync4 } = __require("fs");
8164
+ const { existsSync: existsSync5 } = __require("fs");
8045
8165
  const configPath = getConfigPath();
8046
- if (!existsSync4(configPath))
8166
+ if (!existsSync5(configPath))
8047
8167
  return {};
8048
8168
  try {
8049
- const data = JSON.parse(readFileSync3(configPath, "utf-8"));
8169
+ const data = JSON.parse(readFileSync4(configPath, "utf-8"));
8050
8170
  if (data === null || typeof data !== "object" || Array.isArray(data)) {
8051
8171
  throw new Error("expected a JSON object");
8052
8172
  }
@@ -8057,10 +8177,10 @@ function readFileConfig() {
8057
8177
  }
8058
8178
  }
8059
8179
  function writeFileConfig(data) {
8060
- const { existsSync: existsSync4, writeFileSync: writeFileSync3, mkdirSync: mkdirSync3 } = __require("fs");
8180
+ const { existsSync: existsSync5, writeFileSync: writeFileSync3, mkdirSync: mkdirSync3 } = __require("fs");
8061
8181
  const configPath = getConfigPath();
8062
8182
  const dir = dirname3(configPath);
8063
- if (!existsSync4(dir))
8183
+ if (!existsSync5(dir))
8064
8184
  mkdirSync3(dir, { recursive: true });
8065
8185
  writeFileSync3(configPath, JSON.stringify(data, null, 2) + `
8066
8186
  `, "utf-8");
@@ -8488,7 +8608,7 @@ function buildFilterConditions(filter) {
8488
8608
  const conditions = [];
8489
8609
  const params = [];
8490
8610
  conditions.push("m.status = 'active'");
8491
- conditions.push("(m.expires_at IS NULL OR m.expires_at >= datetime('now'))");
8611
+ conditions.push("(m.expires_at IS NULL OR m.expires_at >= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))");
8492
8612
  if (!filter)
8493
8613
  return { conditions, params };
8494
8614
  if (filter.scope) {
@@ -9856,6 +9976,7 @@ var init_auto_memory = __esm(() => {
9856
9976
  // src/db/webhook_hooks.ts
9857
9977
  var exports_webhook_hooks = {};
9858
9978
  __export(exports_webhook_hooks, {
9979
+ validateWebhookHandlerUrl: () => validateWebhookHandlerUrl,
9859
9980
  updateWebhookHook: () => updateWebhookHook,
9860
9981
  recordWebhookInvocation: () => recordWebhookInvocation,
9861
9982
  listWebhookHooks: () => listWebhookHooks,
@@ -9863,6 +9984,8 @@ __export(exports_webhook_hooks, {
9863
9984
  deleteWebhookHook: () => deleteWebhookHook,
9864
9985
  createWebhookHook: () => createWebhookHook
9865
9986
  });
9987
+ import { isIP } from "net";
9988
+ import { lookup as dnsLookup } from "dns/promises";
9866
9989
  function parseRow(row) {
9867
9990
  return {
9868
9991
  id: row["id"],
@@ -9879,7 +10002,153 @@ function parseRow(row) {
9879
10002
  failureCount: row["failure_count"]
9880
10003
  };
9881
10004
  }
9882
- function createWebhookHook(input, db) {
10005
+ function isBlockedIpv4(parts) {
10006
+ const a = parts[0];
10007
+ const b = parts[1];
10008
+ if (a === 0)
10009
+ return true;
10010
+ if (a === 127)
10011
+ return true;
10012
+ if (a === 169 && b === 254)
10013
+ return true;
10014
+ if (a === 10)
10015
+ return true;
10016
+ if (a === 172 && b >= 16 && b <= 31)
10017
+ return true;
10018
+ if (a === 192 && b === 168)
10019
+ return true;
10020
+ return false;
10021
+ }
10022
+ function isBlockedIpv6(bytes) {
10023
+ const mapped = bytes.slice(0, 10).every((b) => b === 0) && bytes[10] === 255 && bytes[11] === 255;
10024
+ if (mapped)
10025
+ return isBlockedIpv4(bytes.slice(12, 16));
10026
+ if (bytes.every((b) => b === 0))
10027
+ return true;
10028
+ if (bytes.slice(0, 15).every((b) => b === 0) && bytes[15] === 1)
10029
+ return true;
10030
+ if ((bytes[0] & 254) === 252)
10031
+ return true;
10032
+ if (bytes[0] === 254 && (bytes[1] & 192) === 128)
10033
+ return true;
10034
+ return false;
10035
+ }
10036
+ function quadToGroups(quad) {
10037
+ const nums = quad.split(".").map((p) => Number(p));
10038
+ const [a, b, c, d] = nums;
10039
+ const valid = [a, b, c, d].every((n) => n !== undefined && Number.isInteger(n) && n >= 0 && n <= 255);
10040
+ if (!valid)
10041
+ return null;
10042
+ return [a << 8 | b, c << 8 | d];
10043
+ }
10044
+ function parseIpv6Bytes(host) {
10045
+ const groups = host.split("::");
10046
+ if (groups.length > 2)
10047
+ return null;
10048
+ const headRaw = groups[0] ?? "";
10049
+ const tailRaw = groups[1] ?? "";
10050
+ const head = headRaw === "" ? [] : headRaw.split(":");
10051
+ const tail = tailRaw === "" ? [] : tailRaw.split(":");
10052
+ const headNums = [];
10053
+ for (const g of head) {
10054
+ if (!/^[0-9a-f]{1,4}$/i.test(g))
10055
+ return null;
10056
+ headNums.push(parseInt(g, 16));
10057
+ }
10058
+ const tailNums = [];
10059
+ for (const g of tail) {
10060
+ if (/^\d+\.\d+\.\d+\.\d+$/.test(g)) {
10061
+ const quads = quadToGroups(g);
10062
+ if (!quads)
10063
+ return null;
10064
+ tailNums.push(...quads);
10065
+ } else if (/^[0-9a-f]{1,4}$/i.test(g)) {
10066
+ tailNums.push(parseInt(g, 16));
10067
+ } else {
10068
+ return null;
10069
+ }
10070
+ }
10071
+ const hasCompression = groups.length === 2;
10072
+ if (!hasCompression && headNums.length !== 8)
10073
+ return null;
10074
+ if (hasCompression && headNums.length + tailNums.length >= 8)
10075
+ return null;
10076
+ const zeros = 8 - headNums.length - tailNums.length;
10077
+ const all = [...headNums, ...new Array(zeros).fill(0), ...tailNums];
10078
+ const bytes = [];
10079
+ for (const n of all) {
10080
+ bytes.push(n >> 8 & 255, n & 255);
10081
+ }
10082
+ return bytes;
10083
+ }
10084
+ function defaultResolveHost(hostname2) {
10085
+ return dnsLookup(hostname2, { all: true, verbatim: true });
10086
+ }
10087
+ function assertResolvedAddressPublic(address, url) {
10088
+ const version = isIP(address);
10089
+ if (version === 4) {
10090
+ if (isBlockedIpv4(address.split(".").map((p) => Number(p)))) {
10091
+ throw new Error(`${BLOCKED_TARGET_MESSAGE}: "${url}"`);
10092
+ }
10093
+ } else if (version === 6) {
10094
+ const bytes = parseIpv6Bytes(address);
10095
+ if (!bytes || isBlockedIpv6(bytes)) {
10096
+ throw new Error(`${BLOCKED_TARGET_MESSAGE}: "${url}"`);
10097
+ }
10098
+ } else {
10099
+ throw new Error(`${BLOCKED_TARGET_MESSAGE}: "${url}"`);
10100
+ }
10101
+ }
10102
+ async function validateWebhookHandlerUrl(url, opts) {
10103
+ const resolveHost = opts?.lookup ?? defaultResolveHost;
10104
+ let parsed;
10105
+ try {
10106
+ parsed = new URL(url);
10107
+ } catch {
10108
+ throw new Error(`Invalid webhook handler URL "${url}" \u2014 must be a valid http(s) URL`);
10109
+ }
10110
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
10111
+ throw new Error(`Invalid webhook handler URL "${url}" \u2014 only http and https are allowed`);
10112
+ }
10113
+ if (parsed.username || parsed.password) {
10114
+ throw new Error(`Invalid webhook handler URL "${url}" \u2014 embedded credentials are not allowed`);
10115
+ }
10116
+ const host = parsed.hostname.replace(/^\[|\]$/g, "").toLowerCase();
10117
+ if (host === "localhost" || host.endsWith(".localhost")) {
10118
+ throw new Error(`${BLOCKED_TARGET_MESSAGE}: "${url}"`);
10119
+ }
10120
+ const version = isIP(host);
10121
+ if (version === 4 || version === 6) {
10122
+ if (version === 4) {
10123
+ if (isBlockedIpv4(host.split(".").map((p) => Number(p)))) {
10124
+ throw new Error(`${BLOCKED_TARGET_MESSAGE}: "${url}"`);
10125
+ }
10126
+ } else {
10127
+ const bytes = parseIpv6Bytes(host);
10128
+ if (!bytes || isBlockedIpv6(bytes)) {
10129
+ throw new Error(`${BLOCKED_TARGET_MESSAGE}: "${url}"`);
10130
+ }
10131
+ }
10132
+ return;
10133
+ }
10134
+ if (/^[0-9]+(\.[0-9]+)*$/.test(host) || /^0x[0-9a-f]+$/i.test(host) || host.includes("%")) {
10135
+ throw new Error(`${BLOCKED_TARGET_MESSAGE}: "${url}"`);
10136
+ }
10137
+ let addrs;
10138
+ try {
10139
+ addrs = await resolveHost(host);
10140
+ } catch {
10141
+ throw new Error(`${BLOCKED_TARGET_MESSAGE}: "${url}"`);
10142
+ }
10143
+ if (addrs.length === 0) {
10144
+ throw new Error(`${BLOCKED_TARGET_MESSAGE}: "${url}"`);
10145
+ }
10146
+ for (const { address } of addrs) {
10147
+ assertResolvedAddressPublic(address, url);
10148
+ }
10149
+ }
10150
+ async function createWebhookHook(input, db, opts) {
10151
+ await validateWebhookHandlerUrl(input.handlerUrl, opts);
9883
10152
  if (!db && isApiMode()) {
9884
10153
  const { data } = apiJson("POST", "/webhooks", {
9885
10154
  type: input.type,
@@ -9997,6 +10266,7 @@ function recordWebhookInvocation(id, success, db) {
9997
10266
  d.run("UPDATE webhook_hooks SET invocation_count = invocation_count + 1, failure_count = failure_count + 1 WHERE id = ?", [id]);
9998
10267
  }
9999
10268
  }
10269
+ var BLOCKED_TARGET_MESSAGE = "Invalid webhook handler URL \u2014 loopback, link-local, and private network targets are not allowed";
10000
10270
  var init_webhook_hooks = __esm(() => {
10001
10271
  init_database();
10002
10272
  init_api_mode();
@@ -10568,7 +10838,7 @@ async function detectContradiction(newKey, newValue, options = {}, db) {
10568
10838
  conditions.push("project_id = ?");
10569
10839
  params.push(project_id);
10570
10840
  }
10571
- conditions.push("(valid_until IS NULL OR valid_until > datetime('now'))");
10841
+ conditions.push("(valid_until IS NULL OR valid_until > strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))");
10572
10842
  const sql = `SELECT * FROM memories WHERE ${conditions.join(" AND ")} ORDER BY importance DESC LIMIT 10`;
10573
10843
  const rows = d.query(sql).all(...params);
10574
10844
  if (rows.length === 0) {
@@ -10615,6 +10885,7 @@ var init_contradiction = __esm(() => {
10615
10885
  var exports_built_in_hooks = {};
10616
10886
  __export(exports_built_in_hooks, {
10617
10887
  reloadWebhooks: () => reloadWebhooks,
10888
+ makeWebhookHandler: () => makeWebhookHandler,
10618
10889
  loadWebhooksFromDb: () => loadWebhooksFromDb
10619
10890
  });
10620
10891
  async function getAutoMemory() {
@@ -10624,13 +10895,19 @@ async function getAutoMemory() {
10624
10895
  }
10625
10896
  return _processConversationTurn;
10626
10897
  }
10627
- function loadWebhooksFromDb() {
10898
+ async function loadWebhooksFromDb() {
10628
10899
  if (_webhooksLoaded)
10629
10900
  return;
10630
10901
  _webhooksLoaded = true;
10631
10902
  try {
10632
10903
  const webhooks = listWebhookHooks({ enabled: true });
10633
10904
  for (const wh of webhooks) {
10905
+ try {
10906
+ await validateWebhookHandlerUrl(wh.handlerUrl);
10907
+ } catch (err) {
10908
+ console.error(`[hooks] Skipping webhook ${wh.id} (${wh.type}): ${err instanceof Error ? err.message : String(err)}`);
10909
+ continue;
10910
+ }
10634
10911
  hookRegistry.register({
10635
10912
  type: wh.type,
10636
10913
  blocking: wh.blocking,
@@ -10648,9 +10925,10 @@ function loadWebhooksFromDb() {
10648
10925
  console.error("[hooks] Failed to load webhooks from DB:", err);
10649
10926
  }
10650
10927
  }
10651
- function makeWebhookHandler(webhookId, url) {
10928
+ function makeWebhookHandler(webhookId, url, opts) {
10652
10929
  return async (context) => {
10653
10930
  try {
10931
+ await validateWebhookHandlerUrl(url, opts);
10654
10932
  const res = await fetch(url, {
10655
10933
  method: "POST",
10656
10934
  headers: { "Content-Type": "application/json" },
@@ -10663,9 +10941,9 @@ function makeWebhookHandler(webhookId, url) {
10663
10941
  }
10664
10942
  };
10665
10943
  }
10666
- function reloadWebhooks() {
10944
+ async function reloadWebhooks() {
10667
10945
  _webhooksLoaded = false;
10668
- loadWebhooksFromDb();
10946
+ await loadWebhooksFromDb();
10669
10947
  }
10670
10948
  var _processConversationTurn = null, _webhooksLoaded = false;
10671
10949
  var init_built_in_hooks = __esm(() => {
@@ -11762,10 +12040,12 @@ var init_synthesis2 = __esm(() => {
11762
12040
  var exports_session_jobs = {};
11763
12041
  __export(exports_session_jobs, {
11764
12042
  updateSessionJob: () => updateSessionJob,
12043
+ recoverStaleProcessingJobs: () => recoverStaleProcessingJobs,
11765
12044
  listSessionJobs: () => listSessionJobs,
11766
12045
  getSessionJob: () => getSessionJob,
11767
12046
  getNextPendingJob: () => getNextPendingJob,
11768
- createSessionJob: () => createSessionJob
12047
+ createSessionJob: () => createSessionJob,
12048
+ claimSessionJob: () => claimSessionJob
11769
12049
  });
11770
12050
  function parseJobRow(row) {
11771
12051
  return {
@@ -11895,6 +12175,18 @@ function getNextPendingJob(db) {
11895
12175
  return null;
11896
12176
  return parseJobRow(row);
11897
12177
  }
12178
+ function claimSessionJob(id, db) {
12179
+ const d = db || getDatabase();
12180
+ const startedAt = now();
12181
+ const result = d.run("UPDATE session_memory_jobs SET status = 'processing', started_at = ? WHERE id = ? AND status = 'pending'", [startedAt, id]);
12182
+ return result.changes;
12183
+ }
12184
+ function recoverStaleProcessingJobs(maxAgeMs, db) {
12185
+ const d = db || getDatabase();
12186
+ const cutoff = new Date(Date.now() - maxAgeMs).toISOString();
12187
+ const result = d.run("UPDATE session_memory_jobs SET status = 'pending', started_at = NULL WHERE status = 'processing' AND started_at < ?", [cutoff]);
12188
+ return result.changes;
12189
+ }
11898
12190
  var init_session_jobs = __esm(() => {
11899
12191
  init_database();
11900
12192
  init_api_mode();
@@ -12433,7 +12725,11 @@ async function processSessionJob(jobId, db) {
12433
12725
  return result;
12434
12726
  }
12435
12727
  try {
12436
- updateSessionJob(jobId, { status: "processing", started_at: new Date().toISOString() }, db);
12728
+ const changes = claimSessionJob(jobId, db);
12729
+ if (changes === 0) {
12730
+ result.errors.push(`Job already claimed or not pending: ${jobId}`);
12731
+ return result;
12732
+ }
12437
12733
  } catch (e) {
12438
12734
  result.errors.push(`Failed to mark job as processing: ${String(e)}`);
12439
12735
  return result;
@@ -12556,6 +12852,9 @@ function startSessionQueueWorker() {
12556
12852
  return;
12557
12853
  _workerStarted = true;
12558
12854
  setInterval(() => {
12855
+ try {
12856
+ recoverStaleProcessingJobs(30 * 60 * 1000);
12857
+ } catch {}
12559
12858
  _processNext();
12560
12859
  }, 5000);
12561
12860
  }
@@ -12609,13 +12908,13 @@ __export(exports_session_registry, {
12609
12908
  closeRegistry: () => closeRegistry,
12610
12909
  cleanStaleSessions: () => cleanStaleSessions
12611
12910
  });
12612
- import { existsSync as existsSync8, mkdirSync as mkdirSync5 } from "fs";
12613
- import { dirname as dirname6, join as join9 } from "path";
12911
+ import { existsSync as existsSync9, mkdirSync as mkdirSync5 } from "fs";
12912
+ import { dirname as dirname6, join as join10 } from "path";
12614
12913
  function getDb() {
12615
12914
  if (_db2)
12616
12915
  return _db2;
12617
12916
  const dir = dirname6(DB_PATH);
12618
- if (!existsSync8(dir))
12917
+ if (!existsSync9(dir))
12619
12918
  mkdirSync5(dir, { recursive: true });
12620
12919
  _db2 = new SqliteAdapter(DB_PATH);
12621
12920
  _db2.run("PRAGMA journal_mode = WAL");
@@ -12790,7 +13089,7 @@ function closeRegistry() {
12790
13089
  var DB_PATH, _db2 = null;
12791
13090
  var init_session_registry = __esm(() => {
12792
13091
  init_storage();
12793
- DB_PATH = join9(process.env["HOME"] || process.env["USERPROFILE"] || "~", ".open-sessions-registry.db");
13092
+ DB_PATH = join10(process.env["HOME"] || process.env["USERPROFILE"] || "~", ".open-sessions-registry.db");
12794
13093
  });
12795
13094
 
12796
13095
  // src/db/pg-migrations.ts
@@ -27266,7 +27565,7 @@ class JSONSchemaGenerator {
27266
27565
  if (val === undefined) {
27267
27566
  if (this.unrepresentable === "throw") {
27268
27567
  throw new Error("Literal `undefined` cannot be represented in JSON Schema");
27269
- }
27568
+ } else {}
27270
27569
  } else if (typeof val === "bigint") {
27271
27570
  if (this.unrepresentable === "throw") {
27272
27571
  throw new Error("BigInt literals cannot be represented in JSON Schema");
@@ -48916,7 +49215,7 @@ var require_tracestate_impl = __commonJS((exports) => {
48916
49215
  const value = listMember.slice(i + 1, part.length);
48917
49216
  if ((0, tracestate_validators_1.validateKey)(key) && (0, tracestate_validators_1.validateValue)(value)) {
48918
49217
  agg.set(key, value);
48919
- }
49218
+ } else {}
48920
49219
  }
48921
49220
  return agg;
48922
49221
  }, new Map);
@@ -62271,8 +62570,8 @@ var {
62271
62570
 
62272
62571
  // src/cli/index.tsx
62273
62572
  init_database();
62274
- import { readFileSync as readFileSync9 } from "fs";
62275
- import { dirname as dirname8, join as join13 } from "path";
62573
+ import { readFileSync as readFileSync10 } from "fs";
62574
+ import { dirname as dirname8, join as join14 } from "path";
62276
62575
  import { fileURLToPath as fileURLToPath5 } from "url";
62277
62576
 
62278
62577
  // src/db/machines.ts
@@ -64271,6 +64570,18 @@ function registerCrudCommands(program2) {
64271
64570
  }
64272
64571
  resolvedAgentId = ag.id;
64273
64572
  }
64573
+ const requestedSource = opts.source;
64574
+ const claimsAgentAuthor = !requestedSource || requestedSource === "agent";
64575
+ if (!resolvedAgentId && claimsAgentAuthor) {
64576
+ const writingName = resolveWritingAgentName();
64577
+ if (writingName) {
64578
+ resolvedAgentId = getAgent(writingName)?.id ?? registerAgent(writingName).id;
64579
+ } else if (requestedSource === "agent") {
64580
+ throw new Error(`No agent identity: an agent-source save needs a writing agent, and none could be resolved.
64581
+ ` + `Pass --agent <name>, set MEMENTOS_AGENT, or write the fleet identity file ` + `(~/.hasna/conversations/agent-id, produced by "conversations agents register").
64582
+ ` + `To write without claiming an agent author, omit --source or pass ` + `--source user|system|auto|imported.`);
64583
+ }
64584
+ }
64274
64585
  const input = {
64275
64586
  key,
64276
64587
  value,
@@ -64280,7 +64591,7 @@ function registerCrudCommands(program2) {
64280
64591
  tags: mergedTags,
64281
64592
  summary: opts.summary,
64282
64593
  ttl_ms: opts.ttl ? parseDuration(opts.ttl) : undefined,
64283
- source: opts.source,
64594
+ source: requestedSource,
64284
64595
  agent_id: resolvedAgentId,
64285
64596
  session_id: globalOpts.session
64286
64597
  };
@@ -64347,6 +64658,10 @@ function registerCrudCommands(program2) {
64347
64658
  const updateInput = {
64348
64659
  version: existing.version
64349
64660
  };
64661
+ const writingName = resolveWritingAgentName();
64662
+ if (writingName) {
64663
+ updateInput.updated_by_agent = getAgent(writingName)?.id ?? registerAgent(writingName).id;
64664
+ }
64350
64665
  if (opts.value !== undefined)
64351
64666
  updateInput.value = opts.value;
64352
64667
  if (opts.importance !== undefined)
@@ -64374,7 +64689,7 @@ function registerCrudCommands(program2) {
64374
64689
  if (violation)
64375
64690
  throw new Error(formatEnumViolation(violation));
64376
64691
  }
64377
- const changedFields = Object.keys(updateInput).filter((k) => k !== "version");
64692
+ const changedFields = Object.keys(updateInput).filter((k) => k !== "version" && k !== "updated_by_agent");
64378
64693
  if (changedFields.length === 0) {
64379
64694
  throw new Error(`Nothing to update: no fields were given for ${existing.key} (${existing.id.slice(0, 8)}). ` + `Pass at least one of --value, --scope, --category, --status, --importance, --tags, --summary, --pin/--unpin. ` + `Note that -s is the global --session, not --scope.`);
64380
64695
  }
@@ -64404,8 +64719,7 @@ function registerCrudCommands(program2) {
64404
64719
  }
64405
64720
  const matches = getMemoriesByKey(keyOrId, opts.scope, opts.agent, opts.project);
64406
64721
  if (matches.length === 0) {
64407
- const looksLikeId = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(keyOrId);
64408
- if (isApiMode() && looksLikeId && deleteMemory(keyOrId)) {
64722
+ if (isApiMode() && keyOrId.length > 0 && deleteMemory(keyOrId)) {
64409
64723
  if (globalOpts.json) {
64410
64724
  outputJson({ deleted: keyOrId });
64411
64725
  } else {
@@ -64977,7 +65291,7 @@ function registerRemoveCommand(program2) {
64977
65291
  if (mem)
64978
65292
  id = mem.id;
64979
65293
  }
64980
- if (!id && isApiMode() && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(nameOrId)) {
65294
+ if (!id && isApiMode() && nameOrId.length > 0) {
64981
65295
  id = nameOrId;
64982
65296
  }
64983
65297
  if (!id) {
@@ -65094,6 +65408,7 @@ function registerRecallCommand(program2) {
65094
65408
  // src/cli/commands/memory-cmd-list.ts
65095
65409
  init_projects();
65096
65410
  init_memories();
65411
+ init_redact();
65097
65412
  init_helpers();
65098
65413
  import chalk12 from "chalk";
65099
65414
  import { resolve as resolve7 } from "path";
@@ -65141,36 +65456,37 @@ function registerListCommand(program2) {
65141
65456
  };
65142
65457
  }, limit, offset);
65143
65458
  const memories = hasMore && limit !== undefined ? collected.slice(0, limit) : collected;
65459
+ const sanitized = memories.map(redactMemoryForOutput);
65144
65460
  if (fmt === "json") {
65145
- outputJson(memories);
65461
+ outputJson(sanitized);
65146
65462
  return;
65147
65463
  }
65148
65464
  if (fmt === "csv") {
65149
65465
  console.log("key,value,scope,category,importance,id");
65150
- for (const m of memories) {
65466
+ for (const m of sanitized) {
65151
65467
  const v = m.value.replace(/"/g, '""');
65152
65468
  console.log(`"${m.key}","${v}",${m.scope},${m.category},${m.importance},${m.id.slice(0, 8)}`);
65153
65469
  }
65154
65470
  return;
65155
65471
  }
65156
65472
  if (fmt === "yaml") {
65157
- outputYaml(memories);
65473
+ outputYaml(sanitized);
65158
65474
  return;
65159
65475
  }
65160
- if (memories.length === 0) {
65476
+ if (sanitized.length === 0) {
65161
65477
  console.log(chalk12.yellow("No memories found."));
65162
65478
  return;
65163
65479
  }
65164
- console.log(chalk12.bold(`${memories.length}${hasMore ? "+" : ""} memor${memories.length === 1 ? "y" : "ies"}:`));
65165
- for (const m of memories) {
65480
+ console.log(chalk12.bold(`${sanitized.length}${hasMore ? "+" : ""} memor${sanitized.length === 1 ? "y" : "ies"}:`));
65481
+ for (const m of sanitized) {
65166
65482
  console.log(formatMemoryLine(m, {
65167
65483
  valueLength: opts.verbose ? 120 : 64,
65168
65484
  preferSummary: !opts.verbose
65169
65485
  }));
65170
65486
  }
65171
65487
  printPageHint({
65172
- shown: memories.length,
65173
- limit: limit ?? memories.length,
65488
+ shown: sanitized.length,
65489
+ limit: limit ?? sanitized.length,
65174
65490
  offset,
65175
65491
  hasMore,
65176
65492
  command: "mementos list",
@@ -65210,7 +65526,10 @@ function getMemoryStats(db) {
65210
65526
  const byCategory = d.query("SELECT category, COUNT(*) as c FROM memories WHERE status = 'active' GROUP BY category").all();
65211
65527
  const byStatus = d.query("SELECT status, COUNT(*) as c FROM memories WHERE status = 'active' GROUP BY status").all();
65212
65528
  const pinnedCount = d.query("SELECT COUNT(*) as c FROM memories WHERE pinned = 1 AND status = 'active'").get().c;
65213
- const expiredCount = d.query("SELECT COUNT(*) as c FROM memories WHERE status = 'expired' OR (expires_at IS NOT NULL AND expires_at < datetime('now'))").get().c;
65529
+ const expiredCount = d.query("SELECT COUNT(*) as c FROM memories WHERE status = 'expired'").get().c;
65530
+ const expiresAtCount = d.query("SELECT COUNT(*) as c FROM memories WHERE expires_at IS NOT NULL").get().c;
65531
+ const nowIso = new Date().toISOString();
65532
+ const expiredDueCount = d.query("SELECT COUNT(*) as c FROM memories WHERE status = 'expired' OR (expires_at IS NOT NULL AND expires_at < ?)").get(nowIso).c;
65214
65533
  const stats = {
65215
65534
  total,
65216
65535
  by_scope: { global: 0, shared: 0, private: 0, working: 0 },
@@ -65218,7 +65537,9 @@ function getMemoryStats(db) {
65218
65537
  by_status: { active: 0, archived: 0, expired: 0 },
65219
65538
  by_agent: {},
65220
65539
  pinned_count: pinnedCount,
65221
- expired_count: expiredCount
65540
+ expired_count: expiredCount,
65541
+ expires_at_count: expiresAtCount,
65542
+ expired_due_count: expiredDueCount
65222
65543
  };
65223
65544
  for (const row of byScope)
65224
65545
  if (row.scope in stats.by_scope)
@@ -65252,7 +65573,9 @@ function normalizeStats(data) {
65252
65573
  by_status: { active: 0, archived: 0, expired: 0, ...data?.by_status ?? {} },
65253
65574
  by_agent: data?.by_agent ?? {},
65254
65575
  pinned_count: data?.pinned_count ?? 0,
65255
- expired_count: data?.expired_count ?? 0
65576
+ expired_count: data?.expired_count ?? 0,
65577
+ expires_at_count: data?.expires_at_count ?? 0,
65578
+ expired_due_count: data?.expired_due_count ?? 0
65256
65579
  };
65257
65580
  }
65258
65581
  function getMemoryReport(filter = {}, db) {
@@ -65310,7 +65633,7 @@ function getStaleMemoriesPage(filter = {}, db) {
65310
65633
  const limit = filter.limit ?? 20;
65311
65634
  const offset = filter.offset ?? 0;
65312
65635
  if (!db && isApiMode()) {
65313
- const q = toQuery({ days, project_id: filter.project_id, agent_id: filter.agent_id, limit, offset });
65636
+ const q = toQuery({ days, project_id: filter.project_id, agent_id: filter.agent_id, pinned: filter.pinned, limit, offset });
65314
65637
  const { data } = apiJson("GET", `/memories/stale${q}`);
65315
65638
  const rows2 = data?.memories ?? [];
65316
65639
  return {
@@ -65322,8 +65645,14 @@ function getStaleMemoriesPage(filter = {}, db) {
65322
65645
  }
65323
65646
  const d = db || getDatabase();
65324
65647
  const cutoffDate = new Date(Date.now() - days * 86400000).toISOString();
65325
- const conds = ["status = 'active'", "(accessed_at IS NULL OR accessed_at < ?)", "pinned = 0"];
65648
+ const conds = ["status = 'active'", "(accessed_at IS NULL OR accessed_at < ?)"];
65326
65649
  const params = [cutoffDate];
65650
+ if (filter.pinned !== undefined) {
65651
+ conds.push("pinned = ?");
65652
+ params.push(filter.pinned ? 1 : 0);
65653
+ } else {
65654
+ conds.push("pinned = 0");
65655
+ }
65327
65656
  if (filter.project_id) {
65328
65657
  conds.push("project_id = ?");
65329
65658
  params.push(filter.project_id);
@@ -65500,7 +65829,7 @@ init_projects();
65500
65829
  init_helpers();
65501
65830
  function registerStaleCommand(program2) {
65502
65831
  const handleError = makeHandleError(program2);
65503
- program2.command("stale").description("Find memories not accessed recently (for cleanup/review)").option("--days <n>", "Stale threshold in days (default: 30)", parseInt).option("--project <path>", "Project filter").option("--agent <name>", "Agent filter").option("--limit <n>", "Max results (default: 20)", 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").option("--verbose", "Show wider memory snippets").action((opts) => {
65832
+ program2.command("stale").description("Find memories not accessed recently (for cleanup/review)").option("--days <n>", "Stale threshold in days (default: 30)", parseInt).option("--pinned", "Review stale PINNED memories (default view excludes pinned)").option("--project <path>", "Project filter").option("--agent <name>", "Agent filter").option("--limit <n>", "Max results (default: 20)", 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").option("--verbose", "Show wider memory snippets").action((opts) => {
65504
65833
  try {
65505
65834
  const globalOpts = program2.opts();
65506
65835
  const days = opts.days || 30;
@@ -65521,6 +65850,7 @@ function registerStaleCommand(program2) {
65521
65850
  const { rows: collected, hasMore } = collectPagedRows((cursor, pageLimit) => {
65522
65851
  const page = getStaleMemoriesPage({
65523
65852
  days,
65853
+ pinned: opts.pinned ? true : undefined,
65524
65854
  project_id: projectId,
65525
65855
  agent_id: agentId,
65526
65856
  limit: pageLimit,
@@ -65822,7 +66152,7 @@ init_memories();
65822
66152
  init_helpers();
65823
66153
  import chalk18 from "chalk";
65824
66154
  import { resolve as resolve12 } from "path";
65825
- import { readFileSync as readFileSync4 } from "fs";
66155
+ import { readFileSync as readFileSync5 } from "fs";
65826
66156
  function registerImportCommand(program2) {
65827
66157
  const handleError = makeHandleError(program2);
65828
66158
  program2.command("import [file]").description("Import memories from a JSON file or stdin (use '-' or pipe data)").option("--overwrite", "Overwrite existing memories (default: merge)").action(async (file, opts) => {
@@ -65832,7 +66162,7 @@ function registerImportCommand(program2) {
65832
66162
  if (file === "-" || !file && !process.stdin.isTTY) {
65833
66163
  raw = await Bun.stdin.text();
65834
66164
  } else if (file) {
65835
- raw = readFileSync4(resolve12(file), "utf-8");
66165
+ raw = readFileSync5(resolve12(file), "utf-8");
65836
66166
  } else {
65837
66167
  console.error(chalk18.red("No input: provide a file path, use '-' for stdin, or pipe data."));
65838
66168
  process.exit(1);
@@ -65862,14 +66192,14 @@ function registerImportCommand(program2) {
65862
66192
  import chalk19 from "chalk";
65863
66193
 
65864
66194
  // src/lib/config.ts
65865
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync5, readdirSync, writeFileSync as writeFileSync3, unlinkSync as unlinkSync2, cpSync as cpSync2 } from "fs";
65866
- import { homedir as homedir3 } from "os";
65867
- import { basename, dirname as dirname4, join as join7, resolve as resolve13 } from "path";
66195
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync6, readdirSync, writeFileSync as writeFileSync3, unlinkSync as unlinkSync2, cpSync as cpSync2 } from "fs";
66196
+ import { homedir as homedir4 } from "os";
66197
+ import { basename, dirname as dirname4, join as join8, resolve as resolve13 } from "path";
65868
66198
  function isInMemoryDb2(path) {
65869
66199
  return path === ":memory:" || path.startsWith("file::memory:");
65870
66200
  }
65871
66201
  function homeDir() {
65872
- return process.env["HOME"] || process.env["USERPROFILE"] || homedir3();
66202
+ return process.env["HOME"] || process.env["USERPROFILE"] || homedir4();
65873
66203
  }
65874
66204
  var DEFAULT_CONFIG = {
65875
66205
  default_scope: "private",
@@ -65927,11 +66257,11 @@ function isValidCategory(value) {
65927
66257
  return VALID_CATEGORIES2.includes(value);
65928
66258
  }
65929
66259
  function loadConfig() {
65930
- const configPath = join7(homeDir(), ".hasna", "mementos", "config.json");
66260
+ const configPath = join8(homeDir(), ".hasna", "mementos", "config.json");
65931
66261
  let fileConfig = {};
65932
- if (existsSync4(configPath)) {
66262
+ if (existsSync5(configPath)) {
65933
66263
  try {
65934
- const raw = readFileSync5(configPath, "utf-8");
66264
+ const raw = readFileSync6(configPath, "utf-8");
65935
66265
  fileConfig = JSON.parse(raw);
65936
66266
  } catch {}
65937
66267
  }
@@ -65957,8 +66287,8 @@ function findFileWalkingUp(filename) {
65957
66287
  let dir = process.cwd();
65958
66288
  const legacyHomeMementosDb = resolve13(homeDir(), ".mementos", "mementos.db");
65959
66289
  while (true) {
65960
- const candidate = join7(dir, filename);
65961
- if (existsSync4(candidate) && resolve13(candidate) !== legacyHomeMementosDb) {
66290
+ const candidate = join8(dir, filename);
66291
+ if (existsSync5(candidate) && resolve13(candidate) !== legacyHomeMementosDb) {
65962
66292
  return candidate;
65963
66293
  }
65964
66294
  const parent = dirname4(dir);
@@ -65971,7 +66301,7 @@ function findFileWalkingUp(filename) {
65971
66301
  function findGitRoot2() {
65972
66302
  let dir = process.cwd();
65973
66303
  while (true) {
65974
- if (existsSync4(join7(dir, ".git"))) {
66304
+ if (existsSync5(join8(dir, ".git"))) {
65975
66305
  return dir;
65976
66306
  }
65977
66307
  const parent = dirname4(dir);
@@ -65982,27 +66312,27 @@ function findGitRoot2() {
65982
66312
  }
65983
66313
  }
65984
66314
  function profilesDir() {
65985
- return join7(homeDir(), ".hasna", "mementos", "profiles");
66315
+ return join8(homeDir(), ".hasna", "mementos", "profiles");
65986
66316
  }
65987
66317
  function globalConfigPath() {
65988
- return join7(homeDir(), ".hasna", "mementos", "config.json");
66318
+ return join8(homeDir(), ".hasna", "mementos", "config.json");
65989
66319
  }
65990
66320
  function readGlobalConfig() {
65991
66321
  const p = globalConfigPath();
65992
- if (!existsSync4(p))
66322
+ if (!existsSync5(p))
65993
66323
  return {};
65994
66324
  try {
65995
- return JSON.parse(readFileSync5(p, "utf-8"));
66325
+ return JSON.parse(readFileSync6(p, "utf-8"));
65996
66326
  } catch {
65997
66327
  return {};
65998
66328
  }
65999
66329
  }
66000
66330
  function readGlobalConfigForWrite() {
66001
66331
  const p = globalConfigPath();
66002
- if (!existsSync4(p))
66332
+ if (!existsSync5(p))
66003
66333
  return {};
66004
66334
  try {
66005
- const data = JSON.parse(readFileSync5(p, "utf-8"));
66335
+ const data = JSON.parse(readFileSync6(p, "utf-8"));
66006
66336
  if (data === null || typeof data !== "object" || Array.isArray(data)) {
66007
66337
  throw new Error("expected a JSON object");
66008
66338
  }
@@ -66035,13 +66365,13 @@ function setActiveProfile(name) {
66035
66365
  }
66036
66366
  function listProfiles() {
66037
66367
  const dir = profilesDir();
66038
- if (!existsSync4(dir))
66368
+ if (!existsSync5(dir))
66039
66369
  return [];
66040
66370
  return readdirSync(dir).filter((f) => f.endsWith(".db")).map((f) => basename(f, ".db")).sort();
66041
66371
  }
66042
66372
  function deleteProfile(name) {
66043
- const dbPath = join7(profilesDir(), `${name}.db`);
66044
- if (!existsSync4(dbPath))
66373
+ const dbPath = join8(profilesDir(), `${name}.db`);
66374
+ if (!existsSync5(dbPath))
66045
66375
  return false;
66046
66376
  unlinkSync2(dbPath);
66047
66377
  if (getActiveProfile() === name)
@@ -66050,10 +66380,10 @@ function deleteProfile(name) {
66050
66380
  }
66051
66381
  function getDbPath2() {
66052
66382
  const _home = homeDir();
66053
- const _newDir = join7(_home, ".hasna", "mementos");
66054
- const _oldDir = join7(_home, ".mementos");
66055
- if (!existsSync4(_newDir) && existsSync4(_oldDir)) {
66056
- mkdirSync3(join7(_home, ".hasna"), { recursive: true });
66383
+ const _newDir = join8(_home, ".hasna", "mementos");
66384
+ const _oldDir = join8(_home, ".mementos");
66385
+ if (!existsSync5(_newDir) && existsSync5(_oldDir)) {
66386
+ mkdirSync3(join8(_home, ".hasna"), { recursive: true });
66057
66387
  cpSync2(_oldDir, _newDir, { recursive: true });
66058
66388
  }
66059
66389
  const envDbPath = process.env["HASNA_MEMENTOS_DB_PATH"] ?? process.env["MEMENTOS_DB_PATH"];
@@ -66067,7 +66397,7 @@ function getDbPath2() {
66067
66397
  }
66068
66398
  const profile = getActiveProfile();
66069
66399
  if (profile) {
66070
- const profilePath = join7(profilesDir(), `${profile}.db`);
66400
+ const profilePath = join8(profilesDir(), `${profile}.db`);
66071
66401
  ensureDir2(dirname4(profilePath));
66072
66402
  return profilePath;
66073
66403
  }
@@ -66075,21 +66405,21 @@ function getDbPath2() {
66075
66405
  if (dbScope === "project") {
66076
66406
  const gitRoot = findGitRoot2();
66077
66407
  if (gitRoot) {
66078
- const dbPath = join7(gitRoot, ".mementos", "mementos.db");
66408
+ const dbPath = join8(gitRoot, ".mementos", "mementos.db");
66079
66409
  ensureDir2(dirname4(dbPath));
66080
66410
  return dbPath;
66081
66411
  }
66082
66412
  }
66083
- const found = findFileWalkingUp(join7(".mementos", "mementos.db"));
66413
+ const found = findFileWalkingUp(join8(".mementos", "mementos.db"));
66084
66414
  if (found) {
66085
66415
  return found;
66086
66416
  }
66087
- const fallback = join7(homeDir(), ".hasna", "mementos", "mementos.db");
66417
+ const fallback = join8(homeDir(), ".hasna", "mementos", "mementos.db");
66088
66418
  ensureDir2(dirname4(fallback));
66089
66419
  return fallback;
66090
66420
  }
66091
66421
  function ensureDir2(dir) {
66092
- if (!existsSync4(dir)) {
66422
+ if (!existsSync5(dir)) {
66093
66423
  mkdirSync3(dir, { recursive: true });
66094
66424
  }
66095
66425
  }
@@ -66207,7 +66537,7 @@ init_database();
66207
66537
  init_helpers();
66208
66538
  import chalk20 from "chalk";
66209
66539
  import { resolve as resolve14, dirname as dirname5 } from "path";
66210
- import { existsSync as existsSync5, statSync, copyFileSync, mkdirSync as mkdirSync4, readdirSync as readdirSync2 } from "fs";
66540
+ import { existsSync as existsSync6, statSync, copyFileSync, mkdirSync as mkdirSync4, readdirSync as readdirSync2 } from "fs";
66211
66541
  function registerBackupCommand(program2) {
66212
66542
  const handleError = makeHandleError(program2);
66213
66543
  program2.command("backup [path]").description("Backup the SQLite database to a file").option("--list", "List available backups in ~/.hasna/mementos/backups/").action((targetPath, opts) => {
@@ -66216,7 +66546,7 @@ function registerBackupCommand(program2) {
66216
66546
  const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
66217
66547
  const backupsDir = resolve14(home, ".hasna", "mementos", "backups");
66218
66548
  if (opts.list) {
66219
- if (!existsSync5(backupsDir)) {
66549
+ if (!existsSync6(backupsDir)) {
66220
66550
  if (globalOpts.json) {
66221
66551
  outputJson({ backups: [] });
66222
66552
  return;
@@ -66258,7 +66588,7 @@ function registerBackupCommand(program2) {
66258
66588
  return;
66259
66589
  }
66260
66590
  const dbPath = getDbPath();
66261
- if (!existsSync5(dbPath)) {
66591
+ if (!existsSync6(dbPath)) {
66262
66592
  console.error(chalk20.red(`Database not found at ${dbPath}`));
66263
66593
  process.exit(1);
66264
66594
  }
@@ -66266,7 +66596,7 @@ function registerBackupCommand(program2) {
66266
66596
  if (targetPath) {
66267
66597
  dest = resolve14(targetPath);
66268
66598
  } else {
66269
- if (!existsSync5(backupsDir)) {
66599
+ if (!existsSync6(backupsDir)) {
66270
66600
  mkdirSync4(backupsDir, { recursive: true });
66271
66601
  }
66272
66602
  const now3 = new Date;
@@ -66274,7 +66604,7 @@ function registerBackupCommand(program2) {
66274
66604
  dest = resolve14(backupsDir, `mementos-${ts}.db`);
66275
66605
  }
66276
66606
  const destDir = dirname5(dest);
66277
- if (!existsSync5(destDir)) {
66607
+ if (!existsSync6(destDir)) {
66278
66608
  mkdirSync4(destDir, { recursive: true });
66279
66609
  }
66280
66610
  copyFileSync(dbPath, dest);
@@ -66295,29 +66625,31 @@ function registerBackupCommand(program2) {
66295
66625
  // src/cli/commands/io-restore.ts
66296
66626
  init_database();
66297
66627
  init_api_mode();
66628
+ init_memories();
66298
66629
  init_helpers();
66299
66630
  import chalk21 from "chalk";
66300
66631
  import { resolve as resolve15 } from "path";
66301
- import { existsSync as existsSync6, statSync as statSync2, copyFileSync as copyFileSync2, readdirSync as readdirSync3 } from "fs";
66632
+ import { existsSync as existsSync7, statSync as statSync2, copyFileSync as copyFileSync2, readdirSync as readdirSync3 } from "fs";
66633
+ function readMemoriesFromBackup(source) {
66634
+ const { Database: Database3 } = __require("bun:sqlite");
66635
+ const backupDb = new Database3(source, { readonly: true });
66636
+ try {
66637
+ const rows = backupDb.query("SELECT * FROM memories").all();
66638
+ return { rows, count: rows.length };
66639
+ } finally {
66640
+ backupDb.close();
66641
+ }
66642
+ }
66302
66643
  function registerRestoreCommand(program2) {
66303
66644
  const handleError = makeHandleError(program2);
66304
66645
  program2.command("restore [file]").description("Restore the database from a backup file").option("--latest", "Restore the most recent backup from ~/.hasna/mementos/backups/").option("--force", "Skip confirmation and perform the restore").action((filePath, opts) => {
66305
66646
  try {
66306
66647
  const globalOpts = program2.opts();
66307
- if (isApiMode()) {
66308
- const msg = "restore operates on the local SQLite database and is not available in API mode (the self-hosted cloud store is authoritative). Unset HASNA_MEMENTOS_API_URL / HASNA_MEMENTOS_API_KEY to restore a local db.";
66309
- if (globalOpts.json) {
66310
- outputJson({ error: msg });
66311
- } else {
66312
- console.error(chalk21.red(msg));
66313
- }
66314
- process.exit(1);
66315
- }
66316
66648
  const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
66317
66649
  const backupsDir = resolve15(home, ".hasna", "mementos", "backups");
66318
66650
  let source;
66319
66651
  if (opts.latest) {
66320
- if (!existsSync6(backupsDir)) {
66652
+ if (!existsSync7(backupsDir)) {
66321
66653
  console.error(chalk21.red("No backups directory found."));
66322
66654
  process.exit(1);
66323
66655
  }
@@ -66337,16 +66669,82 @@ function registerRestoreCommand(program2) {
66337
66669
  console.error(chalk21.red("Provide a backup file path or use --latest"));
66338
66670
  process.exit(1);
66339
66671
  }
66340
- if (!existsSync6(source)) {
66672
+ if (!existsSync7(source)) {
66341
66673
  console.error(chalk21.red(`Backup file not found: ${source}`));
66342
66674
  process.exit(1);
66343
66675
  }
66344
- const dbPath = getDbPath();
66345
66676
  const backupStat = statSync2(source);
66346
66677
  const backupSizeMB = (backupStat.size / (1024 * 1024)).toFixed(1);
66347
66678
  const backupSizeStr = backupStat.size >= 1024 * 1024 ? `${backupSizeMB} MB` : `${(backupStat.size / 1024).toFixed(1)} KB`;
66679
+ if (isApiMode()) {
66680
+ const { rows, count } = readMemoriesFromBackup(source);
66681
+ if (!opts.force) {
66682
+ if (globalOpts.json) {
66683
+ outputJson({
66684
+ action: "restore",
66685
+ source,
66686
+ target: "cloud-api",
66687
+ backup_size: backupStat.size,
66688
+ backup_memories: count,
66689
+ status: "dry_run",
66690
+ message: "Restores the backup's memories into the hosted store; existing store rows are never overwritten. Use --force to confirm."
66691
+ });
66692
+ return;
66693
+ }
66694
+ console.log(chalk21.bold("Restore preview (hosted store):"));
66695
+ console.log(` Source: ${chalk21.cyan(source)} (${backupSizeStr})`);
66696
+ console.log(` Backup memories: ${chalk21.green(String(count))}`);
66697
+ console.log(` Target: hosted store (cloud-api)`);
66698
+ console.log(` Semantics: merges the backup's memories; existing rows are never overwritten`);
66699
+ console.log();
66700
+ console.log(chalk21.yellow("Use --force to confirm restore"));
66701
+ return;
66702
+ }
66703
+ const result = bulkUpsertMemories(rows);
66704
+ if (result.rejected > 0) {
66705
+ const msg = `${result.rejected} of ${result.total} memories were rejected and did not persist. See errors.`;
66706
+ if (globalOpts.json) {
66707
+ outputJson({
66708
+ action: "restore",
66709
+ status: "failed",
66710
+ source,
66711
+ target: "cloud-api",
66712
+ inserted: result.inserted,
66713
+ skipped: result.skipped,
66714
+ rejected: result.rejected,
66715
+ total: result.total,
66716
+ error: msg
66717
+ });
66718
+ } else {
66719
+ console.error(chalk21.red(msg));
66720
+ }
66721
+ process.exit(1);
66722
+ }
66723
+ if (globalOpts.json) {
66724
+ outputJson({
66725
+ action: "restore",
66726
+ source,
66727
+ target: "cloud-api",
66728
+ backup_size: backupStat.size,
66729
+ backup_memories: count,
66730
+ restored_memories: result.inserted,
66731
+ inserted: result.inserted,
66732
+ skipped: result.skipped,
66733
+ rejected: result.rejected,
66734
+ total: result.total,
66735
+ status: "completed"
66736
+ });
66737
+ return;
66738
+ }
66739
+ console.log(`Restored from: ${chalk21.green(source)}`);
66740
+ console.log(` Restored memories: ${chalk21.green(String(result.inserted))} (inserted)`);
66741
+ console.log(` Skipped (already present): ${chalk21.yellow(String(result.skipped))}`);
66742
+ console.log(` Rejected: ${chalk21.yellow(String(result.rejected))}`);
66743
+ return;
66744
+ }
66745
+ const dbPath = getDbPath();
66348
66746
  let currentCount = 0;
66349
- if (existsSync6(dbPath)) {
66747
+ if (existsSync7(dbPath)) {
66350
66748
  try {
66351
66749
  const db = getDatabase();
66352
66750
  const row = db.query("SELECT COUNT(*) as count FROM memories").get();
@@ -67600,11 +67998,11 @@ init_memories();
67600
67998
  init_agents();
67601
67999
  init_projects();
67602
68000
  import chalk27 from "chalk";
67603
- import { join as join8 } from "path";
67604
- import { homedir as homedir4 } from "os";
68001
+ import { join as join9 } from "path";
68002
+ import { homedir as homedir5 } from "os";
67605
68003
  import {
67606
- readFileSync as readFileSync6,
67607
- existsSync as existsSync7,
68004
+ readFileSync as readFileSync7,
68005
+ existsSync as existsSync8,
67608
68006
  accessSync,
67609
68007
  statSync as statSync3,
67610
68008
  constants as fsConstants
@@ -67639,7 +68037,7 @@ function registerDoctorCommand(program2) {
67639
68037
  checks.push({ name: "Version", status: "ok", detail: version });
67640
68038
  const dbPath = getDbPath();
67641
68039
  let db = null;
67642
- if (dbPath !== ":memory:" && existsSync7(dbPath)) {
68040
+ if (dbPath !== ":memory:" && existsSync8(dbPath)) {
67643
68041
  try {
67644
68042
  accessSync(dbPath, fsConstants.R_OK | fsConstants.W_OK);
67645
68043
  checks.push({ name: "Database file", status: "ok", detail: dbPath });
@@ -67658,7 +68056,7 @@ function registerDoctorCommand(program2) {
67658
68056
  checks.push({ name: "Database connection", status: "fail", detail: e instanceof Error ? e.message : String(e) });
67659
68057
  }
67660
68058
  try {
67661
- if (dbPath !== ":memory:" && existsSync7(dbPath)) {
68059
+ if (dbPath !== ":memory:" && existsSync8(dbPath)) {
67662
68060
  const stats = statSync3(dbPath);
67663
68061
  const sizeKb = (stats.size / 1024).toFixed(1);
67664
68062
  const sizeMb = (stats.size / (1024 * 1024)).toFixed(2);
@@ -67791,9 +68189,9 @@ function registerDoctorCommand(program2) {
67791
68189
  checks.push({ name: "MCP server", status: "warn", detail: "could not check (is claude CLI installed?)" });
67792
68190
  }
67793
68191
  try {
67794
- const settingsFilePath = join8(homedir4(), ".claude", "settings.json");
67795
- if (existsSync7(settingsFilePath)) {
67796
- const settings = JSON.parse(readFileSync6(settingsFilePath, "utf-8"));
68192
+ const settingsFilePath = join9(homedir5(), ".claude", "settings.json");
68193
+ if (existsSync8(settingsFilePath)) {
68194
+ const settings = JSON.parse(readFileSync7(settingsFilePath, "utf-8"));
67797
68195
  const hooksObj = settings["hooks"] || {};
67798
68196
  const stopHooks = hooksObj["Stop"] || [];
67799
68197
  const hasMementos = stopHooks.some((e) => e.hooks?.some((h) => h.command && h.command.includes("mementos")));
@@ -67809,11 +68207,11 @@ function registerDoctorCommand(program2) {
67809
68207
  checks.push({ name: "Stop hook", status: "warn", detail: "could not check stop hook" });
67810
68208
  }
67811
68209
  if (process.platform === "darwin") {
67812
- const plistFilePath = join8(homedir4(), "Library", "LaunchAgents", "com.hasna.mementos.plist");
68210
+ const plistFilePath = join9(homedir5(), "Library", "LaunchAgents", "com.hasna.mementos.plist");
67813
68211
  checks.push({
67814
68212
  name: "Auto-start",
67815
- status: existsSync7(plistFilePath) ? "ok" : "warn",
67816
- detail: existsSync7(plistFilePath) ? "configured (starts on login)" : "not configured \u2192 run: mementos init"
68213
+ status: existsSync8(plistFilePath) ? "ok" : "warn",
68214
+ detail: existsSync8(plistFilePath) ? "configured (starts on login)" : "not configured \u2192 run: mementos init"
67817
68215
  });
67818
68216
  } else {
67819
68217
  checks.push({ name: "Auto-start", status: "ok", detail: `n/a on ${process.platform}` });
@@ -67892,9 +68290,9 @@ async function runCloudDoctor(globalOpts, checks) {
67892
68290
  checks.push({ name: "MCP server", status: "warn", detail: "could not check (is claude CLI installed?)" });
67893
68291
  }
67894
68292
  try {
67895
- const settingsFilePath = join8(homedir4(), ".claude", "settings.json");
67896
- if (existsSync7(settingsFilePath)) {
67897
- const settings = JSON.parse(readFileSync6(settingsFilePath, "utf-8"));
68293
+ const settingsFilePath = join9(homedir5(), ".claude", "settings.json");
68294
+ if (existsSync8(settingsFilePath)) {
68295
+ const settings = JSON.parse(readFileSync7(settingsFilePath, "utf-8"));
67898
68296
  const hooksObj = settings["hooks"] || {};
67899
68297
  const stopHooks = hooksObj["Stop"] || [];
67900
68298
  const hasMementos = stopHooks.some((e) => e.hooks?.some((h) => h.command && h.command.includes("mementos")));
@@ -67910,11 +68308,11 @@ async function runCloudDoctor(globalOpts, checks) {
67910
68308
  checks.push({ name: "Stop hook", status: "warn", detail: "could not check stop hook" });
67911
68309
  }
67912
68310
  if (process.platform === "darwin") {
67913
- const plistFilePath = join8(homedir4(), "Library", "LaunchAgents", "com.hasna.mementos.plist");
68311
+ const plistFilePath = join9(homedir5(), "Library", "LaunchAgents", "com.hasna.mementos.plist");
67914
68312
  checks.push({
67915
68313
  name: "Auto-start",
67916
- status: existsSync7(plistFilePath) ? "ok" : "warn",
67917
- detail: existsSync7(plistFilePath) ? "configured (starts on login)" : "not configured \u2192 run: mementos init"
68314
+ status: existsSync8(plistFilePath) ? "ok" : "warn",
68315
+ detail: existsSync8(plistFilePath) ? "configured (starts on login)" : "not configured \u2192 run: mementos init"
67918
68316
  });
67919
68317
  } else {
67920
68318
  checks.push({ name: "Auto-start", status: "ok", detail: `n/a on ${process.platform}` });
@@ -68358,7 +68756,7 @@ By type:`));
68358
68756
  webhooksCmd.command("create <type> <url>").description("Create a persistent webhook hook").option("--blocking", "Block the operation until the webhook responds").option("--priority <n>", "Hook priority (default 50)", "50").option("--agent <id>", "Scope to specific agent").option("--project <id>", "Scope to specific project").option("--description <text>", "Human-readable description").action(async (type, url, opts) => {
68359
68757
  const { createWebhookHook: createWebhookHook2 } = await Promise.resolve().then(() => (init_webhook_hooks(), exports_webhook_hooks));
68360
68758
  const { reloadWebhooks: reloadWebhooks2 } = await Promise.resolve().then(() => (init_built_in_hooks(), exports_built_in_hooks));
68361
- const wh = createWebhookHook2({
68759
+ const wh = await createWebhookHook2({
68362
68760
  type,
68363
68761
  handlerUrl: url,
68364
68762
  blocking: opts.blocking ?? false,
@@ -68367,7 +68765,7 @@ By type:`));
68367
68765
  projectId: opts.project,
68368
68766
  description: opts.description
68369
68767
  });
68370
- reloadWebhooks2();
68768
+ await reloadWebhooks2();
68371
68769
  console.log(chalk31.green("\u2713 Webhook created"));
68372
68770
  console.log(` ID: ${chalk31.cyan(wh.id)}`);
68373
68771
  console.log(` Type: ${wh.type}`);
@@ -68388,7 +68786,7 @@ By type:`));
68388
68786
  const { reloadWebhooks: reloadWebhooks2 } = await Promise.resolve().then(() => (init_built_in_hooks(), exports_built_in_hooks));
68389
68787
  const updated = updateWebhookHook2(id, { enabled: true });
68390
68788
  if (updated) {
68391
- reloadWebhooks2();
68789
+ await reloadWebhooks2();
68392
68790
  console.log(chalk31.green(`\u2713 Webhook ${id} enabled`));
68393
68791
  } else {
68394
68792
  console.error(chalk31.red(`Webhook not found: ${id}`));
@@ -68400,7 +68798,7 @@ By type:`));
68400
68798
  const { reloadWebhooks: reloadWebhooks2 } = await Promise.resolve().then(() => (init_built_in_hooks(), exports_built_in_hooks));
68401
68799
  const updated = updateWebhookHook2(id, { enabled: false });
68402
68800
  if (updated) {
68403
- reloadWebhooks2();
68801
+ await reloadWebhooks2();
68404
68802
  console.log(chalk31.yellow(`\u2298 Webhook ${id} disabled`));
68405
68803
  } else {
68406
68804
  console.error(chalk31.red(`Webhook not found: ${id}`));
@@ -69440,6 +69838,7 @@ function syncMemoriesTable(source, target, local, direction, currentMachineId) {
69440
69838
  const countResult = source.get(`SELECT COUNT(*) as cnt FROM "${MEMORY_TABLE}"`);
69441
69839
  stat.total_rows = countResult?.cnt ?? 0;
69442
69840
  const rows = since ? source.all(`SELECT * FROM "${MEMORY_TABLE}" WHERE updated_at > ?`, since) : source.all(`SELECT * FROM "${MEMORY_TABLE}"`);
69841
+ const erroredRowIds = new Set;
69443
69842
  for (const row of rows) {
69444
69843
  try {
69445
69844
  const sourceMachine = sourceMachineRef(row, currentMachineId);
@@ -69498,18 +69897,34 @@ function syncMemoriesTable(source, target, local, direction, currentMachineId) {
69498
69897
  clearMemoryEmbedding(winnerDb, String(winner["id"]));
69499
69898
  stat.synced_rows++;
69500
69899
  } catch (error) {
69900
+ erroredRowIds.add(String(row["id"] ?? "unknown"));
69501
69901
  stat.errors.push(`Memory ${String(row["id"] ?? "unknown")}: ${error instanceof Error ? error.message : String(error)}`);
69502
69902
  }
69503
69903
  }
69504
69904
  if (rows.length === 0) {
69505
69905
  stat.skipped_rows = stat.total_rows;
69506
69906
  }
69507
- upsertMemorySyncMeta(local, {
69508
- table_name: MEMORY_TABLE,
69509
- direction,
69510
- last_synced_at: new Date().toISOString(),
69511
- last_synced_row_count: stat.synced_rows
69512
- });
69907
+ if (rows.length > 0 && stat.errors.length === 0) {
69908
+ let maxSyncedAt = null;
69909
+ for (const row of rows) {
69910
+ if (erroredRowIds.has(String(row["id"] ?? "unknown"))) {
69911
+ continue;
69912
+ }
69913
+ const value = row["updated_at"];
69914
+ if (typeof value === "string" && (maxSyncedAt === null || value > maxSyncedAt)) {
69915
+ maxSyncedAt = value;
69916
+ }
69917
+ }
69918
+ const nextCursor = maxSyncedAt ?? syncMeta?.last_synced_at ?? null;
69919
+ if (nextCursor !== null) {
69920
+ upsertMemorySyncMeta(local, {
69921
+ table_name: MEMORY_TABLE,
69922
+ direction,
69923
+ last_synced_at: nextCursor,
69924
+ last_synced_row_count: stat.synced_rows
69925
+ });
69926
+ }
69927
+ }
69513
69928
  } catch (error) {
69514
69929
  stat.errors.push(error instanceof Error ? error.message : String(error));
69515
69930
  }
@@ -69899,19 +70314,19 @@ function registerStorageCommands(program2) {
69899
70314
  // src/cli/commands/init.ts
69900
70315
  import chalk41 from "chalk";
69901
70316
  import {
69902
- readFileSync as readFileSync7,
70317
+ readFileSync as readFileSync8,
69903
70318
  writeFileSync as writeFileSync4,
69904
- existsSync as existsSync9,
70319
+ existsSync as existsSync10,
69905
70320
  copyFileSync as copyFileSync3,
69906
70321
  mkdirSync as mkdirSync6
69907
70322
  } from "fs";
69908
- import { dirname as dirname7, join as join10 } from "path";
69909
- import { homedir as homedir5 } from "os";
70323
+ import { dirname as dirname7, join as join11 } from "path";
70324
+ import { homedir as homedir6 } from "os";
69910
70325
  import { fileURLToPath as fileURLToPath4 } from "url";
69911
70326
  function registerInitCommand(program2) {
69912
70327
  program2.command("init").description("One-command setup: register MCP, install stop hook, configure auto-start").action(async () => {
69913
70328
  const { platform: platform2 } = process;
69914
- const home = homedir5();
70329
+ const home = homedir6();
69915
70330
  const isMac = platform2 === "darwin";
69916
70331
  console.log("");
69917
70332
  console.log(chalk41.bold(" mementos \u2014 setting up your memory layer"));
@@ -69966,17 +70381,17 @@ function registerInitCommand(program2) {
69966
70381
  } else {
69967
70382
  console.log(chalk41.green(" \u2713 MCP server registered with Claude Code"));
69968
70383
  }
69969
- const hooksDir = join10(home, ".claude", "hooks");
69970
- const hookDest = join10(hooksDir, "mementos-stop-hook.ts");
69971
- const settingsPath = join10(home, ".claude", "settings.json");
70384
+ const hooksDir = join11(home, ".claude", "hooks");
70385
+ const hookDest = join11(hooksDir, "mementos-stop-hook.ts");
70386
+ const settingsPath = join11(home, ".claude", "settings.json");
69972
70387
  const hookCommand = `bun ${hookDest}`;
69973
70388
  let hookAlreadyInstalled = false;
69974
70389
  let hookError = null;
69975
70390
  try {
69976
70391
  let settings = {};
69977
- if (existsSync9(settingsPath)) {
70392
+ if (existsSync10(settingsPath)) {
69978
70393
  try {
69979
- settings = JSON.parse(readFileSync7(settingsPath, "utf-8"));
70394
+ settings = JSON.parse(readFileSync8(settingsPath, "utf-8"));
69980
70395
  } catch {
69981
70396
  settings = {};
69982
70397
  }
@@ -69987,19 +70402,19 @@ function registerInitCommand(program2) {
69987
70402
  if (alreadyHasMementos) {
69988
70403
  hookAlreadyInstalled = true;
69989
70404
  } else {
69990
- if (!existsSync9(hooksDir)) {
70405
+ if (!existsSync10(hooksDir)) {
69991
70406
  mkdirSync6(hooksDir, { recursive: true });
69992
70407
  }
69993
- if (!existsSync9(hookDest)) {
70408
+ if (!existsSync10(hookDest)) {
69994
70409
  const packageDir = dirname7(dirname7(fileURLToPath4(import.meta.url)));
69995
70410
  const candidatePaths = [
69996
- join10(packageDir, "scripts", "hooks", "claude-stop-hook.ts"),
69997
- join10(packageDir, "..", "scripts", "hooks", "claude-stop-hook.ts"),
69998
- join10(home, ".bun", "install", "global", "node_modules", "@hasna", "mementos", "scripts", "hooks", "claude-stop-hook.ts")
70411
+ join11(packageDir, "scripts", "hooks", "claude-stop-hook.ts"),
70412
+ join11(packageDir, "..", "scripts", "hooks", "claude-stop-hook.ts"),
70413
+ join11(home, ".bun", "install", "global", "node_modules", "@hasna", "mementos", "scripts", "hooks", "claude-stop-hook.ts")
69999
70414
  ];
70000
70415
  let hookSourceFound = false;
70001
70416
  for (const src of candidatePaths) {
70002
- if (existsSync9(src)) {
70417
+ if (existsSync10(src)) {
70003
70418
  copyFileSync3(src, hookDest);
70004
70419
  hookSourceFound = true;
70005
70420
  break;
@@ -70065,7 +70480,7 @@ main().catch(() => {});
70065
70480
  if (!isMac) {
70066
70481
  console.log(chalk41.dim(` \xB7 Auto-start skipped (not macOS \u2014 platform: ${platform2})`));
70067
70482
  } else {
70068
- const plistPath = join10(home, "Library", "LaunchAgents", "com.hasna.mementos.plist");
70483
+ const plistPath = join11(home, "Library", "LaunchAgents", "com.hasna.mementos.plist");
70069
70484
  const plistContent = `<?xml version="1.0" encoding="UTF-8"?>
70070
70485
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
70071
70486
  <plist version="1.0">
@@ -70090,11 +70505,11 @@ main().catch(() => {});
70090
70505
  </plist>
70091
70506
  `;
70092
70507
  try {
70093
- if (existsSync9(plistPath)) {
70508
+ if (existsSync10(plistPath)) {
70094
70509
  autoStartAlreadyInstalled = true;
70095
70510
  } else {
70096
- const launchAgentsDir = join10(home, "Library", "LaunchAgents");
70097
- if (!existsSync9(launchAgentsDir)) {
70511
+ const launchAgentsDir = join11(home, "Library", "LaunchAgents");
70512
+ if (!existsSync10(launchAgentsDir)) {
70098
70513
  mkdirSync6(launchAgentsDir, { recursive: true });
70099
70514
  }
70100
70515
  writeFileSync4(plistPath, plistContent, "utf-8");
@@ -70110,7 +70525,7 @@ main().catch(() => {});
70110
70525
  console.log(chalk41.green(" \u2713 Auto-start configured (starts on login)"));
70111
70526
  }
70112
70527
  if (!autoStartAlreadyInstalled && !autoStartError) {
70113
- const plistPath2 = join10(home, "Library", "LaunchAgents", "com.hasna.mementos.plist");
70528
+ const plistPath2 = join11(home, "Library", "LaunchAgents", "com.hasna.mementos.plist");
70114
70529
  const loadResult = await run(["launchctl", "load", plistPath2]);
70115
70530
  if (!loadResult.ok) {
70116
70531
  console.log(chalk41.dim(` \xB7 launchctl load: ${loadResult.output || "already loaded"}`));
@@ -71224,13 +71639,13 @@ function lessonTagForCli(kind) {
71224
71639
 
71225
71640
  // src/cli/brains.ts
71226
71641
  import {
71227
- existsSync as existsSync11,
71642
+ existsSync as existsSync12,
71228
71643
  mkdirSync as mkdirSync8,
71229
71644
  writeFileSync as writeFileSync6,
71230
71645
  readdirSync as readdirSync4
71231
71646
  } from "fs";
71232
- import { homedir as homedir7 } from "os";
71233
- import { join as join12 } from "path";
71647
+ import { homedir as homedir8 } from "os";
71648
+ import { join as join13 } from "path";
71234
71649
  import chalk43 from "chalk";
71235
71650
 
71236
71651
  // src/lib/gatherer.ts
@@ -71307,24 +71722,24 @@ var gatherTrainingData = async (options = {}) => {
71307
71722
  };
71308
71723
 
71309
71724
  // src/lib/model-config.ts
71310
- import { existsSync as existsSync10, mkdirSync as mkdirSync7, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
71311
- import { homedir as homedir6 } from "os";
71312
- import { join as join11 } from "path";
71725
+ import { existsSync as existsSync11, mkdirSync as mkdirSync7, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "fs";
71726
+ import { homedir as homedir7 } from "os";
71727
+ import { join as join12 } from "path";
71313
71728
  var DEFAULT_MODEL = "gpt-4o-mini";
71314
- var CONFIG_DIR = join11(homedir6(), ".hasna", "mementos");
71315
- var CONFIG_PATH = join11(CONFIG_DIR, "config.json");
71729
+ var CONFIG_DIR = join12(homedir7(), ".hasna", "mementos");
71730
+ var CONFIG_PATH = join12(CONFIG_DIR, "config.json");
71316
71731
  function readConfig() {
71317
- if (!existsSync10(CONFIG_PATH))
71732
+ if (!existsSync11(CONFIG_PATH))
71318
71733
  return {};
71319
71734
  try {
71320
- const raw = readFileSync8(CONFIG_PATH, "utf-8");
71735
+ const raw = readFileSync9(CONFIG_PATH, "utf-8");
71321
71736
  return JSON.parse(raw);
71322
71737
  } catch {
71323
71738
  return {};
71324
71739
  }
71325
71740
  }
71326
71741
  function writeConfig(config2) {
71327
- if (!existsSync10(CONFIG_DIR)) {
71742
+ if (!existsSync11(CONFIG_DIR)) {
71328
71743
  mkdirSync7(CONFIG_DIR, { recursive: true });
71329
71744
  }
71330
71745
  writeFileSync5(CONFIG_PATH, JSON.stringify(config2, null, 2) + `
@@ -71372,12 +71787,12 @@ function makeBrainsCommand() {
71372
71787
  limit: opts.limit,
71373
71788
  since
71374
71789
  });
71375
- const outputDir = opts.output ?? join12(homedir7(), ".hasna", "mementos", "training");
71376
- if (!existsSync11(outputDir)) {
71790
+ const outputDir = opts.output ?? join13(homedir8(), ".hasna", "mementos", "training");
71791
+ if (!existsSync12(outputDir)) {
71377
71792
  mkdirSync8(outputDir, { recursive: true });
71378
71793
  }
71379
71794
  const timestamp2 = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
71380
- const outputPath = join12(outputDir, `mementos-training-${timestamp2}.jsonl`);
71795
+ const outputPath = join13(outputDir, `mementos-training-${timestamp2}.jsonl`);
71381
71796
  const jsonl = result.examples.map((ex) => JSON.stringify(ex)).join(`
71382
71797
  `);
71383
71798
  writeFileSync6(outputPath, jsonl + `
@@ -71401,8 +71816,8 @@ function makeBrainsCommand() {
71401
71816
  try {
71402
71817
  let datasetPath = opts.dataset;
71403
71818
  if (!datasetPath) {
71404
- const trainingDir = join12(homedir7(), ".hasna", "mementos", "training");
71405
- if (!existsSync11(trainingDir)) {
71819
+ const trainingDir = join13(homedir8(), ".hasna", "mementos", "training");
71820
+ if (!existsSync12(trainingDir)) {
71406
71821
  printError("No training data found. Run `mementos brains gather` first.");
71407
71822
  process.exit(1);
71408
71823
  }
@@ -71412,9 +71827,9 @@ function makeBrainsCommand() {
71412
71827
  printError("No JSONL training files found. Run `mementos brains gather` first.");
71413
71828
  process.exit(1);
71414
71829
  }
71415
- datasetPath = join12(trainingDir, latestFile);
71830
+ datasetPath = join13(trainingDir, latestFile);
71416
71831
  }
71417
- if (!datasetPath || !existsSync11(datasetPath)) {
71832
+ if (!datasetPath || !existsSync12(datasetPath)) {
71418
71833
  printError(`Dataset file not found: ${datasetPath ?? "(unresolved)"}`);
71419
71834
  process.exit(1);
71420
71835
  }
@@ -71540,8 +71955,8 @@ function registerAllCommands(program2) {
71540
71955
  // src/cli/index.tsx
71541
71956
  function getPackageVersion2() {
71542
71957
  try {
71543
- const pkgPath = join13(dirname8(fileURLToPath5(import.meta.url)), "..", "..", "package.json");
71544
- const pkg = JSON.parse(readFileSync9(pkgPath, "utf-8"));
71958
+ const pkgPath = join14(dirname8(fileURLToPath5(import.meta.url)), "..", "..", "package.json");
71959
+ const pkg = JSON.parse(readFileSync10(pkgPath, "utf-8"));
71545
71960
  return pkg.version || "0.0.0";
71546
71961
  } catch {
71547
71962
  return "0.0.0";