hippo-memory 1.61.0 → 1.62.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 (57) hide show
  1. package/README.md +28 -53
  2. package/dist/agent-memories/apply.d.ts +1 -1
  3. package/dist/agent-memories/claude-code.d.ts +3 -1
  4. package/dist/agent-memories/claude-code.js +53 -9
  5. package/dist/agent-memories/sync.d.ts +3 -3
  6. package/dist/agent-memories/sync.js +14 -6
  7. package/dist/agent-memories/types.d.ts +0 -2
  8. package/dist/api/assemble.js +55 -54
  9. package/dist/api/context-select.d.ts +49 -0
  10. package/dist/api/context-select.js +344 -0
  11. package/dist/api/context.d.ts +2 -2
  12. package/dist/api/context.js +195 -522
  13. package/dist/api/drill-down.js +36 -33
  14. package/dist/api/promote.js +55 -66
  15. package/dist/api/recall.js +303 -438
  16. package/dist/api/sleep.js +203 -218
  17. package/dist/capture/compact.d.ts +1 -1
  18. package/dist/capture/compact.js +2 -2
  19. package/dist/cli/briefs.js +324 -306
  20. package/dist/cli/context.js +44 -34
  21. package/dist/cli/continuity.js +283 -271
  22. package/dist/cli/curate.js +35 -34
  23. package/dist/cli/decisions.js +333 -333
  24. package/dist/cli/explain.js +66 -60
  25. package/dist/cli/maintenance.js +62 -51
  26. package/dist/cli/playbooks.js +387 -370
  27. package/dist/cli/projects.js +8 -5
  28. package/dist/cli/recall.js +28 -43
  29. package/dist/cli/remember.js +113 -70
  30. package/dist/cli/session-hooks.js +100 -90
  31. package/dist/cli/setup.js +267 -246
  32. package/dist/cli/status.js +73 -64
  33. package/dist/cli/transfer.js +85 -99
  34. package/dist/compaction-record.d.ts +0 -2
  35. package/dist/compaction-record.js +1 -1
  36. package/dist/customer-notes.js +77 -68
  37. package/dist/dag.js +222 -186
  38. package/dist/decisions.js +93 -76
  39. package/dist/doctor.js +11 -7
  40. package/dist/goals.js +99 -86
  41. package/dist/incidents.js +45 -38
  42. package/dist/policies.js +85 -68
  43. package/dist/processes.js +87 -71
  44. package/dist/project-briefs.js +135 -108
  45. package/dist/project-merge.d.ts +12 -4
  46. package/dist/project-merge.js +130 -45
  47. package/dist/shared.d.ts +9 -0
  48. package/dist/shared.js +10 -8
  49. package/dist/skills.js +81 -65
  50. package/dist/store/search-rows.d.ts +2 -2
  51. package/dist/store/search-rows.js +15 -9
  52. package/dist/version.d.ts +1 -1
  53. package/dist/version.js +1 -1
  54. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  55. package/extensions/openclaw-plugin/package.json +1 -1
  56. package/openclaw.plugin.json +1 -1
  57. package/package.json +1 -1
@@ -8,175 +8,182 @@ import * as incidentsModule from '../incidents.js';
8
8
  import { resolveTenantId } from '../tenant.js';
9
9
  import { printError } from './output.js';
10
10
  import { requireInit } from './shared.js';
11
+ function parseListLimit(flags) {
12
+ const limitRaw = flags['limit'];
13
+ const limit = limitRaw !== undefined ? parseInt(String(limitRaw), 10) : 100;
14
+ if (!Number.isFinite(limit) || limit <= 0) {
15
+ printError(`Invalid --limit: "${limitRaw}". Must be a positive integer.`);
16
+ process.exit(1);
17
+ }
18
+ return limit;
19
+ }
20
+ /** parseInt-based id parse for predictions and decisions; incidents use the strict parse below. */
21
+ function parseObjectId(idRaw, noun) {
22
+ const id = parseInt(String(idRaw), 10);
23
+ if (!Number.isFinite(id) || id <= 0) {
24
+ printError(`Invalid ${noun} id: "${idRaw}"`);
25
+ process.exit(1);
26
+ }
27
+ return id;
28
+ }
11
29
  // ---------------------------------------------------------------------------
12
30
  // E2 prediction first-class object (v0.31)
13
31
  // docs/plans/2026-05-26-e2-prediction-object.md
14
32
  // ---------------------------------------------------------------------------
