hippo-memory 1.72.0 → 2.0.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 (259) hide show
  1. package/README.md +6 -4
  2. package/dist/agent-memories/apply.js +5 -5
  3. package/dist/api/assemble.js +2 -1
  4. package/dist/api/audit.d.ts +7 -7
  5. package/dist/api/audit.js +19 -17
  6. package/dist/api/auth.d.ts +5 -6
  7. package/dist/api/auth.js +28 -33
  8. package/dist/api/conflicts.js +1 -1
  9. package/dist/api/context-auto.js +4 -2
  10. package/dist/api/context-select.js +8 -20
  11. package/dist/api/context.js +12 -10
  12. package/dist/api/drill-down.js +2 -1
  13. package/dist/api/forget.d.ts +2 -2
  14. package/dist/api/forget.js +6 -7
  15. package/dist/api/graph.js +2 -1
  16. package/dist/api/learn.d.ts +1 -1
  17. package/dist/api/learn.js +45 -44
  18. package/dist/api/memories.d.ts +4 -2
  19. package/dist/api/memories.js +16 -5
  20. package/dist/api/objects.js +2 -1
  21. package/dist/api/outcome.d.ts +5 -5
  22. package/dist/api/outcome.js +16 -15
  23. package/dist/api/predictions.js +2 -1
  24. package/dist/api/promote.d.ts +3 -3
  25. package/dist/api/promote.js +25 -27
  26. package/dist/api/prompt-hook.d.ts +3 -1
  27. package/dist/api/prompt-hook.js +1 -0
  28. package/dist/api/quarantine.js +2 -1
  29. package/dist/api/recall-core.js +8 -3
  30. package/dist/api/recall-pipeline.js +17 -17
  31. package/dist/api/recall-types.d.ts +3 -1
  32. package/dist/api/recall.js +32 -29
  33. package/dist/api/remember-local.d.ts +5 -2
  34. package/dist/api/remember-local.js +14 -6
  35. package/dist/api/remember.d.ts +5 -2
  36. package/dist/api/remember.js +16 -14
  37. package/dist/api/session-end.d.ts +24 -0
  38. package/dist/api/session-end.js +31 -1
  39. package/dist/api/sleep-run.js +5 -4
  40. package/dist/api/sleep.js +4 -3
  41. package/dist/api/tokens.js +1 -1
  42. package/dist/api/types.d.ts +1 -7
  43. package/dist/capture/command.js +1 -1
  44. package/dist/capture/compaction-spool.d.ts +3 -5
  45. package/dist/capture/compaction-spool.js +41 -46
  46. package/dist/capture/session-end-caller.js +1 -1
  47. package/dist/capture/session-worker.js +19 -6
  48. package/dist/cli/audit.js +10 -10
  49. package/dist/cli/auth.js +4 -4
  50. package/dist/cli/client.d.ts +1 -2
  51. package/dist/cli/client.js +13 -13
  52. package/dist/cli/context.js +1 -1
  53. package/dist/cli/curate.d.ts +1 -1
  54. package/dist/cli/curate.js +9 -9
  55. package/dist/cli/decisions.js +4 -5
  56. package/dist/cli/hook-runtime.d.ts +3 -1
  57. package/dist/cli/hook-runtime.js +2 -2
  58. package/dist/cli/init.d.ts +1 -1
  59. package/dist/cli/init.js +9 -9
  60. package/dist/cli/object-verbs.d.ts +2 -2
  61. package/dist/cli/object-verbs.js +4 -4
  62. package/dist/cli/predictions.js +2 -3
  63. package/dist/cli/quality-repair-once.d.ts +1 -1
  64. package/dist/cli/quality-repair-once.js +2 -2
  65. package/dist/cli/refine-llm.js +2 -1
  66. package/dist/cli/remember.js +5 -5
  67. package/dist/cli/serve.js +9 -4
  68. package/dist/cli/session-hooks.d.ts +0 -4
  69. package/dist/cli/session-hooks.js +57 -66
  70. package/dist/cli/setup.d.ts +1 -1
  71. package/dist/cli/setup.js +2 -2
  72. package/dist/cli/shared.d.ts +1 -1
  73. package/dist/cli/shared.js +2 -2
  74. package/dist/cli/sleep.js +2 -2
  75. package/dist/cli/transfer.d.ts +3 -3
  76. package/dist/cli/transfer.js +34 -15
  77. package/dist/cli/usage.d.ts +1 -1
  78. package/dist/cli/usage.js +1 -1
  79. package/dist/cli/verb-row.d.ts +2 -0
  80. package/dist/cli/verbs/health.js +2 -0
  81. package/dist/cli/verbs/memory.js +43 -51
  82. package/dist/cli/verbs/objects.js +6 -3
  83. package/dist/cli/verbs/sessions.js +4 -2
  84. package/dist/cli.js +1 -2
  85. package/dist/connectors/dlq.js +2 -1
  86. package/dist/connectors/github/deletion.js +2 -1
  87. package/dist/connectors/github/ingest.js +2 -1
  88. package/dist/connectors/github/tenant-routing.js +2 -1
  89. package/dist/connectors/github/webhook.js +4 -2
  90. package/dist/connectors/ingest.js +2 -1
  91. package/dist/connectors/slack/deletion.js +2 -1
  92. package/dist/connectors/slack/ingest.js +2 -1
  93. package/dist/connectors/slack/tenant-routing.js +2 -1
  94. package/dist/consolidate/conflicts.js +1 -1
  95. package/dist/consolidate/dag.js +14 -9
  96. package/dist/consolidate/dedupe.js +1 -1
  97. package/dist/consolidate/memory-value.js +1 -1
  98. package/dist/consolidate/merge.js +6 -6
  99. package/dist/consolidate/sleep.js +1 -1
  100. package/dist/consolidate/traces.js +2 -2
  101. package/dist/core/config.js +15 -16
  102. package/dist/core/markdown.js +2 -2
  103. package/dist/core/memory.d.ts +4 -4
  104. package/dist/core/memory.js +10 -5
  105. package/dist/core/physics.d.ts +3 -1
  106. package/dist/core/physics.js +17 -2
  107. package/dist/core/project-identity.js +2 -1
  108. package/dist/core/search-types.d.ts +1 -1
  109. package/dist/dashboard/dashboard-actions.d.ts +2 -2
  110. package/dist/dashboard/dashboard-actions.js +5 -5
  111. package/dist/dashboard/dashboard-queries.js +2 -7
  112. package/dist/dashboard/dashboard-snapshot.js +1 -3
  113. package/dist/dashboard/dashboard.js +16 -5
  114. package/dist/db/connect.d.ts +2 -1
  115. package/dist/db/connect.js +2 -1
  116. package/dist/db/migrate.js +14 -7
  117. package/dist/db/migrations/v11.js +1 -0
  118. package/dist/db/migrations/v14.js +1 -0
  119. package/dist/db/physics-state.d.ts +0 -4
  120. package/dist/db/physics-state.js +5 -31
  121. package/dist/db/request-stores.d.ts +1 -0
  122. package/dist/db/request-stores.js +2 -1
  123. package/dist/db/vector-store.js +2 -1
  124. package/dist/doctor.d.ts +3 -0
  125. package/dist/doctor.js +7 -7
  126. package/dist/embeddings/local.js +2 -2
  127. package/dist/embeddings/provider.js +1 -10
  128. package/dist/entry/store-kit.d.ts +37 -0
  129. package/dist/entry/store-kit.js +25 -0
  130. package/dist/entry/testing.d.ts +6 -0
  131. package/dist/entry/testing.js +7 -0
  132. package/dist/eval/judgment.js +1 -1
  133. package/dist/hooks/json-hooks.js +6 -2
  134. package/dist/importers/core.js +3 -3
  135. package/dist/importers/vault.d.ts +1 -1
  136. package/dist/importers/vault.js +18 -18
  137. package/dist/index.d.ts +2 -2
  138. package/dist/index.js +1 -1
  139. package/dist/learn/autolearn.d.ts +6 -2
  140. package/dist/learn/autolearn.js +8 -6
  141. package/dist/learn/extract.js +1 -1
  142. package/dist/learn/invalidation.d.ts +2 -1
  143. package/dist/learn/invalidation.js +10 -5
  144. package/dist/mcp/memory-tools.d.ts +1 -1
  145. package/dist/mcp/memory-tools.js +6 -9
  146. package/dist/mcp/request.js +20 -19
  147. package/dist/mcp/stdio.js +13 -8
  148. package/dist/rerankers/cross-encoder.js +10 -3
  149. package/dist/search/embed-memories.d.ts +5 -2
  150. package/dist/search/embed-memories.js +40 -28
  151. package/dist/search/engine.d.ts +8 -0
  152. package/dist/search/engine.js +10 -0
  153. package/dist/search/vector.js +5 -5
  154. package/dist/server/auth.d.ts +1 -1
  155. package/dist/server/auth.js +7 -8
  156. package/dist/server/boot.d.ts +2 -1
  157. package/dist/server/boot.js +28 -19
  158. package/dist/server/client-ip.d.ts +9 -4
  159. package/dist/server/client-ip.js +35 -17
  160. package/dist/server/route-table.js +5 -6
  161. package/dist/server/routes/memories.js +4 -2
  162. package/dist/server/sleep-offload.js +3 -3
  163. package/dist/server/tls.d.ts +7 -2
  164. package/dist/server/tls.js +17 -5
  165. package/dist/server/types.d.ts +7 -2
  166. package/dist/server.d.ts +4 -41
  167. package/dist/server.js +3 -30
  168. package/dist/sharing/global-store.d.ts +4 -5
  169. package/dist/sharing/global-store.js +6 -11
  170. package/dist/sharing/global-sync.js +1 -1
  171. package/dist/sharing/search-both.js +9 -2
  172. package/dist/sharing/share.js +2 -4
  173. package/dist/store/ambient.js +2 -1
  174. package/dist/store/candidates.js +18 -8
  175. package/dist/store/cards.js +5 -4
  176. package/dist/store/compactions-record.js +8 -7
  177. package/dist/store/conflicts.d.ts +7 -1
  178. package/dist/store/conflicts.js +6 -5
  179. package/dist/store/connectors/dlq.js +2 -1
  180. package/dist/store/connectors/event-log.d.ts +13 -0
  181. package/dist/store/connectors/event-log.js +24 -0
  182. package/dist/store/connectors/github.js +6 -8
  183. package/dist/store/connectors/slack.js +6 -10
  184. package/dist/store/dashboard-reads.js +1 -0
  185. package/dist/store/delete-and-batch.js +5 -4
  186. package/dist/store/delivery-recorder.d.ts +2 -2
  187. package/dist/store/delivery-recorder.js +1 -6
  188. package/dist/store/descendant-page.js +2 -1
  189. package/dist/store/dormant.js +7 -3
  190. package/dist/store/entry-reads.d.ts +25 -3
  191. package/dist/store/entry-reads.js +43 -12
  192. package/dist/store/entry-row.js +9 -2
  193. package/dist/store/entry-writes.d.ts +10 -7
  194. package/dist/store/entry-writes.js +23 -11
  195. package/dist/store/goals.js +3 -2
  196. package/dist/store/graph-reads.js +5 -4
  197. package/dist/store/graph-view-rows.js +2 -1
  198. package/dist/store/half-life.js +2 -2
  199. package/dist/store/handoffs.d.ts +9 -3
  200. package/dist/store/handoffs.js +4 -3
  201. package/dist/store/index.d.ts +2 -2
  202. package/dist/store/index.js +1 -1
  203. package/dist/store/mirrors.js +9 -4
  204. package/dist/store/open.d.ts +2 -1
  205. package/dist/store/open.js +2 -1
  206. package/dist/store/port.d.ts +59 -3
  207. package/dist/store/quality-repair.js +1 -1
  208. package/dist/store/recall-trace.js +2 -1
  209. package/dist/store/rejected-values.js +14 -9
  210. package/dist/store/rows.js +2 -2
  211. package/dist/store/rule-sql.d.ts +2 -0
  212. package/dist/store/rule-sql.js +12 -6
  213. package/dist/store/search-rows.d.ts +5 -5
  214. package/dist/store/search-rows.js +9 -12
  215. package/dist/store/set-aside.js +1 -1
  216. package/dist/store/sqlite/connector-writes-group.js +3 -1
  217. package/dist/store/sqlite/entry-writes-group.d.ts +3 -3
  218. package/dist/store/sqlite/entry-writes-group.js +1 -1
  219. package/dist/store/sqlite/held-connection.d.ts +2 -0
  220. package/dist/store/sqlite/held-connection.js +1 -1
  221. package/dist/store/sqlite/last-recall-group.d.ts +13 -0
  222. package/dist/store/sqlite/last-recall-group.js +57 -0
  223. package/dist/store/sqlite/local.d.ts +0 -17
  224. package/dist/store/sqlite/local.js +0 -31
  225. package/dist/store/sqlite/maintenance-groups.d.ts +6 -0
  226. package/dist/store/sqlite/maintenance-groups.js +60 -0
  227. package/dist/store/sqlite/objects-group.js +14 -6
  228. package/dist/store/sqlite/store.d.ts +1 -8
  229. package/dist/store/sqlite/store.js +13 -24
  230. package/dist/store/summaries.d.ts +7 -0
  231. package/dist/store/summaries.js +49 -7
  232. package/dist/store/token-ledger-rows.js +3 -2
  233. package/dist/store/vector-index.js +5 -2
  234. package/dist/util/anthropic-messages.d.ts +3 -1
  235. package/dist/util/anthropic-messages.js +4 -2
  236. package/dist/util/atomic-write.d.ts +5 -2
  237. package/dist/util/atomic-write.js +12 -8
  238. package/dist/util/env.d.ts +10 -1
  239. package/dist/util/env.js +10 -1
  240. package/dist/util/format.d.ts +2 -0
  241. package/dist/util/format.js +4 -0
  242. package/dist/util/request-scope.d.ts +6 -2
  243. package/dist/util/request-scope.js +7 -3
  244. package/dist/util/spawn-argv.d.ts +9 -0
  245. package/dist/util/spawn-argv.js +57 -0
  246. package/dist/util/sql.d.ts +3 -0
  247. package/dist/util/sql.js +6 -0
  248. package/dist/util/vector-codec.d.ts +4 -0
  249. package/dist/util/vector-codec.js +15 -0
  250. package/dist/util/version.d.ts +1 -1
  251. package/dist/util/version.js +1 -1
  252. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  253. package/extensions/openclaw-plugin/package.json +1 -1
  254. package/openclaw.plugin.json +1 -1
  255. package/package.json +3 -1
  256. package/dist/api/on-store.d.ts +0 -16
  257. package/dist/api/on-store.js +0 -38
  258. package/dist/cli/lenient-id.d.ts +0 -2
  259. package/dist/cli/lenient-id.js +0 -13