15
- export function cmdPredict(hippoRoot, args, flags) {
16
- requireInit(hippoRoot);
17
- const tenantId = resolveTenantId({});
18
- const subcommand = args[0] ?? '';
19
- if (subcommand === 'close') {
20
- const idRaw = args[1];
21
- if (!idRaw) {
22
- printError('Usage: hippo predict close <id> --state <closed|closed-unknown> [--actual <v>] [--note "..."]');
23
- process.exit(1);
24
- }
25
- const id = parseInt(String(idRaw), 10);
26
- if (!Number.isFinite(id) || id <= 0) {
27
- printError(`Invalid prediction id: "${idRaw}"`);
28
- process.exit(1);
29
- }
30
- const stateRaw = typeof flags['state'] === 'string' ? flags['state'].trim() : '';
31
- if (!predictionsModule.VALID_CLOSURE_STATES.has(stateRaw) || stateRaw === 'open') {
32
- printError(`Invalid --state: "${stateRaw}". Must be one of: closed | closed-unknown.`);
33
- process.exit(1);
34
- }
35
- const actualRaw = flags['actual'];
36
- const actualValue = actualRaw !== undefined ? Number(actualRaw) : undefined;
37
- if (actualRaw !== undefined && !Number.isFinite(actualValue)) {
38
- printError(`Invalid --actual: "${actualRaw}". Must be a number.`);
39
- process.exit(1);
40
- }
41
- const noteRaw = flags['note'];
42
- const closureNote = typeof noteRaw === 'string' ? noteRaw : undefined;
43
- const closed = predictionsModule.closePrediction(hippoRoot, tenantId, id, {
44
- closureState: stateRaw,
45
- actualValue,
46
- closureNote,
33
+ function predictClose(hippoRoot, tenantId, args, flags) {
34
+ const idRaw = args[1];
35
+ if (!idRaw) {
36
+ printError('Usage: hippo predict close <id> --state <closed|closed-unknown> [--actual <v>] [--note "..."]');
37
+ process.exit(1);
38
+ }
39
+ const id = parseObjectId(idRaw, 'prediction');
40
+ const stateRaw = typeof flags['state'] === 'string' ? flags['state'].trim() : '';
41
+ if (!predictionsModule.VALID_CLOSURE_STATES.has(stateRaw) || stateRaw === 'open') {
42
+ printError(`Invalid --state: "${stateRaw}". Must be one of: closed | closed-unknown.`);
43
+ process.exit(1);
44
+ }
45
+ const actualRaw = flags['actual'];
46
+ const actualValue = actualRaw !== undefined ? Number(actualRaw) : undefined;
47
+ if (actualRaw !== undefined && !Number.isFinite(actualValue)) {
48
+ printError(`Invalid --actual: "${actualRaw}". Must be a number.`);
49
+ process.exit(1);
50
+ }
51
+ const noteRaw = flags['note'];
52
+ const closureNote = typeof noteRaw === 'string' ? noteRaw : undefined;
53
+ const closed = predictionsModule.closePrediction(hippoRoot, tenantId, id, {
54
+ closureState: stateRaw,
55
+ actualValue,
56
+ closureNote,
57
+ });
58
+ console.log(`Prediction ${closed.id} closed: state=${closed.closureState}${closed.actualValue !== null ? ` actual=${closed.actualValue}` : ''}`);
59
+ }
60
+ function loadPredictionList(hippoRoot, tenantId, status, classTag, limit) {
61
+ if (status === 'open') {
62
+ return predictionsModule.loadOpenPredictions(hippoRoot, tenantId, {
63
+ classTag: classTag || undefined,
64
+ limit,
47
65
  });
48
- console.log(`Prediction ${closed.id} closed: state=${closed.closureState}${closed.actualValue !== null ? ` actual=${closed.actualValue}` : ''}`);
49
- return;
50
66
  }
51
- if (subcommand === 'list') {
52
- const classTagRaw = flags['class'];
53
- const classTag = typeof classTagRaw === 'string' ? classTagRaw.trim() : '';
54
- const statusRaw = flags['status'];
55
- const status = typeof statusRaw === 'string' ? statusRaw.trim() : 'all';
56
- const limitRaw = flags['limit'];
57
- const limit = limitRaw !== undefined ? parseInt(String(limitRaw), 10) : 100;
58
- if (!Number.isFinite(limit) || limit <= 0) {
59
- printError(`Invalid --limit: "${limitRaw}". Must be a positive integer.`);
60
- process.exit(1);
61
- }
62
- let results;
63
- if (status === 'open') {
64
- results = predictionsModule.loadOpenPredictions(hippoRoot, tenantId, {
65
- classTag: classTag || undefined,
66
- limit,
67
- });
68
- }
69
- else if (status === 'all') {
70
- // No closure-state filter; pull both via loadPredictionsByClass if class given
71
- if (classTag) {
72
- results = predictionsModule.loadPredictionsByClass(hippoRoot, tenantId, classTag, { limit });
73
- }
74
- else {
75
- // No class filter + status=all = pull open + closed across all classes
76
- // (kept simple: report open via loadOpenPredictions; closed via two
77
- // class scans isn't symmetrical. v1 callers typically pass --class.)
78
- results = predictionsModule.loadOpenPredictions(hippoRoot, tenantId, { limit });
79
- }
80
- }
81
- else {
82
- if (!predictionsModule.VALID_CLOSURE_STATES.has(status)) {
83
- printError(`Invalid --status: "${status}". Must be one of: open | closed | closed-unknown | all.`);
84
- process.exit(1);
85
- }
86
- if (classTag) {
87
- results = predictionsModule.loadPredictionsByClass(hippoRoot, tenantId, classTag, {
88
- closureState: status,
89
- limit,
90
- });
91
- }
92
- else {
93
- // status filter without class — scan all classes is more complex; v1 requires --class for non-default status
94
- printError('--status filter (non-open) requires --class to be set.');
95
- process.exit(1);
96
- }
97
- }
98
- if (results.length === 0) {
99
- console.log(classTag ? `No predictions in class "${classTag}".` : 'No predictions.');
100
- return;
101
- }
102
- console.log(`Found ${results.length} predictions:\n`);
103
- for (const p of results) {
104
- const estPart = p.estimateValue !== null ? ` estimate=${p.estimateValue}${p.estimateUnit ? ` ${p.estimateUnit}` : ''}` : '';
105
- const actPart = p.actualValue !== null ? ` actual=${p.actualValue}` : '';
106
- const tgtPart = p.targetDate ? ` target=${p.targetDate}` : '';
107
- console.log(`#${p.id} [${p.closureState}] class=${p.classTag}${estPart}${actPart}${tgtPart}`);
108
- console.log(` ${p.claimText}`);
109
- if (p.closureNote)
110
- console.log(` note: ${p.closureNote}`);
111
- }
112
- return;
67
+ if (status === 'all') {
68
+ // No closure-state filter; pull both via loadPredictionsByClass if class given
69
+ if (classTag) {
70
+ return predictionsModule.loadPredictionsByClass(hippoRoot, tenantId, classTag, { limit });
71
+ }
72
+ // No class filter + status=all = pull open + closed across all classes
73
+ // (kept simple: report open via loadOpenPredictions; closed via two
74
+ // class scans isn't symmetrical. v1 callers typically pass --class.)
75
+ return predictionsModule.loadOpenPredictions(hippoRoot, tenantId, { limit });
113
76
  }
114
- if (subcommand === 'show') {
115
- const idRaw = args[1];
116
- if (!idRaw) {
117
- printError('Usage: hippo predict show <id>');
118
- process.exit(1);
119
- }
120
- const id = parseInt(String(idRaw), 10);
121
- if (!Number.isFinite(id) || id <= 0) {
122
- printError(`Invalid prediction id: "${idRaw}"`);
123
- process.exit(1);
124
- }
125
- const pred = predictionsModule.loadPredictionById(hippoRoot, tenantId, id);
126
- if (!pred) {
127
- printError(`Prediction ${id} not found.`);
128
- process.exit(1);
129
- }
130
- console.log(`Prediction #${pred.id}`);
131
- console.log(` class: ${pred.classTag}`);
132
- console.log(` claim: ${pred.claimText}`);
133
- console.log(` state: ${pred.closureState}`);
134
- if (pred.estimateValue !== null)
135
- console.log(` estimate: ${pred.estimateValue}${pred.estimateUnit ? ' ' + pred.estimateUnit : ''}`);
136
- if (pred.targetDate)
137
- console.log(` target: ${pred.targetDate}`);
138
- if (pred.actualValue !== null)
139
- console.log(` actual: ${pred.actualValue}`);
140
- if (pred.closedAt)
141
- console.log(` closed: ${pred.closedAt}`);
142
- if (pred.closureNote)
143
- console.log(` note: ${pred.closureNote}`);
144
- if (pred.memoryId)
145
- console.log(` memory: ${pred.memoryId}`);
146
- console.log(` created: ${pred.createdAt}`);
77
+ if (!predictionsModule.VALID_CLOSURE_STATES.has(status)) {
78
+ printError(`Invalid --status: "${status}". Must be one of: open | closed | closed-unknown | all.`);
79
+ process.exit(1);
80
+ }
81
+ if (!classTag) {
82
+ // status filter without class — scan all classes is more complex; v1 requires --class for non-default status
83
+ printError('--status filter (non-open) requires --class to be set.');
84
+ process.exit(1);
85
+ }
86
+ return predictionsModule.loadPredictionsByClass(hippoRoot, tenantId, classTag, {
87
+ closureState: status,
88
+ limit,
89
+ });
90
+ }
91
+ function predictList(hippoRoot, tenantId, flags) {
92
+ const classTagRaw = flags['class'];
93
+ const classTag = typeof classTagRaw === 'string' ? classTagRaw.trim() : '';
94
+ const statusRaw = flags['status'];
95
+ const status = typeof statusRaw === 'string' ? statusRaw.trim() : 'all';
96
+ const limit = parseListLimit(flags);
97
+ const results = loadPredictionList(hippoRoot, tenantId, status, classTag, limit);
98
+ if (results.length === 0) {
99
+ console.log(classTag ? `No predictions in class "${classTag}".` : 'No predictions.');
147
100
  return;
148
101
  }
149
- if (subcommand === 'baserate') {
150
- // J3 reference-class / planning-fallacy detector
151
- const classTagRaw = flags['class'];
152
- if (typeof classTagRaw !== 'string' || !classTagRaw.trim()) {
153
- printError('Usage: hippo predict baserate --class <c>');
154
- process.exit(1);
155
- }
156
- const baserate = predictionsModule.computePredictionBaserate(hippoRoot, tenantId, classTagRaw.trim());
157
- if (baserate.nClosed === 0) {
158
- console.log(`No closed predictions in class "${baserate.classTag}" yet.`);
159
- console.log(` Create one with: hippo predict "<claim>" --class ${baserate.classTag} --estimate N`);
160
- console.log(` Close it later: hippo predict close <id> --state closed --actual N`);
161
- return;
162
- }
163
- console.log(baserate.summary);
164
- console.log(` n_closed: ${baserate.nClosed}`);
165
- console.log(` n_ratio_eligible: ${baserate.nRatioEligible}`);
166
- if (baserate.meanEstimate !== null)
167
- console.log(` mean_estimate: ${baserate.meanEstimate.toFixed(3)}`);
168
- if (baserate.meanActual !== null)
169
- console.log(` mean_actual: ${baserate.meanActual.toFixed(3)}`);
170
- if (baserate.meanRatio !== null)
171
- console.log(` mean_ratio: ${baserate.meanRatio.toFixed(3)}x`);
172
- if (baserate.p50Ratio !== null)
173
- console.log(` p50_ratio: ${baserate.p50Ratio.toFixed(3)}x`);
174
- if (baserate.mae !== null)
175
- console.log(` mae: ${baserate.mae.toFixed(3)}`);
102
+ console.log(`Found ${results.length} predictions:\n`);
103
+ for (const p of results) {
104
+ const estPart = p.estimateValue !== null ? ` estimate=${p.estimateValue}${p.estimateUnit ? ` ${p.estimateUnit}` : ''}` : '';
105
+ const actPart = p.actualValue !== null ? ` actual=${p.actualValue}` : '';
106
+ const tgtPart = p.targetDate ? ` target=${p.targetDate}` : '';
107
+ console.log(`#${p.id} [${p.closureState}] class=${p.classTag}${estPart}${actPart}${tgtPart}`);
108
+ console.log(` ${p.claimText}`);
109
+ if (p.closureNote)
110
+ console.log(` note: ${p.closureNote}`);
111
+ }
112
+ }
113
+ function predictShow(hippoRoot, tenantId, args) {
114
+ const idRaw = args[1];
115
+ if (!idRaw) {
116
+ printError('Usage: hippo predict show <id>');
117
+ process.exit(1);
118
+ }
119
+ const id = parseObjectId(idRaw, 'prediction');
120
+ const pred = predictionsModule.loadPredictionById(hippoRoot, tenantId, id);
121
+ if (!pred) {
122
+ printError(`Prediction ${id} not found.`);
123
+ process.exit(1);
124
+ }
125
+ console.log(`Prediction #${pred.id}`);
126
+ console.log(` class: ${pred.classTag}`);
127
+ console.log(` claim: ${pred.claimText}`);
128
+ console.log(` state: ${pred.closureState}`);
129
+ if (pred.estimateValue !== null)
130
+ console.log(` estimate: ${pred.estimateValue}${pred.estimateUnit ? ' ' + pred.estimateUnit : ''}`);
131
+ if (pred.targetDate)
132
+ console.log(` target: ${pred.targetDate}`);
133
+ if (pred.actualValue !== null)
134
+ console.log(` actual: ${pred.actualValue}`);
135
+ if (pred.closedAt)
136
+ console.log(` closed: ${pred.closedAt}`);
137
+ if (pred.closureNote)
138
+ console.log(` note: ${pred.closureNote}`);
139
+ if (pred.memoryId)
140
+ console.log(` memory: ${pred.memoryId}`);
141
+ console.log(` created: ${pred.createdAt}`);
142
+ }
143
+ function predictBaserate(hippoRoot, tenantId, flags) {
144
+ // J3 reference-class / planning-fallacy detector
145
+ const classTagRaw = flags['class'];
146
+ if (typeof classTagRaw !== 'string' || !classTagRaw.trim()) {
147
+ printError('Usage: hippo predict baserate --class <c>');
148
+ process.exit(1);
149
+ }
150
+ const baserate = predictionsModule.computePredictionBaserate(hippoRoot, tenantId, classTagRaw.trim());
151
+ if (baserate.nClosed === 0) {
152
+ console.log(`No closed predictions in class "${baserate.classTag}" yet.`);
153
+ console.log(` Create one with: hippo predict "<claim>" --class ${baserate.classTag} --estimate N`);
154
+ console.log(` Close it later: hippo predict close <id> --state closed --actual N`);
176
155
  return;
177
156
  }
157
+ console.log(baserate.summary);
158
+ console.log(` n_closed: ${baserate.nClosed}`);
159
+ console.log(` n_ratio_eligible: ${baserate.nRatioEligible}`);
160
+ if (baserate.meanEstimate !== null)
161
+ console.log(` mean_estimate: ${baserate.meanEstimate.toFixed(3)}`);
162
+ if (baserate.meanActual !== null)
163
+ console.log(` mean_actual: ${baserate.meanActual.toFixed(3)}`);
164
+ if (baserate.meanRatio !== null)
165
+ console.log(` mean_ratio: ${baserate.meanRatio.toFixed(3)}x`);
166
+ if (baserate.p50Ratio !== null)
167
+ console.log(` p50_ratio: ${baserate.p50Ratio.toFixed(3)}x`);
168
+ if (baserate.mae !== null)
169
+ console.log(` mae: ${baserate.mae.toFixed(3)}`);
170
+ }
171
+ export function cmdPredict(hippoRoot, args, flags) {
172
+ requireInit(hippoRoot);
173
+ const tenantId = resolveTenantId({});
174
+ const subcommand = args[0] ?? '';
175
+ if (subcommand === 'close')
176
+ return predictClose(hippoRoot, tenantId, args, flags);
177
+ if (subcommand === 'list')
178
+ return predictList(hippoRoot, tenantId, flags);
179
+ if (subcommand === 'show')
180
+ return predictShow(hippoRoot, tenantId, args);
181
+ if (subcommand === 'baserate')
182
+ return predictBaserate(hippoRoot, tenantId, flags);
178
183
  // Default subcommand: create. args[0] is the claim text.
179
- const claimText = subcommand;
184
+ predictCreate(hippoRoot, tenantId, subcommand, flags);
185
+ }
186
+ function predictCreate(hippoRoot, tenantId, claimText, flags) {
180
187
  if (!claimText) {
181
188
  printError('Usage: hippo predict "<claim>" --class <c> [--estimate <v>] [--unit <u>] [--target <YYYY-MM-DD>]');
182
189
  printError(' hippo predict close <id> --state <closed|closed-unknown> [--actual <v>] [--note "..."]');
@@ -211,96 +218,88 @@ export function cmdPredict(hippoRoot, args, flags) {
211
218
  if (created.memoryId)
212
219
  console.log(` memory: ${created.memoryId}`);
213
220
  }
214
- export function cmdDecide(hippoRoot, args, flags) {
215
- requireInit(hippoRoot);
216
- const tenantId = resolveTenantId({});
217
- const subcommand = args[0] ?? '';
218
- if (subcommand === 'list') {
219
- const statusRaw = flags['status'];
220
- const status = typeof statusRaw === 'string' ? statusRaw.trim() : 'all';
221
- const limitRaw = flags['limit'];
222
- const limit = limitRaw !== undefined ? parseInt(String(limitRaw), 10) : 100;
223
- if (!Number.isFinite(limit) || limit <= 0) {
224
- printError(`Invalid --limit: "${limitRaw}". Must be a positive integer.`);
225
- process.exit(1);
226
- }
227
- let results;
228
- if (status === 'all') {
229
- results = decisionsModule.loadDecisions(hippoRoot, tenantId, { limit });
230
- }
231
- else {
232
- if (!decisionsModule.VALID_DECISION_STATES.has(status)) {
233
- printError(`Invalid --status: "${status}". Must be one of: active | superseded | closed | all.`);
234
- process.exit(1);
235
- }
236
- results = decisionsModule.loadDecisions(hippoRoot, tenantId, {
237
- status: status,
238
- limit,
239
- });
240
- }
241
- if (results.length === 0) {
242
- console.log('No decisions.');
243
- return;
244
- }
245
- console.log(`Found ${results.length} decisions:\n`);
246
- for (const d of results) {
247
- const supPart = d.supersededBy !== null ? ` superseded_by=#${d.supersededBy}` : '';
248
- console.log(`#${d.id} [${d.status}]${supPart} memory=${d.memoryId ?? '-'}`);
249
- console.log(` ${d.decisionText}`);
250
- if (d.context)
251
- console.log(` context: ${d.context}`);
252
- }
253
- return;
221
+ function decideList(hippoRoot, tenantId, flags) {
222
+ const statusRaw = flags['status'];
223
+ const status = typeof statusRaw === 'string' ? statusRaw.trim() : 'all';
224
+ const limit = parseListLimit(flags);
225
+ let results;
226
+ if (status === 'all') {
227
+ results = decisionsModule.loadDecisions(hippoRoot, tenantId, { limit });
254
228
  }
255
- if (subcommand === 'get') {
256
- const idRaw = args[1];
257
- if (!idRaw) {
258
- printError('Usage: hippo decide get <id>');
259
- process.exit(1);
260
- }
261
- const id = parseInt(String(idRaw), 10);
262
- if (!Number.isFinite(id) || id <= 0) {
263
- printError(`Invalid decision id: "${idRaw}"`);
264
- process.exit(1);
265
- }
266
- const decision = decisionsModule.loadDecisionById(hippoRoot, tenantId, id);
267
- if (!decision) {
268
- printError(`Decision ${id} not found.`);
229
+ else {
230
+ if (!decisionsModule.VALID_DECISION_STATES.has(status)) {
231
+ printError(`Invalid --status: "${status}". Must be one of: active | superseded | closed | all.`);
269
232
  process.exit(1);
270
233
  }
271
- console.log(`Decision #${decision.id}`);
272
- console.log(` status: ${decision.status}`);
273
- console.log(` text: ${decision.decisionText}`);
274
- if (decision.context)
275
- console.log(` context: ${decision.context}`);
276
- if (decision.supersededBy !== null)
277
- console.log(` superseded_by: #${decision.supersededBy}`);
278
- if (decision.supersededAt)
279
- console.log(` superseded_at: ${decision.supersededAt}`);
280
- if (decision.closedAt)
281
- console.log(` closed_at: ${decision.closedAt}`);
282
- if (decision.memoryId)
283
- console.log(` memory: ${decision.memoryId}`);
284
- console.log(` created: ${decision.createdAt}`);
285
- return;
234
+ results = decisionsModule.loadDecisions(hippoRoot, tenantId, {
235
+ status: status,
236
+ limit,
237
+ });
286
238
  }
287
- if (subcommand === 'close') {
288
- const idRaw = args[1];
289
- if (!idRaw) {
290
- printError('Usage: hippo decide close <id>');
291
- process.exit(1);
292
- }
293
- const id = parseInt(String(idRaw), 10);
294
- if (!Number.isFinite(id) || id <= 0) {
295
- printError(`Invalid decision id: "${idRaw}"`);
296
- process.exit(1);
297
- }
298
- const closed = decisionsModule.closeDecision(hippoRoot, tenantId, id);
299
- console.log(`Decision #${closed.id} closed.`);
239
+ if (results.length === 0) {
240
+ console.log('No decisions.');
300
241
  return;
301
242
  }
243
+ console.log(`Found ${results.length} decisions:\n`);
244
+ for (const d of results) {
245
+ const supPart = d.supersededBy !== null ? ` superseded_by=#${d.supersededBy}` : '';
246
+ console.log(`#${d.id} [${d.status}]${supPart} memory=${d.memoryId ?? '-'}`);
247
+ console.log(` ${d.decisionText}`);
248
+ if (d.context)
249
+ console.log(` context: ${d.context}`);
250
+ }
251
+ }
252
+ function decideGet(hippoRoot, tenantId, args) {
253
+ const idRaw = args[1];
254
+ if (!idRaw) {
255
+ printError('Usage: hippo decide get <id>');
256
+ process.exit(1);
257
+ }
258
+ const id = parseObjectId(idRaw, 'decision');
259
+ const decision = decisionsModule.loadDecisionById(hippoRoot, tenantId, id);
260
+ if (!decision) {
261
+ printError(`Decision ${id} not found.`);
262
+ process.exit(1);
263
+ }
264
+ console.log(`Decision #${decision.id}`);
265
+ console.log(` status: ${decision.status}`);
266
+ console.log(` text: ${decision.decisionText}`);
267
+ if (decision.context)
268
+ console.log(` context: ${decision.context}`);
269
+ if (decision.supersededBy !== null)
270
+ console.log(` superseded_by: #${decision.supersededBy}`);
271
+ if (decision.supersededAt)
272
+ console.log(` superseded_at: ${decision.supersededAt}`);
273
+ if (decision.closedAt)
274
+ console.log(` closed_at: ${decision.closedAt}`);
275
+ if (decision.memoryId)
276
+ console.log(` memory: ${decision.memoryId}`);
277
+ console.log(` created: ${decision.createdAt}`);
278
+ }
279
+ function decideClose(hippoRoot, tenantId, args) {
280
+ const idRaw = args[1];
281
+ if (!idRaw) {
282
+ printError('Usage: hippo decide close <id>');
283
+ process.exit(1);
284
+ }
285
+ const id = parseObjectId(idRaw, 'decision');
286
+ const closed = decisionsModule.closeDecision(hippoRoot, tenantId, id);
287
+ console.log(`Decision #${closed.id} closed.`);
288
+ }
289
+ export function cmdDecide(hippoRoot, args, flags) {
290
+ requireInit(hippoRoot);
291
+ const tenantId = resolveTenantId({});
292
+ const subcommand = args[0] ?? '';
293
+ if (subcommand === 'list')
294
+ return decideList(hippoRoot, tenantId, flags);
295
+ if (subcommand === 'get')
296
+ return decideGet(hippoRoot, tenantId, args);
297
+ if (subcommand === 'close')
298
+ return decideClose(hippoRoot, tenantId, args);
302
299
  // Default subcommand: create. args[0] is the decision text.
303
- const decisionText = subcommand;
300
+ decideCreate(hippoRoot, tenantId, subcommand, flags);
301
+ }
302
+ function decideCreate(hippoRoot, tenantId, decisionText, flags) {
304
303
  if (!decisionText) {
305
304
  printError('Usage: hippo decide "<decision>" [--context "<why>"] [--supersedes <memory-id>]');
306
305
  printError(' hippo decide list [--status active|superseded|closed|all] [--limit N]');
@@ -384,109 +383,110 @@ function parsePositiveIncidentId(idRaw) {
384
383
  }
385
384
  return id;
386
385
  }
387
- export function cmdIncident(hippoRoot, args, flags) {
388
- requireInit(hippoRoot);
389
- const tenantId = resolveTenantId({});
390
- const subcommand = args[0] ?? '';
391
- if (subcommand === 'list') {
392
- const statusRaw = flags['status'];
393
- const status = typeof statusRaw === 'string' ? statusRaw.trim() : 'all';
394
- const limitRaw = flags['limit'];
395
- const limit = limitRaw !== undefined ? parseInt(String(limitRaw), 10) : 100;
396
- if (!Number.isFinite(limit) || limit <= 0) {
397
- printError(`Invalid --limit: "${limitRaw}". Must be a positive integer.`);
398
- process.exit(1);
399
- }
400
- let results;
401
- if (status === 'all') {
402
- results = incidentsModule.loadIncidents(hippoRoot, tenantId, { limit });
403
- }
404
- else {
405
- if (!incidentsModule.VALID_INCIDENT_STATES.has(status)) {
406
- printError(`Invalid --status: "${status}". Must be one of: open | resolved | closed | all.`);
407
- process.exit(1);
408
- }
409
- results = incidentsModule.loadIncidents(hippoRoot, tenantId, {
410
- status: status,
411
- limit,
412
- });
413
- }
414
- if (results.length === 0) {
415
- console.log('No incidents.');
416
- return;
417
- }
418
- console.log(`Found ${results.length} incidents:\n`);
419
- for (const inc of results) {
420
- const linkPart = inc.linkedMemoryIds.length > 0 ? ` links=${inc.linkedMemoryIds.length}` : '';
421
- console.log(`#${inc.id} [${inc.status}]${linkPart} memory=${inc.memoryId ?? '-'}`);
422
- console.log(` ${inc.incidentText}`);
423
- if (inc.context)
424
- console.log(` context: ${inc.context}`);
425
- }
426
- return;
386
+ function incidentList(hippoRoot, tenantId, flags) {
387
+ const statusRaw = flags['status'];
388
+ const status = typeof statusRaw === 'string' ? statusRaw.trim() : 'all';
389
+ const limit = parseListLimit(flags);
390
+ let results;
391
+ if (status === 'all') {
392
+ results = incidentsModule.loadIncidents(hippoRoot, tenantId, { limit });
427
393
  }
428
- if (subcommand === 'get') {
429
- const idRaw = args[1];
430
- if (!idRaw) {
431
- printError('Usage: hippo incident get <id>');
394
+ else {
395
+ if (!incidentsModule.VALID_INCIDENT_STATES.has(status)) {
396
+ printError(`Invalid --status: "${status}". Must be one of: open | resolved | closed | all.`);
432
397
  process.exit(1);
433
398
  }
434
- const id = parsePositiveIncidentId(idRaw);
435
- const incident = incidentsModule.loadIncidentById(hippoRoot, tenantId, id);
436
- if (!incident) {
437
- printError(`Incident ${id} not found.`);
438
- process.exit(1);
439
- }
440
- console.log(`Incident #${incident.id}`);
441
- console.log(` status: ${incident.status}`);
442
- console.log(` text: ${incident.incidentText}`);
443
- if (incident.context)
444
- console.log(` context: ${incident.context}`);
445
- if (incident.resolutionText)
446
- console.log(` resolution: ${incident.resolutionText}`);
447
- if (incident.resolvedAt)
448
- console.log(` resolved_at: ${incident.resolvedAt}`);
449
- if (incident.closedAt)
450
- console.log(` closed_at: ${incident.closedAt}`);
451
- if (incident.linkedMemoryIds.length > 0) {
452
- console.log(` linked memories: ${incident.linkedMemoryIds.join(', ')}`);
453
- }
454
- if (incident.memoryId)
455
- console.log(` memory: ${incident.memoryId}`);
456
- console.log(` created: ${incident.createdAt}`);
457
- return;
399
+ results = incidentsModule.loadIncidents(hippoRoot, tenantId, {
400
+ status: status,
401
+ limit,
402
+ });
458
403
  }
459
- if (subcommand === 'resolve') {
460
- const idRaw = args[1];
461
- if (!idRaw) {
462
- printError('Usage: hippo incident resolve <id> --resolution "<text>"');
463
- process.exit(1);
464
- }
465
- const id = parsePositiveIncidentId(idRaw);
466
- const resolutionRaw = flags['resolution'];
467
- if (typeof resolutionRaw !== 'string' || !resolutionRaw.trim()) {
468
- printError('--resolution requires a non-empty value, e.g. hippo incident resolve <id> --resolution "root cause fixed".');
469
- process.exit(1);
470
- }
471
- const resolved = incidentsModule.resolveIncident(hippoRoot, tenantId, id, resolutionRaw);
472
- console.log(`Incident #${resolved.id} resolved.`);
404
+ if (results.length === 0) {
405
+ console.log('No incidents.');
473
406
  return;
474
407
  }
475
- if (subcommand === 'close') {
476
- const idRaw = args[1];
477
- if (!idRaw) {
478
- printError('Usage: hippo incident close <id>');
479
- process.exit(1);
480
- }
481
- const id = parsePositiveIncidentId(idRaw);
482
- const closed = incidentsModule.closeIncident(hippoRoot, tenantId, id);
483
- console.log(`Incident #${closed.id} closed.`);
484
- return;
408
+ console.log(`Found ${results.length} incidents:\n`);
409
+ for (const inc of results) {
410
+ const linkPart = inc.linkedMemoryIds.length > 0 ? ` links=${inc.linkedMemoryIds.length}` : '';
411
+ console.log(`#${inc.id} [${inc.status}]${linkPart} memory=${inc.memoryId ?? '-'}`);
412
+ console.log(` ${inc.incidentText}`);
413
+ if (inc.context)
414
+ console.log(` context: ${inc.context}`);
485
415
  }
416
+ }
417
+ function incidentGet(hippoRoot, tenantId, args) {
418
+ const idRaw = args[1];
419
+ if (!idRaw) {
420
+ printError('Usage: hippo incident get <id>');
421
+ process.exit(1);
422
+ }
423
+ const id = parsePositiveIncidentId(idRaw);
424
+ const incident = incidentsModule.loadIncidentById(hippoRoot, tenantId, id);
425
+ if (!incident) {
426
+ printError(`Incident ${id} not found.`);
427
+ process.exit(1);
428
+ }
429
+ console.log(`Incident #${incident.id}`);
430
+ console.log(` status: ${incident.status}`);
431
+ console.log(` text: ${incident.incidentText}`);
432
+ if (incident.context)
433
+ console.log(` context: ${incident.context}`);
434
+ if (incident.resolutionText)
435
+ console.log(` resolution: ${incident.resolutionText}`);
436
+ if (incident.resolvedAt)
437
+ console.log(` resolved_at: ${incident.resolvedAt}`);
438
+ if (incident.closedAt)
439
+ console.log(` closed_at: ${incident.closedAt}`);
440
+ if (incident.linkedMemoryIds.length > 0) {
441
+ console.log(` linked memories: ${incident.linkedMemoryIds.join(', ')}`);
442
+ }
443
+ if (incident.memoryId)
444
+ console.log(` memory: ${incident.memoryId}`);
445
+ console.log(` created: ${incident.createdAt}`);
446
+ }
447
+ function incidentResolve(hippoRoot, tenantId, args, flags) {
448
+ const idRaw = args[1];
449
+ if (!idRaw) {
450
+ printError('Usage: hippo incident resolve <id> --resolution "<text>"');
451
+ process.exit(1);
452
+ }
453
+ const id = parsePositiveIncidentId(idRaw);
454
+ const resolutionRaw = flags['resolution'];
455
+ if (typeof resolutionRaw !== 'string' || !resolutionRaw.trim()) {
456
+ printError('--resolution requires a non-empty value, e.g. hippo incident resolve <id> --resolution "root cause fixed".');
457
+ process.exit(1);
458
+ }
459
+ const resolved = incidentsModule.resolveIncident(hippoRoot, tenantId, id, resolutionRaw);
460
+ console.log(`Incident #${resolved.id} resolved.`);
461
+ }
462
+ function incidentClose(hippoRoot, tenantId, args) {
463
+ const idRaw = args[1];
464
+ if (!idRaw) {
465
+ printError('Usage: hippo incident close <id>');
466
+ process.exit(1);
467
+ }
468
+ const id = parsePositiveIncidentId(idRaw);
469
+ const closed = incidentsModule.closeIncident(hippoRoot, tenantId, id);
470
+ console.log(`Incident #${closed.id} closed.`);
471
+ }
472
+ export function cmdIncident(hippoRoot, args, flags) {
473
+ requireInit(hippoRoot);
474
+ const tenantId = resolveTenantId({});
475
+ const subcommand = args[0] ?? '';
476
+ if (subcommand === 'list')
477
+ return incidentList(hippoRoot, tenantId, flags);
478
+ if (subcommand === 'get')
479
+ return incidentGet(hippoRoot, tenantId, args);
480
+ if (subcommand === 'resolve')
481
+ return incidentResolve(hippoRoot, tenantId, args, flags);
482
+ if (subcommand === 'close')
483
+ return incidentClose(hippoRoot, tenantId, args);
486
484
  // Default subcommand: open (create). Accept both the documented
487
485
  // `incident open "<text>"` form and the bare `incident "<text>"` form: for the
488
486
  // `open` keyword the text is args[1], otherwise args[0] IS the text.
489
- const incidentText = subcommand === 'open' ? (args[1] ?? '') : subcommand;
487
+ incidentCreate(hippoRoot, tenantId, subcommand === 'open' ? (args[1] ?? '') : subcommand, flags);
488
+ }
489
+ function incidentCreate(hippoRoot, tenantId, incidentText, flags) {
490
490
  if (!incidentText) {
491
491
  printError('Usage: hippo incident "<incident>" [--context "<details>"] [--link <memory-id>]...');
492
492
  printError(' hippo incident list [--status open|resolved|closed|all] [--limit N]');