@@ -1,3 +1,4 @@
1
+ import { placeholders } from '../util/sql.js';
1
2
  export function insertTokenRow(db, row) {
2
3
  db.prepare(`INSERT INTO token_ledger (ts, tenant_id, session_id, surface, event, items, tokens, block_hash)
3
4
  VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(row.ts, row.tenantId, row.sessionId, row.surface, row.event, row.items, row.tokens, row.blockHash);
@@ -34,7 +35,7 @@ export function surfaceTotals(db, tenantId, sinceIso) {
34
35
  export function sessionTotals(db, tenantId, sinceIso, rereadSurfaces) {
35
36
  // SAFETY: the SELECT names exactly these four aggregate columns.
36
37
  return db.prepare(`SELECT COUNT(DISTINCT session_id) AS sessions,
37
- COUNT(DISTINCT CASE WHEN event <> 'reset' AND surface IN (${rereadSurfaces.map(() => '?').join(', ')}) THEN session_id END) AS hook_sessions,
38
+ COUNT(DISTINCT CASE WHEN event <> 'reset' AND surface IN (${placeholders(rereadSurfaces.length)}) THEN session_id END) AS hook_sessions,
38
39
  COUNT(DISTINCT CASE WHEN event = 'reread' THEN session_id END) AS reread_sessions,
39
40
  SUM(CASE WHEN event = 'inject' THEN tokens ELSE 0 END) AS tokens
40
41
  FROM token_ledger
@@ -53,7 +54,7 @@ export function sessionTokenRows(db, tenantId, sinceIso) {
53
54
  export function injectedBlocks(db, tenantId, sessionId, surfaces) {
54
55
  // SAFETY: the SELECT names exactly these three columns.
55
56
  return db.prepare(`SELECT ts, surface, tokens FROM token_ledger
56
- WHERE tenant_id = ? AND session_id = ? AND event = 'inject' AND surface IN (${surfaces.map(() => '?').join(', ')})`).all(tenantId, sessionId, ...surfaces);
57
+ WHERE tenant_id = ? AND session_id = ? AND event = 'inject' AND surface IN (${placeholders(surfaces.length)})`).all(tenantId, sessionId, ...surfaces);
57
58
  }
58
59
  export function deleteRereadRows(db, tenantId, sessionId) {
59
60
  db.prepare(`DELETE FROM token_ledger WHERE tenant_id = ? AND session_id = ? AND event = 'reread'`).run(tenantId, sessionId);
@@ -2,11 +2,14 @@
2
2
  import { getMeta, setMeta, withWriteScope } from '../db/index.js';
3
3
  import { onHandle } from './open.js';
4
4
  import { DEFAULT_EMBEDDING_MODEL } from '../embeddings/local.js';
5
- import { float32ToBuffer, initializeParticle, loadPhysicsState, resetAllPhysicsState, savePhysicsState } from '../db/physics-state.js';
5
+ import { loadPhysicsState, resetAllPhysicsState, savePhysicsState } from '../db/physics-state.js';
6
+ import { initializeParticle } from '../core/physics.js';
7
+ import { float32ToBuffer } from '../util/vector-codec.js';
6
8
  import { EMBEDDING_MODEL_META_KEY, deleteOrphanVectors, hasStoredVectors, loadVectors, loadVectorViews, replaceAllVectors, storedVectorDims, storedVectorIds, upsertVectors, } from '../db/vector-store.js';
7
9
  import { selectEntriesByIds } from './entry-reads.js';
8
10
  import { chunked } from '../util/chunked.js';
9
11
  import { MEMORY_SELECT_COLUMNS, rowToEntry } from './rows.js';
12
+ import { placeholders } from '../util/sql.js';
10
13
  const MAX_BACKFILL_PAGE = 500;
11
14
  /** The model the index was built by: the stored identity, else the default model when vectors exist. */
12
15
  export function indexedModel(state) {
@@ -98,7 +101,7 @@ function ownedIds(db, ids, tenantId) {
98
101
  const owned = new Set();
99
102
  for (const chunk of chunked([...new Set(ids)])) {
100
103
  // SAFETY: the SELECT names one column, id.
101
- const rows = db.prepare(`SELECT id FROM memories WHERE +tenant_id = ? AND id IN (${chunk.map(() => '?').join(', ')})`).all(tenantId, ...chunk);
104
+ const rows = db.prepare(`SELECT id FROM memories WHERE +tenant_id = ? AND id IN (${placeholders(chunk.length)})`).all(tenantId, ...chunk);
102
105
  for (const row of rows)
103
106
  owned.add(row.id);
104
107
  }
@@ -17,6 +17,8 @@ export type AnthropicMessageFailure = {
17
17
  } | {
18
18
  kind: 'unreadable';
19
19
  message: string;
20
+ } | {
21
+ kind: 'empty';
20
22
  };
21
23
  export type AnthropicMessageResult = {
22
24
  ok: true;
@@ -27,7 +29,7 @@ export type AnthropicMessageResult = {
27
29
  };
28
30
  /** True for a failure the next call would repeat, so a caller looping over items should stop rather than retry. */
29
31
  export declare function isPermanentFailure(failure: AnthropicMessageFailure): boolean;
30
- /** The reply's first text block, trimmed (empty when the reply has none); never throws. */
32
+ /** The reply's first text block, trimmed; a reply with no text is a failure of kind 'empty'. Never throws. */
31
33
  export declare function sendAnthropicMessage(req: AnthropicMessageRequest): Promise<AnthropicMessageResult>;
32
34
  /** The text extraction and DAG summaries hand to their `onError` callback. */
33
35
  export declare function describeMessageFailure(failure: AnthropicMessageFailure): string;
@@ -31,7 +31,7 @@ async function errorSnippet(res) {
31
31
  // Masked before the cut, so a key that straddles the cut is never left half shown.
32
32
  return redactSecretsStrict(body.replace(/\s+/g, ' ').trim()).slice(0, ERROR_SNIPPET_CHARS);
33
33
  }
34
- /** The reply's first text block, trimmed (empty when the reply has none); never throws. */
34
+ /** The reply's first text block, trimmed; a reply with no text is a failure of kind 'empty'. Never throws. */
35
35
  export async function sendAnthropicMessage(req) {
36
36
  let res;
37
37
  try {
@@ -58,7 +58,8 @@ export async function sendAnthropicMessage(req) {
58
58
  const data = await readCappedJson(res, MAX_REPLY_BYTES);
59
59
  if (!isJsonObject(data))
60
60
  return { ok: false, failure: { kind: 'unreadable', message: 'the reply is not a JSON object' } };
61
- return { ok: true, text: firstText(data) };
61
+ const text = firstText(data);
62
+ return text === '' ? { ok: false, failure: { kind: 'empty' } } : { ok: true, text };
62
63
  }
63
64
  catch (err) {
64
65
  return { ok: false, failure: { kind: 'unreadable', message: errorMessage(err) } };
@@ -70,6 +71,7 @@ export function describeMessageFailure(failure) {
70
71
  case 'request': return `request failed: ${failure.message}`;
71
72
  case 'http': return failure.detail ? `HTTP ${failure.status}: ${failure.detail}` : `HTTP ${failure.status}`;
72
73
  case 'unreadable': return `unparseable response: ${failure.message}`;
74
+ case 'empty': return 'the reply held no text';
73
75
  }
74
76
  }
75
77
  //# sourceMappingURL=anthropic-messages.js.map
@@ -1,4 +1,7 @@
1
1
  /** Replaces `file` through a flushed temp file and a rename, so a reader, or a crash at any point, finds the old content or the new, whole.
2
- * A file with other hard links, or a rename the platform refuses (logged), is written in place: a crash mid-write can leave it truncated. */
3
- export declare function writeFileAtomic(file: string, text: string): void;
2
+ * A file with other hard links, or a rename the platform refuses (logged), is written in place: a crash mid-write can leave it truncated.
3
+ * `durable: false` skips the fsyncs for derived files that can be rebuilt; a reader still never sees a torn file. */
4
+ export declare function writeFileAtomic(file: string, text: string, opts?: {
5
+ readonly durable?: boolean;
6
+ }): void;
4
7
  //# sourceMappingURL=atomic-write.d.ts.map
@@ -53,13 +53,14 @@ function removeTemp(tmp) {
53
53
  }
54
54
  }
55
55
  /** Flushed before the rename, or a crash soon after could leave the file's name on bytes that never reached the disk. */
56
- function writeFlushed(tmp, text, old) {
56
+ function writeFlushed(tmp, text, old, durable) {
57
57
  const fd = fs.openSync(tmp, 'w', (old?.mode ?? 0o666) & 0o777);
58
58
  try {
59
59
  fs.writeFileSync(fd, text, 'utf8');
60
60
  if (old)
61
61
  keepAccess(tmp, old);
62
- fs.fsyncSync(fd);
62
+ if (durable)
63
+ fs.fsyncSync(fd);
63
64
  }
64
65
  finally {
65
66
  fs.closeSync(fd);
@@ -82,17 +83,18 @@ function flushFolder(dir) {
82
83
  log.warn(`could not flush the folder ${dir} to disk after replacing a file in it: ${errorMessage(err)}`);
83
84
  }
84
85
  }
85
- function replaceViaTemp(target, text, old) {
86
+ function replaceViaTemp(target, text, old, durable) {
86
87
  const tmp = `${target}.${process.pid}.tmp`;
87
88
  try {
88
- writeFlushed(tmp, text, old);
89
+ writeFlushed(tmp, text, old, durable);
89
90
  renameOnto(tmp, target);
90
91
  }
91
92
  catch (err) {
92
93
  removeTemp(tmp);
93
94
  throw err;
94
95
  }
95
- flushFolder(path.dirname(target));
96
+ if (durable)
97
+ flushFolder(path.dirname(target));
96
98
  }
97
99
  /** The pre-rename write: it truncates first, so it is only the fallback, but it needs just a writable file and keeps hard links together. */
98
100
  function writeInPlace(target, text) {
@@ -109,8 +111,10 @@ function writeInPlace(target, text) {
109
111
  }
110
112
  }
111
113
  /** Replaces `file` through a flushed temp file and a rename, so a reader, or a crash at any point, finds the old content or the new, whole.
112
- * A file with other hard links, or a rename the platform refuses (logged), is written in place: a crash mid-write can leave it truncated. */
113
- export function writeFileAtomic(file, text) {
114
+ * A file with other hard links, or a rename the platform refuses (logged), is written in place: a crash mid-write can leave it truncated.
115
+ * `durable: false` skips the fsyncs for derived files that can be rebuilt; a reader still never sees a torn file. */
116
+ export function writeFileAtomic(file, text, opts = {}) {
117
+ const durable = opts.durable !== false;
114
118
  const target = writeTarget(file);
115
119
  const old = fs.statSync(target, { throwIfNoEntry: false });
116
120
  if (old === undefined) {
@@ -126,7 +130,7 @@ export function writeFileAtomic(file, text) {
126
130
  return;
127
131
  }
128
132
  try {
129
- replaceViaTemp(target, text, old);
133
+ replaceViaTemp(target, text, old, durable);
130
134
  }
131
135
  catch (err) {
132
136
  if (!(err instanceof Error) || !REPLACE_REFUSALS.includes(errnoCode(err)))
@@ -22,6 +22,8 @@ export declare function envHippoHome(): string | undefined;
22
22
  export declare function envXdgDataHome(): string | undefined;
23
23
  export declare function envHomeDir(): string | undefined;
24
24
  export declare function envPath(): string | undefined;
25
+ /** The extensions win32 tries on a bare command name, in order. */
26
+ export declare function envPathExt(): string | undefined;
25
27
  export declare function envModelCache(): string | undefined;
26
28
  export declare function envHippoSessionId(): string | undefined;
27
29
  export declare function envClaudeCodeSessionId(): string | undefined;
@@ -47,9 +49,15 @@ export declare function envPort(): string | undefined;
47
49
  /** How long a client waits for a running server's /health before writing to the store directly; server-detect.ts holds the default. */
48
50
  export declare function envHealthProbeMs(): number | undefined;
49
51
  export declare function envRequireAuth(): boolean;
50
- /** `1` lets a request made on this machine in with no key, as host admin; HIPPO_REQUIRE_AUTH=1 wins over it. */
52
+ /** `1` lets a request made on this machine in with no key; HIPPO_REQUIRE_AUTH=1 wins over it. */
51
53
  export declare function envAllowKeylessLocal(): boolean;
54
+ /** The role a keyless local request gets: `admin` (host admin) only when asked for by name, else `member`. */
55
+ export declare function envKeylessLocalRole(): 'admin' | 'member';
56
+ /** `1` lets a non-loopback bind start without TLS, for TLS that ends at a proxy in front. */
57
+ export declare function envAllowCleartext(): boolean;
52
58
  export declare function envV1Rps(): string | undefined;
59
+ /** Requests per second each authenticated caller may make; `0` turns the per-caller limit off. */
60
+ export declare function envCallerRps(): string | undefined;
53
61
  /** How long a request body may take to arrive; http-util.ts holds the default. */
54
62
  export declare function envBodyTimeoutMs(): number | undefined;
55
63
  /** How long POST /v1/sleep lets its consolidation run; server/sleep-offload.ts holds the default. */
@@ -63,6 +71,7 @@ export declare function envStoreQueueMax(): number | undefined;
63
71
  export declare function envDailyStepTimeoutMs(): number | undefined;
64
72
  export declare function envApiKey(): string | undefined;
65
73
  export declare function envClientIpHeader(): string | undefined;
74
+ /** The peers whose HIPPO_CLIENT_IP_HEADER is believed: IPs or CIDRs, or `*` for any peer. */
66
75
  export declare function envTrustedProxies(): string | undefined;
67
76
  /** Paths to the PEM certificate and key `hippo serve` answers HTTPS with; the --tls-cert and --tls-key flags win. */
68
77
  export declare function envTlsCert(): string | undefined;
package/dist/util/env.js CHANGED
@@ -64,6 +64,8 @@ export function envHippoHome() { return raw('HIPPO_HOME')?.trim(); }
64
64
  export function envXdgDataHome() { return raw('XDG_DATA_HOME')?.trim(); }
65
65
  export function envHomeDir() { return raw('HOME') || raw('USERPROFILE'); }
66
66
  export function envPath() { return raw('PATH'); }
67
+ /** The extensions win32 tries on a bare command name, in order. */
68
+ export function envPathExt() { return raw('PATHEXT'); }
67
69
  export function envModelCache() { return trimmed('HIPPO_MODEL_CACHE'); }
68
70
  // Session, scope and tenant.
69
71
  export function envHippoSessionId() { return trimmed('HIPPO_SESSION_ID'); }
@@ -94,9 +96,15 @@ export function envPort() { return raw('HIPPO_PORT'); }
94
96
  /** How long a client waits for a running server's /health before writing to the store directly; server-detect.ts holds the default. */
95
97
  export function envHealthProbeMs() { return positiveInt('HIPPO_HEALTH_PROBE_MS'); }
96
98
  export function envRequireAuth() { return isOne('HIPPO_REQUIRE_AUTH'); }
97
- /** `1` lets a request made on this machine in with no key, as host admin; HIPPO_REQUIRE_AUTH=1 wins over it. */
99
+ /** `1` lets a request made on this machine in with no key; HIPPO_REQUIRE_AUTH=1 wins over it. */
98
100
  export function envAllowKeylessLocal() { return isOne('HIPPO_ALLOW_KEYLESS_LOCAL'); }
101
+ /** The role a keyless local request gets: `admin` (host admin) only when asked for by name, else `member`. */
102
+ export function envKeylessLocalRole() { return trimmed('HIPPO_KEYLESS_LOCAL_ROLE')?.toLowerCase() === 'admin' ? 'admin' : 'member'; }
103
+ /** `1` lets a non-loopback bind start without TLS, for TLS that ends at a proxy in front. */
104
+ export function envAllowCleartext() { return isOne('HIPPO_ALLOW_CLEARTEXT'); }
99
105
  export function envV1Rps() { return raw('HIPPO_V1_RPS'); }
106
+ /** Requests per second each authenticated caller may make; `0` turns the per-caller limit off. */
107
+ export function envCallerRps() { return trimmed('HIPPO_CALLER_RPS'); }
100
108
  /** How long a request body may take to arrive; http-util.ts holds the default. */
101
109
  export function envBodyTimeoutMs() { return positiveInt('HIPPO_BODY_TIMEOUT_MS'); }
102
110
  /** How long POST /v1/sleep lets its consolidation run; server/sleep-offload.ts holds the default. */
@@ -110,6 +118,7 @@ export function envStoreQueueMax() { return positiveInt('HIPPO_STORE_QUEUE_MAX')
110
118
  export function envDailyStepTimeoutMs() { return positiveInt('HIPPO_DAILY_STEP_TIMEOUT_MS'); }
111
119
  export function envApiKey() { return raw('HIPPO_API_KEY'); }
112
120
  export function envClientIpHeader() { return raw('HIPPO_CLIENT_IP_HEADER')?.trim().toLowerCase(); }
121
+ /** The peers whose HIPPO_CLIENT_IP_HEADER is believed: IPs or CIDRs, or `*` for any peer. */
113
122
  export function envTrustedProxies() { return raw('HIPPO_TRUSTED_PROXIES'); }
114
123
  /** Paths to the PEM certificate and key `hippo serve` answers HTTPS with; the --tls-cert and --tls-key flags win. */
115
124
  export function envTlsCert() { return trimmed('HIPPO_TLS_CERT'); }
@@ -1,3 +1,5 @@
1
1
  /** A 0..1 fraction as a percentage with one decimal, e.g. 0.256 -> "25.6%". */
2
2
  export declare function pct(fraction: number): string;
3
+ /** Rounds to one decimal place. */
4
+ export declare function round1(value: number): number;
3
5
  //# sourceMappingURL=format.d.ts.map
@@ -3,4 +3,8 @@
3
3
  export function pct(fraction) {
4
4
  return (fraction * 100).toFixed(1) + '%';
5
5
  }
6
+ /** Rounds to one decimal place. */
7
+ export function round1(value) {
8
+ return Math.round(value * 10) / 10;
9
+ }
6
10
  //# sourceMappingURL=format.js.map
@@ -5,6 +5,8 @@ export interface RequestScope {
5
5
  readonly requestId: string | undefined;
6
6
  /** Set for a served request that must be answered by a given time. */
7
7
  readonly deadline?: RequestDeadline | undefined;
8
+ /** Aborts when the request passes its deadline, so a handler that checks it stops starting work nobody waits for. */
9
+ readonly signal?: AbortSignal | undefined;
8
10
  }
9
11
  /** How long a request may run: 120 s, or HIPPO_REQUEST_DEADLINE_MS, where 0 turns the deadline off. */
10
12
  export declare function requestDeadlineMs(): number;
@@ -24,7 +26,9 @@ export declare const requestScopes: AsyncLocalStorage<RequestScope>;
24
26
  export declare function currentRequestId(): string | undefined;
25
27
  /** The deadline of the request this code runs for, if it has one. */
26
28
  export declare function currentDeadline(): RequestDeadline | undefined;
29
+ /** The abort signal of the request this code runs for, if it has one. */
30
+ export declare function currentSignal(): AbortSignal | undefined;
27
31
  /** Runs `fn` with `requestId` on every log line it writes, including the ones from
28
- * timers and promises it starts. `deadline` reaches every store call made inside. */
29
- export declare function runWithRequestId<T>(requestId: string, fn: () => T, deadline?: RequestDeadline): T;
32
+ * timers and promises it starts. `deadline` reaches every store call made inside, and `signal` every check of `currentSignal`. */
33
+ export declare function runWithRequestId<T>(requestId: string, fn: () => T, deadline?: RequestDeadline, signal?: AbortSignal): T;
30
34
  //# sourceMappingURL=request-scope.d.ts.map
@@ -45,9 +45,13 @@ export function currentRequestId() {
45
45
  export function currentDeadline() {
46
46
  return requestScopes.getStore()?.deadline;
47
47
  }
48
+ /** The abort signal of the request this code runs for, if it has one. */
49
+ export function currentSignal() {
50
+ return requestScopes.getStore()?.signal;
51
+ }
48
52
  /** Runs `fn` with `requestId` on every log line it writes, including the ones from
49
- * timers and promises it starts. `deadline` reaches every store call made inside. */
50
- export function runWithRequestId(requestId, fn, deadline) {
51
- return requestScopes.run({ requestId, deadline }, fn);
53
+ * timers and promises it starts. `deadline` reaches every store call made inside, and `signal` every check of `currentSignal`. */
54
+ export function runWithRequestId(requestId, fn, deadline, signal) {
55
+ return requestScopes.run({ requestId, deadline, signal }, fn);
52
56
  }
53
57
  //# sourceMappingURL=request-scope.js.map
@@ -0,0 +1,9 @@
1
+ import { type ChildProcess, type SpawnOptions } from 'node:child_process';
2
+ /** Quotes one cmd.exe argument; each `%` leaves the quotes as `^%`, so no %NAME% pair can expand. */
3
+ export declare function quoteCmdArg(arg: string): string;
4
+ /** cmd.exe arguments that run a .cmd or .bat shim with every forwarded argument delivered verbatim. */
5
+ export declare function cmdShimArgs(shimPath: string, forwardArgs: readonly string[]): string[];
6
+ /** Spawns argv[0] with the rest as its arguments and no shell. Node refuses to start a .cmd or .bat without one (CVE-2024-27980),
7
+ * so on win32 such a shim runs through cmd.exe with every argument quoted for it. */
8
+ export declare function spawnArgv(argv: readonly string[], options: SpawnOptions): ChildProcess;
9
+ //# sourceMappingURL=spawn-argv.d.ts.map
@@ -0,0 +1,57 @@
1
+ // Starting a command from an argv list with no shell, Windows .cmd and .bat shims included, so no argument is ever parsed as shell syntax.
2
+ import { spawn } from 'node:child_process';
3
+ import * as fs from 'node:fs';
4
+ import * as path from 'node:path';
5
+ import { envPath, envPathExt } from './env.js';
6
+ /** Quotes one cmd.exe argument; each `%` leaves the quotes as `^%`, so no %NAME% pair can expand. */
7
+ export function quoteCmdArg(arg) {
8
+ if (arg.length === 0)
9
+ return '""';
10
+ if (!/[ \t"&()^<>|%!]/.test(arg))
11
+ return arg;
12
+ return `"${arg.replace(/"/g, '""').replace(/%/g, '"^%"')}"`;
13
+ }
14
+ /** cmd.exe arguments that run a .cmd or .bat shim with every forwarded argument delivered verbatim. */
15
+ export function cmdShimArgs(shimPath, forwardArgs) {
16
+ const command = `"${shimPath}"${forwardArgs.length > 0 ? ` ${forwardArgs.map(quoteCmdArg).join(' ')}` : ''}`;
17
+ // The line is already quoted for cmd.exe, so Node must not quote it again; /s strips the outer pair.
18
+ return ['/d', '/v:off', '/s', '/c', `"${command}"`];
19
+ }
20
+ const DEFAULT_PATHEXT = '.COM;.EXE;.BAT;.CMD';
21
+ function isFile(candidate) {
22
+ return fs.statSync(candidate, { throwIfNoEntry: false })?.isFile() === true;
23
+ }
24
+ /** The file `command` names on win32: as given when it has a folder, else the first hit in the working folder then PATH, trying each PATHEXT. */
25
+ function resolveWindowsCommand(command) {
26
+ const exts = (envPathExt() ?? DEFAULT_PATHEXT).split(';').filter((ext) => ext.length > 0);
27
+ const dirs = /[\\/]/.test(command) ? [''] : [process.cwd(), ...(envPath() ?? '').split(path.delimiter).filter((dir) => dir.length > 0)];
28
+ for (const dir of dirs) {
29
+ const base = dir === '' ? command : path.join(dir, command);
30
+ const candidates = path.extname(command) === '' ? exts.map((ext) => base + ext) : [base, ...exts.map((ext) => base + ext)];
31
+ const hit = candidates.find(isFile);
32
+ if (hit !== undefined)
33
+ return hit;
34
+ }
35
+ return null;
36
+ }
37
+ /** Spawns argv[0] with the rest as its arguments and no shell. Node refuses to start a .cmd or .bat without one (CVE-2024-27980),
38
+ * so on win32 such a shim runs through cmd.exe with every argument quoted for it. */
39
+ export function spawnArgv(argv, options) {
40
+ const [command, ...args] = argv;
41
+ if (command === undefined || command.length === 0)
42
+ throw new RangeError('spawnArgv needs a command: argv is empty');
43
+ if (process.platform === 'win32') {
44
+ const resolved = resolveWindowsCommand(command);
45
+ const ext = resolved === null ? '' : path.extname(resolved).toLowerCase();
46
+ if (resolved !== null && (ext === '.cmd' || ext === '.bat')) {
47
+ return spawn('cmd.exe', cmdShimArgs(resolved, args), {
48
+ ...options,
49
+ windowsHide: options.windowsHide ?? true,
50
+ shell: false,
51
+ windowsVerbatimArguments: true,
52
+ });
53
+ }
54
+ }
55
+ return spawn(command, args, { ...options, windowsHide: options.windowsHide ?? true, shell: false });
56
+ }
57
+ //# sourceMappingURL=spawn-argv.js.map
@@ -0,0 +1,3 @@
1
+ /** `n` bound-parameter marks joined for an IN list or VALUES row, e.g. 3 -> "?, ?, ?". */
2
+ export declare function placeholders(n: number): string;
3
+ //# sourceMappingURL=sql.d.ts.map
@@ -0,0 +1,6 @@
1
+ // SQL text helpers shared by every layer that builds an IN list or VALUES row.
2
+ /** `n` bound-parameter marks joined for an IN list or VALUES row, e.g. 3 -> "?, ?, ?". */
3
+ export function placeholders(n) {
4
+ return Array.from({ length: n }, () => '?').join(', ');
5
+ }
6
+ //# sourceMappingURL=sql.js.map
@@ -0,0 +1,4 @@
1
+ /** Float32 vector <-> Buffer codec for BLOB columns. */
2
+ export declare function float32ToBuffer(arr: number[]): Buffer;
3
+ export declare function bufferToFloat32(buf: Buffer | Uint8Array): number[];
4
+ //# sourceMappingURL=vector-codec.d.ts.map
@@ -0,0 +1,15 @@
1
+ /** Float32 vector <-> Buffer codec for BLOB columns. */
2
+ export function float32ToBuffer(arr) {
3
+ const f32 = new Float32Array(arr);
4
+ return Buffer.from(f32.buffer);
5
+ }
6
+ export function bufferToFloat32(buf) {
7
+ if (buf.byteLength === 0 || buf.byteLength % 4 !== 0)
8
+ return [];
9
+ // Ensure we have a properly aligned copy (SQLite may return Uint8Array, not Buffer)
10
+ const bytes = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
11
+ const aligned = new ArrayBuffer(bytes.length);
12
+ new Uint8Array(aligned).set(bytes);
13
+ return Array.from(new Float32Array(aligned));
14
+ }
15
+ //# sourceMappingURL=vector-codec.js.map
@@ -1,6 +1,6 @@
1
1
  /** Single source of truth for the package version, written by scripts/sync-version.mjs; never edit by hand. Read by the db rollback guard, /health, MCP.
2
2
  * A constant, not a runtime package.json read: the published bundle may lack package.json on a path ESM can resolve. */
3
- export declare const PACKAGE_VERSION = "1.72.0";
3
+ export declare const PACKAGE_VERSION = "2.0.0";
4
4
  /** The floor a store takes on its first expiring key: the first release with schema
5
5
  * v53, set by hand in that release; scripts/check-expiring-keys-floor.mjs gates it. */
6
6
  export declare const EXPIRING_KEYS_MIN_BINARY = "1.64.0";
@@ -1,6 +1,6 @@
1
1
  /** Single source of truth for the package version, written by scripts/sync-version.mjs; never edit by hand. Read by the db rollback guard, /health, MCP.
2
2
  * A constant, not a runtime package.json read: the published bundle may lack package.json on a path ESM can resolve. */
3
- export const PACKAGE_VERSION = '1.72.0';
3
+ export const PACKAGE_VERSION = '2.0.0';
4
4
  /** The floor a store takes on its first expiring key: the first release with schema
5
5
  * v53, set by hand in that release; scripts/check-expiring-keys-floor.mjs gates it. */
6
6
  export const EXPIRING_KEYS_MIN_BINARY = '1.64.0';
@@ -2,7 +2,7 @@
2
2
  "id": "hippo-memory",
3
3
  "name": "Hippo Memory",
4
4
  "description": "Make your agent's memory work like a brain. Hippo is long-term memory for coding agents.",
5
- "version": "1.72.0",
5
+ "version": "2.0.0",
6
6
 
7
7
  "configSchema": {
8
8
  "type": "object",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hippo-memory",
3
- "version": "1.72.0",
3
+ "version": "2.0.0",
4
4
  "type": "module",
5
5
  "description": "Make your agent's memory work like a brain. Hippo is long-term memory for coding agents.",
6
6
  "main": "index.ts",
@@ -2,7 +2,7 @@
2
2
  "id": "hippo-memory",
3
3
  "name": "Hippo Memory",
4
4
  "description": "Make your agent's memory work like a brain. Hippo is long-term memory for coding agents.",
5
- "version": "1.72.0",
5
+ "version": "2.0.0",
6
6
  "configSchema": {
7
7
  "type": "object",
8
8
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hippo-memory",
3
- "version": "1.72.0",
3
+ "version": "2.0.0",
4
4
  "description": "Make your agent's memory work like a brain. Hippo is long-term memory for coding agents.",
5
5
  "mcpName": "io.github.kitfunso/hippo-memory",
6
6
  "type": "module",
@@ -8,6 +8,8 @@
8
8
  "exports": {
9
9
  ".": "./dist/index.js",
10
10
  "./server": "./dist/server.js",
11
+ "./store-kit": "./dist/entry/store-kit.js",
12
+ "./testing": "./dist/entry/testing.js",
11
13
  "./project-identity": "./dist/entry/project-identity.js",
12
14
  "./json-hooks": "./dist/entry/json-hooks.js",
13
15
  "./session-text": "./dist/entry/session-text.js"
@@ -1,16 +0,0 @@
1
- import type { HippoStore, StoreGroups } from '../store/port.js';
2
- import { type SqliteLocal } from '../store/sqlite/local.js';
3
- import { type SqliteSyncStore } from '../store/sqlite/store.js';
4
- import type { Context, StoreReply } from './types.js';
5
- /** What a port call answers: a Promise from a served store, the value itself from hippo.db. */
6
- export type Reply<T> = T | Promise<T>;
7
- /** The port one body is written against: a served store, or hippo.db answering the same methods at once. */
8
- export type StorePort = HippoStore | SqliteSyncStore;
9
- /** `next` on the reply's value, at once for a value, so hippo.db's path never becomes a Promise. */
10
- export declare function andThen<T, U>(reply: Reply<T>, next: (value: T) => Reply<U>): Reply<U>;
11
- /** Stands in for a group the store lacks, as `port.keyAudit ?? notPorted(port, 'keyAudit')`; it answers 501 as any unported path does. */
12
- export declare function notPorted(port: StorePort, group: keyof StoreGroups): never;
13
- /** Runs `run` on `ctx.store`, where a throw rejects, else on hippo.db under `ctx.hippoRoot`, where it throws. The store path never reads `hippoRoot`.
14
- * `local` runs what needs hippo.db's own handle or meta table; a served store refuses it, or does without where the write allows. */
15
- export declare function onStore<C extends Context, R>(ctx: C, run: (port: StorePort, local: SqliteLocal) => Reply<R>): StoreReply<C, R>;
16
- //# sourceMappingURL=on-store.d.ts.map
@@ -1,38 +0,0 @@
1
- // The one place an api function picks between a served store and hippo.db, so each operation has one body.
2
- // Only functions published as StoreReply use this carrier (see "Sync core, async port" in docs/ARCHITECTURE.md);
3
- // a new operation is async and goes through storeFor(ctx).
4
- import { SqliteBlockedError, StoreNotPortedError } from '../util/sqlite-blocked.js';
5
- import { REFUSED_ON_A_STORE, sqliteLocal } from '../store/sqlite/local.js';
6
- import { sqliteSyncStore } from '../store/sqlite/store.js';
7
- /** `next` on the reply's value, at once for a value, so hippo.db's path never becomes a Promise. */
8
- export function andThen(reply, next) {
9
- return reply instanceof Promise ? reply.then(next) : next(reply);
10
- }
11
- /** Stands in for a group the store lacks, as `port.keyAudit ?? notPorted(port, 'keyAudit')`; it answers 501 as any unported path does. */
12
- export function notPorted(port, group) {
13
- throw new StoreNotPortedError(port.kind, group);
14
- }
15
- /** A served store's stand-in for SqliteLocal. The last recall sits in hippo.db's
16
- * meta table, so only a store that is hippo.db keeps and reads it, on `hippoRoot`. */
17
- function servedLocal(store, hippoRoot) {
18
- if (store.kind === 'sqlite')
19
- return { ...sqliteLocal(hippoRoot), ...REFUSED_ON_A_STORE };
20
- return {
21
- ...REFUSED_ON_A_STORE,
22
- applyOutcomeToLastRecall() {
23
- throw new SqliteBlockedError(store.kind);
24
- },
25
- finishLastRecall: (writes) => store.finishRecall(writes).then(() => null),
26
- };
27
- }
28
- /** Runs `run` on `ctx.store`, where a throw rejects, else on hippo.db under `ctx.hippoRoot`, where it throws. The store path never reads `hippoRoot`.
29
- * `local` runs what needs hippo.db's own handle or meta table; a served store refuses it, or does without where the write allows. */
30
- export function onStore(ctx, run) {
31
- const { store } = ctx;
32
- // No await before `run`: an add-on relies on the first port call starting inside this call.
33
- const reply = store ? (async () => run(store, servedLocal(store, ctx.hippoRoot)))() : run(sqliteSyncStore(ctx.hippoRoot), sqliteLocal(ctx.hippoRoot));
34
- // SAFETY: the store path is an async function's Promise and hippo.db's port calls answer
35
- // values, so the Promise comes back exactly when ctx has a store, as StoreReply says.
36
- return reply;
37
- }
38
- //# sourceMappingURL=on-store.js.map
@@ -1,2 +0,0 @@
1
- export declare function parseObjectId(idRaw: string, noun: string): number;
2
- //# sourceMappingURL=lenient-id.d.ts.map
@@ -1,13 +0,0 @@
1
- // The id parser the predict and decide verbs share.
2
- import { printError } from './output.js';
3
- import { CliExit } from './exit.js';
4
- // Lenient on purpose (parseInt reads "1abc" as 1) until the next major version; the other object verbs use the strict parser.
5
- export function parseObjectId(idRaw, noun) {
6
- const id = parseInt(String(idRaw), 10);
7
- if (!Number.isFinite(id) || id <= 0) {
8
- printError(`Invalid ${noun} id: "${idRaw}"`);
9
- throw new CliExit(1);
10
- }
11
- return id;
12
- }
13
- //# sourceMappingURL=lenient-id.js.map