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
@@ -31,143 +31,141 @@ function briefUsage() {
31
31
  printError(' hippo brief close <id>');
32
32
  printError(' hippo brief refresh "<repo>" [--dry-run] (auto-assemble the brief from the repo\'s receipts)');
33
33
  }
34
- export function cmdProjectBrief(hippoRoot, args, flags) {
35
- requireInit(hippoRoot);
36
- const tenantId = resolveTenantId({});
37
- const subcommand = args[0] ?? '';
38
- if (subcommand === 'list') {
39
- const statusRaw = flags['status'];
40
- const status = typeof statusRaw === 'string' ? statusRaw.trim() : 'all';
41
- const repoRaw = flags['repo'];
42
- const repo = typeof repoRaw === 'string' && repoRaw.trim() ? repoRaw.trim() : undefined;
43
- const limitRaw = flags['limit'];
44
- const limit = limitRaw !== undefined ? parseInt(String(limitRaw), 10) : 100;
45
- if (!Number.isFinite(limit) || limit <= 0) {
46
- printError(`Invalid --limit: "${limitRaw}". Must be a positive integer.`);
47
- process.exit(1);
48
- }
49
- const opts = { limit, repo };
50
- if (status !== 'all') {
51
- if (!briefsModule.VALID_BRIEF_STATES.has(status)) {
52
- printError(`Invalid --status: "${status}". Must be one of: active | superseded | closed | all.`);
53
- process.exit(1);
54
- }
55
- opts.status = status;
56
- }
57
- const results = briefsModule.loadProjectBriefs(hippoRoot, tenantId, opts);
58
- if (results.length === 0) {
59
- console.log('No project briefs.');
60
- return;
61
- }
62
- console.log(`Found ${results.length} project briefs:\n`);
63
- for (const b of results)
64
- printBriefRow(b);
65
- return;
34
+ function parseListLimit(flags) {
35
+ const limitRaw = flags['limit'];
36
+ const limit = limitRaw !== undefined ? parseInt(String(limitRaw), 10) : 100;
37
+ if (!Number.isFinite(limit) || limit <= 0) {
38
+ printError(`Invalid --limit: "${limitRaw}". Must be a positive integer.`);
39
+ process.exit(1);
66
40
  }
67
- if (subcommand === 'refresh') {
68
- const repoRaw = args[1];
69
- if (!repoRaw) {
70
- printError('Usage: hippo brief refresh "<repo>" [--dry-run]');
71
- process.exit(1);
72
- }
73
- const dryRun = Boolean(flags['dry-run']);
74
- try {
75
- if (dryRun) {
76
- const { markdown, receiptCount } = briefsModule.assembleBriefFromReceipts(hippoRoot, tenantId, repoRaw);
77
- printError(`(dry-run: assembled from ${receiptCount} receipt(s); brief NOT written)`);
78
- console.log(markdown);
79
- return;
80
- }
81
- const created = briefsModule.refreshBrief(hippoRoot, tenantId, repoRaw, 'cli');
82
- console.log(`Project brief #${created.id} recorded (v${created.version}) for repo "${created.repo}".`);
83
- if (created.changeSummary)
84
- console.log(` change: ${created.changeSummary}`);
85
- if (created.memoryId)
86
- console.log(` memory: ${created.memoryId}`);
87
- }
88
- catch (e) {
89
- printError(e.message);
41
+ return limit;
42
+ }
43
+ function briefList(hippoRoot, tenantId, flags) {
44
+ const statusRaw = flags['status'];
45
+ const status = typeof statusRaw === 'string' ? statusRaw.trim() : 'all';
46
+ const repoRaw = flags['repo'];
47
+ const repo = typeof repoRaw === 'string' && repoRaw.trim() ? repoRaw.trim() : undefined;
48
+ const limit = parseListLimit(flags);
49
+ const opts = { limit, repo };
50
+ if (status !== 'all') {
51
+ if (!briefsModule.VALID_BRIEF_STATES.has(status)) {
52
+ printError(`Invalid --status: "${status}". Must be one of: active | superseded | closed | all.`);
90
53
  process.exit(1);
91
54
  }
92
- return;
55
+ opts.status = status;
93
56
  }
94
- if (subcommand === 'get') {
95
- const idRaw = args[1];
96
- if (!idRaw) {
97
- printError('Usage: hippo brief get <id>');
98
- process.exit(1);
99
- }
100
- const id = parsePositiveBriefId(idRaw);
101
- const b = briefsModule.loadProjectBriefById(hippoRoot, tenantId, id);
102
- if (!b) {
103
- printError(`Project brief ${id} not found.`);
104
- process.exit(1);
105
- }
106
- console.log(`Project brief #${b.id}`);
107
- console.log(` repo: ${b.repo}`);
108
- console.log(` status: ${b.status}`);
109
- console.log(` version: ${b.version}`);
110
- console.log(` summary: ${b.summary}`);
111
- if (b.changeSummary)
112
- console.log(` change_summary: ${b.changeSummary}`);
113
- if (b.supersededBy !== null)
114
- console.log(` superseded_by: #${b.supersededBy}`);
115
- if (b.supersededAt)
116
- console.log(` superseded_at: ${b.supersededAt}`);
117
- if (b.closedAt)
118
- console.log(` closed_at: ${b.closedAt}`);
119
- if (b.memoryId)
120
- console.log(` memory: ${b.memoryId}`);
121
- console.log(` created: ${b.createdAt}`);
57
+ const results = briefsModule.loadProjectBriefs(hippoRoot, tenantId, opts);
58
+ if (results.length === 0) {
59
+ console.log('No project briefs.');
122
60
  return;
123
61
  }
124
- if (subcommand === 'supersede') {
125
- const idRaw = args[1];
126
- if (!idRaw) {
127
- printError('Usage: hippo brief supersede <id> --summary "<text>" [--change "<summary>"]');
128
- process.exit(1);
129
- }
130
- const id = parsePositiveBriefId(idRaw);
131
- const summaryRaw = flags['summary'];
132
- if (typeof summaryRaw !== 'string' || !summaryRaw.trim()) {
133
- printError('hippo brief supersede requires --summary "<text>" for the new version.');
134
- process.exit(1);
135
- }
136
- const existing = briefsModule.loadProjectBriefById(hippoRoot, tenantId, id);
137
- if (!existing) {
138
- printError(`Project brief ${id} not found.`);
139
- process.exit(1);
140
- }
141
- const changeRaw = flags['change'];
142
- try {
143
- const created = briefsModule.saveProjectBrief(hippoRoot, tenantId, {
144
- repo: existing.repo,
145
- summary: summaryRaw,
146
- changeSummary: typeof changeRaw === 'string' && changeRaw ? changeRaw : undefined,
147
- supersedesBriefId: id,
148
- extraTags: extractPathTags(process.cwd()),
149
- });
150
- console.log(`Project brief #${created.id} recorded (v${created.version}), superseding #${id}.`);
151
- if (created.memoryId)
152
- console.log(` memory: ${created.memoryId}`);
153
- }
154
- catch (e) {
155
- printError(e.message);
156
- process.exit(1);
157
- }
158
- return;
62
+ console.log(`Found ${results.length} project briefs:
63
+ `);
64
+ for (const b of results)
65
+ printBriefRow(b);
66
+ }
67
+ function briefRefresh(hippoRoot, tenantId, args, flags) {
68
+ const repoRaw = args[1];
69
+ if (!repoRaw) {
70
+ printError('Usage: hippo brief refresh "<repo>" [--dry-run]');
71
+ process.exit(1);
159
72
  }
160
- if (subcommand === 'close') {
161
- const idRaw = args[1];
162
- if (!idRaw) {
163
- printError('Usage: hippo brief close <id>');
164
- process.exit(1);
73
+ const dryRun = Boolean(flags['dry-run']);
74
+ try {
75
+ if (dryRun) {
76
+ const { markdown, receiptCount } = briefsModule.assembleBriefFromReceipts(hippoRoot, tenantId, repoRaw);
77
+ printError(`(dry-run: assembled from ${receiptCount} receipt(s); brief NOT written)`);
78
+ console.log(markdown);
79
+ return;
165
80
  }
166
- const id = parsePositiveBriefId(idRaw);
167
- const closed = briefsModule.closeProjectBrief(hippoRoot, tenantId, id);
168
- console.log(`Project brief #${closed.id} closed.`);
169
- return;
81
+ const created = briefsModule.refreshBrief(hippoRoot, tenantId, repoRaw, 'cli');
82
+ console.log(`Project brief #${created.id} recorded (v${created.version}) for repo "${created.repo}".`);
83
+ if (created.changeSummary)
84
+ console.log(` change: ${created.changeSummary}`);
85
+ if (created.memoryId)
86
+ console.log(` memory: ${created.memoryId}`);
87
+ }
88
+ catch (e) {
89
+ printError(e.message);
90
+ process.exit(1);
170
91
  }
92
+ }
93
+ function briefGet(hippoRoot, tenantId, args) {
94
+ const idRaw = args[1];
95
+ if (!idRaw) {
96
+ printError('Usage: hippo brief get <id>');
97
+ process.exit(1);
98
+ }
99
+ const id = parsePositiveBriefId(idRaw);
100
+ const b = briefsModule.loadProjectBriefById(hippoRoot, tenantId, id);
101
+ if (!b) {
102
+ printError(`Project brief ${id} not found.`);
103
+ process.exit(1);
104
+ }
105
+ console.log(`Project brief #${b.id}`);
106
+ console.log(` repo: ${b.repo}`);
107
+ console.log(` status: ${b.status}`);
108
+ console.log(` version: ${b.version}`);
109
+ console.log(` summary: ${b.summary}`);
110
+ if (b.changeSummary)
111
+ console.log(` change_summary: ${b.changeSummary}`);
112
+ if (b.supersededBy !== null)
113
+ console.log(` superseded_by: #${b.supersededBy}`);
114
+ if (b.supersededAt)
115
+ console.log(` superseded_at: ${b.supersededAt}`);
116
+ if (b.closedAt)
117
+ console.log(` closed_at: ${b.closedAt}`);
118
+ if (b.memoryId)
119
+ console.log(` memory: ${b.memoryId}`);
120
+ console.log(` created: ${b.createdAt}`);
121
+ }
122
+ function briefSupersede(hippoRoot, tenantId, args, flags) {
123
+ const idRaw = args[1];
124
+ if (!idRaw) {
125
+ printError('Usage: hippo brief supersede <id> --summary "<text>" [--change "<summary>"]');
126
+ process.exit(1);
127
+ }
128
+ const id = parsePositiveBriefId(idRaw);
129
+ const summaryRaw = flags['summary'];
130
+ if (typeof summaryRaw !== 'string' || !summaryRaw.trim()) {
131
+ printError('hippo brief supersede requires --summary "<text>" for the new version.');
132
+ process.exit(1);
133
+ }
134
+ const existing = briefsModule.loadProjectBriefById(hippoRoot, tenantId, id);
135
+ if (!existing) {
136
+ printError(`Project brief ${id} not found.`);
137
+ process.exit(1);
138
+ }
139
+ const changeRaw = flags['change'];
140
+ try {
141
+ const created = briefsModule.saveProjectBrief(hippoRoot, tenantId, {
142
+ repo: existing.repo,
143
+ summary: summaryRaw,
144
+ changeSummary: typeof changeRaw === 'string' && changeRaw ? changeRaw : undefined,
145
+ supersedesBriefId: id,
146
+ extraTags: extractPathTags(process.cwd()),
147
+ });
148
+ console.log(`Project brief #${created.id} recorded (v${created.version}), superseding #${id}.`);
149
+ if (created.memoryId)
150
+ console.log(` memory: ${created.memoryId}`);
151
+ }
152
+ catch (e) {
153
+ printError(e.message);
154
+ process.exit(1);
155
+ }
156
+ }
157
+ function briefClose(hippoRoot, tenantId, args) {
158
+ const idRaw = args[1];
159
+ if (!idRaw) {
160
+ printError('Usage: hippo brief close <id>');
161
+ process.exit(1);
162
+ }
163
+ const id = parsePositiveBriefId(idRaw);
164
+ const closed = briefsModule.closeProjectBrief(hippoRoot, tenantId, id);
165
+ console.log(`Project brief #${closed.id} closed.`);
166
+ }
167
+ function briefCreate(hippoRoot, tenantId, args, flags) {
168
+ const subcommand = args[0] ?? '';
171
169
  // Default subcommand: new (create). Accept both `brief new "<repo>"` and the
172
170
  // bare `brief "<repo>"` form: for the `new` keyword the repo is args[1].
173
171
  const repo = subcommand === 'new' ? (args[1] ?? '') : subcommand;
@@ -191,6 +189,22 @@ export function cmdProjectBrief(hippoRoot, args, flags) {
191
189
  process.exit(1);
192
190
  }
193
191
  }
192
+ export function cmdProjectBrief(hippoRoot, args, flags) {
193
+ requireInit(hippoRoot);
194
+ const tenantId = resolveTenantId({});
195
+ const subcommand = args[0] ?? '';
196
+ if (subcommand === 'list')
197
+ return briefList(hippoRoot, tenantId, flags);
198
+ if (subcommand === 'refresh')
199
+ return briefRefresh(hippoRoot, tenantId, args, flags);
200
+ if (subcommand === 'get')
201
+ return briefGet(hippoRoot, tenantId, args);
202
+ if (subcommand === 'supersede')
203
+ return briefSupersede(hippoRoot, tenantId, args, flags);
204
+ if (subcommand === 'close')
205
+ return briefClose(hippoRoot, tenantId, args);
206
+ briefCreate(hippoRoot, tenantId, args, flags);
207
+ }
194
208
  function parsePositiveNoteId(idRaw) {
195
209
  const s = String(idRaw ?? '').trim();
196
210
  const id = parseInt(s, 10);
@@ -212,203 +226,207 @@ function noteUsage() {
212
226
  printError(' hippo note supersede <id> --text "<note>" [--change "<summary>"]');
213
227
  printError(' hippo note close <id>');
214
228
  }
215
- export function cmdGraph(hippoRoot, args, flags) {
216
- requireInit(hippoRoot);
217
- const tenantId = resolveTenantId({});
218
- const subcommand = args[0] ?? '';
219
- if (subcommand === 'extract') {
220
- const result = extractGraph(hippoRoot, tenantId);
221
- const byType = Object.entries(result.byType)
222
- .map(([t, n]) => `${t} ${n}`)
223
- .join(', ');
224
- const supersedes = result.relations - result.references;
225
- console.log(`Graph extracted: ${result.entities} entities (${byType}) + ${result.relations} relations (${supersedes} supersedes, ${result.references} references).`);
226
- if (result.truncated.length > 0) {
227
- printError(`WARNING: under-extracted (hit the per-type cap): ${result.truncated.join(', ')}. The graph is incomplete for those types.`);
228
- }
229
+ function graphExtract(hippoRoot, tenantId) {
230
+ const result = extractGraph(hippoRoot, tenantId);
231
+ const byType = Object.entries(result.byType)
232
+ .map(([t, n]) => `${t} ${n}`)
233
+ .join(', ');
234
+ const supersedes = result.relations - result.references;
235
+ console.log(`Graph extracted: ${result.entities} entities (${byType}) + ${result.relations} relations (${supersedes} supersedes, ${result.references} references).`);
236
+ if (result.truncated.length > 0) {
237
+ printError(`WARNING: under-extracted (hit the per-type cap): ${result.truncated.join(', ')}. The graph is incomplete for those types.`);
238
+ }
239
+ }
240
+ function graphShow(hippoRoot, tenantId, entity, flags) {
241
+ const model = buildGraphModel(hippoRoot, tenantId, { entity, limit: DEFAULT_VIEW_LIMIT });
242
+ if (flags['json']) {
243
+ console.log(JSON.stringify(model, null, 2));
229
244
  return;
230
245
  }
231
- const entity = typeof flags['entity'] === 'string' ? flags['entity'] : undefined;
232
- if (subcommand === 'show') {
233
- const model = buildGraphModel(hippoRoot, tenantId, { entity, limit: DEFAULT_VIEW_LIMIT });
234
- if (flags['json']) {
235
- console.log(JSON.stringify(model, null, 2));
236
- return;
237
- }
238
- if (model.nodes.length === 0) {
239
- console.log(entity ? `No entity named "${entity}".` : 'Graph is empty. Run `hippo graph extract` first.');
240
- return;
241
- }
242
- console.log(`Graph: ${model.nodes.length} entities, ${model.edges.length} relations${model.truncated ? ' (truncated)' : ''}`);
243
- const byType = new Map();
244
- for (const n of model.nodes) {
245
- const arr = byType.get(n.type) ?? [];
246
- arr.push({ id: n.id, name: n.name });
247
- byType.set(n.type, arr);
248
- }
249
- for (const [type, ns] of byType) {
250
- console.log(`\n${type} (${ns.length}):`);
251
- for (const n of ns)
252
- console.log(` [${n.id}] ${n.name}`);
253
- }
254
- if (model.edges.length > 0) {
255
- const nameById = new Map(model.nodes.map((n) => [n.id, n.name]));
256
- console.log('\nrelations:');
257
- for (const e of model.edges) {
258
- console.log(` ${nameById.get(e.from)} --${e.relType}--> ${nameById.get(e.to)}`);
259
- }
260
- }
246
+ if (model.nodes.length === 0) {
247
+ console.log(entity ? `No entity named "${entity}".` : 'Graph is empty. Run `hippo graph extract` first.');
261
248
  return;
262
249
  }
263
- if (subcommand === 'view') {
264
- const format = typeof flags['format'] === 'string' ? flags['format'] : 'html';
265
- if (format !== 'html' && format !== 'canvas') {
266
- printError("graph view: --format must be 'html' or 'canvas'");
267
- process.exit(1);
268
- }
269
- const model = buildGraphModel(hippoRoot, tenantId, { entity, limit: DEFAULT_VIEW_LIMIT });
270
- const content = format === 'canvas' ? renderGraphCanvas(model) : renderGraphHtml(model);
271
- const defaultOut = format === 'canvas' ? 'hippo-graph.canvas' : 'hippo-graph.html';
272
- const out = typeof flags['out'] === 'string' ? flags['out'] : defaultOut;
273
- fs.writeFileSync(out, content, 'utf8');
274
- console.log(`Wrote ${model.nodes.length} entities + ${model.edges.length} relations to ${out}${model.truncated ? ' (truncated)' : ''}`);
275
- if (flags['open'] && format === 'html') {
276
- // Best-effort browser launch; never fail the command if it doesn't work.
277
- try {
278
- const [cmd, cmdArgs] = process.platform === 'win32'
279
- ? ['cmd', ['/c', 'start', '', out]]
280
- : process.platform === 'darwin'
281
- ? ['open', [out]]
282
- : ['xdg-open', [out]];
283
- const child = spawn(cmd, cmdArgs, { detached: true, stdio: 'ignore', windowsHide: true });
284
- // A missing launcher (e.g. xdg-open absent) emits 'error' asynchronously;
285
- // an unhandled 'error' event would throw, so swallow it — the file is
286
- // already written and its path printed above.
287
- child.on('error', () => { });
288
- child.unref();
289
- }
290
- catch {
291
- /* ignore — the file is written; the path is printed above */
292
- }
250
+ console.log(`Graph: ${model.nodes.length} entities, ${model.edges.length} relations${model.truncated ? ' (truncated)' : ''}`);
251
+ const byType = new Map();
252
+ for (const n of model.nodes) {
253
+ const arr = byType.get(n.type) ?? [];
254
+ arr.push({ id: n.id, name: n.name });
255
+ byType.set(n.type, arr);
256
+ }
257
+ for (const [type, ns] of byType) {
258
+ console.log(`\n${type} (${ns.length}):`);
259
+ for (const n of ns)
260
+ console.log(` [${n.id}] ${n.name}`);
261
+ }
262
+ if (model.edges.length > 0) {
263
+ const nameById = new Map(model.nodes.map((n) => [n.id, n.name]));
264
+ console.log('\nrelations:');
265
+ for (const e of model.edges) {
266
+ console.log(` ${nameById.get(e.from)} --${e.relType}--> ${nameById.get(e.to)}`);
293
267
  }
294
- return;
295
268
  }
269
+ }
270
+ function openInBrowser(out) {
271
+ // Best-effort browser launch; never fail the command if it doesn't work.
272
+ try {
273
+ const [cmd, cmdArgs] = process.platform === 'win32'
274
+ ? ['cmd', ['/c', 'start', '', out]]
275
+ : process.platform === 'darwin'
276
+ ? ['open', [out]]
277
+ : ['xdg-open', [out]];
278
+ const child = spawn(cmd, cmdArgs, { detached: true, stdio: 'ignore', windowsHide: true });
279
+ // A missing launcher (e.g. xdg-open absent) emits 'error' asynchronously;
280
+ // an unhandled 'error' event would throw, so swallow it — the file is
281
+ // already written and its path printed above.
282
+ child.on('error', () => { });
283
+ child.unref();
284
+ }
285
+ catch {
286
+ /* ignore — the file is written; the path is printed above */
287
+ }
288
+ }
289
+ function graphView(hippoRoot, tenantId, entity, flags) {
290
+ const format = typeof flags['format'] === 'string' ? flags['format'] : 'html';
291
+ if (format !== 'html' && format !== 'canvas') {
292
+ printError("graph view: --format must be 'html' or 'canvas'");
293
+ process.exit(1);
294
+ }
295
+ const model = buildGraphModel(hippoRoot, tenantId, { entity, limit: DEFAULT_VIEW_LIMIT });
296
+ const content = format === 'canvas' ? renderGraphCanvas(model) : renderGraphHtml(model);
297
+ const defaultOut = format === 'canvas' ? 'hippo-graph.canvas' : 'hippo-graph.html';
298
+ const out = typeof flags['out'] === 'string' ? flags['out'] : defaultOut;
299
+ fs.writeFileSync(out, content, 'utf8');
300
+ console.log(`Wrote ${model.nodes.length} entities + ${model.edges.length} relations to ${out}${model.truncated ? ' (truncated)' : ''}`);
301
+ if (flags['open'] && format === 'html')
302
+ openInBrowser(out);
303
+ }
304
+ export function cmdGraph(hippoRoot, args, flags) {
305
+ requireInit(hippoRoot);
306
+ const tenantId = resolveTenantId({});
307
+ const subcommand = args[0] ?? '';
308
+ if (subcommand === 'extract')
309
+ return graphExtract(hippoRoot, tenantId);
310
+ const entity = typeof flags['entity'] === 'string' ? flags['entity'] : undefined;
311
+ if (subcommand === 'show')
312
+ return graphShow(hippoRoot, tenantId, entity, flags);
313
+ if (subcommand === 'view')
314
+ return graphView(hippoRoot, tenantId, entity, flags);
296
315
  printError('Usage:\n' +
297
316
  ' hippo graph extract Rebuild the entity/relation graph from consolidated objects\n' +
298
317
  ' hippo graph show [--entity NAME] [--json] Inspect entities + their edges (text or JSON)\n' +
299
318
  ' hippo graph view [--out FILE] [--open] [--format html|canvas] [--entity NAME] Generate an interactive node-link diagram');
300
319
  process.exit(1);
301
320
  }
302
- export function cmdCustomerNote(hippoRoot, args, flags) {
303
- requireInit(hippoRoot);
304
- const tenantId = resolveTenantId({});
305
- const subcommand = args[0] ?? '';
306
- if (subcommand === 'list') {
307
- const statusRaw = flags['status'];
308
- const status = typeof statusRaw === 'string' ? statusRaw.trim() : 'all';
309
- const customerRaw = flags['customer'];
310
- const customer = typeof customerRaw === 'string' && customerRaw.trim() ? customerRaw.trim() : undefined;
311
- const limitRaw = flags['limit'];
312
- const limit = limitRaw !== undefined ? parseInt(String(limitRaw), 10) : 100;
313
- if (!Number.isFinite(limit) || limit <= 0) {
314
- printError(`Invalid --limit: "${limitRaw}". Must be a positive integer.`);
321
+ function noteList(hippoRoot, tenantId, flags) {
322
+ const statusRaw = flags['status'];
323
+ const status = typeof statusRaw === 'string' ? statusRaw.trim() : 'all';
324
+ const customerRaw = flags['customer'];
325
+ const customer = typeof customerRaw === 'string' && customerRaw.trim() ? customerRaw.trim() : undefined;
326
+ const limit = parseListLimit(flags);
327
+ const opts = { limit, customer };
328
+ if (status !== 'all') {
329
+ if (!customerNotesModule.VALID_NOTE_STATES.has(status)) {
330
+ printError(`Invalid --status: "${status}". Must be one of: active | superseded | closed | all.`);
315
331
  process.exit(1);
316
332
  }
317
- const opts = { limit, customer };
318
- if (status !== 'all') {
319
- if (!customerNotesModule.VALID_NOTE_STATES.has(status)) {
320
- printError(`Invalid --status: "${status}". Must be one of: active | superseded | closed | all.`);
321
- process.exit(1);
322
- }
323
- opts.status = status;
324
- }
325
- const results = customerNotesModule.loadCustomerNotes(hippoRoot, tenantId, opts);
326
- if (results.length === 0) {
327
- console.log('No customer notes.');
328
- return;
329
- }
330
- console.log(`Found ${results.length} customer notes:\n`);
331
- for (const n of results)
332
- printNoteRow(n);
333
- return;
333
+ opts.status = status;
334
334
  }
335
- if (subcommand === 'get') {
336
- const idRaw = args[1];
337
- if (!idRaw) {
338
- printError('Usage: hippo note get <id>');
339
- process.exit(1);
340
- }
341
- const id = parsePositiveNoteId(idRaw);
342
- const n = customerNotesModule.loadCustomerNoteById(hippoRoot, tenantId, id);
343
- if (!n) {
344
- printError(`Customer note ${id} not found.`);
345
- process.exit(1);
346
- }
347
- console.log(`Customer note #${n.id}`);
348
- console.log(` customer: ${n.customer}`);
349
- console.log(` status: ${n.status}`);
350
- console.log(` version: ${n.version}`);
351
- console.log(` note: ${n.note}`);
352
- if (n.changeSummary)
353
- console.log(` change_summary: ${n.changeSummary}`);
354
- if (n.supersededBy !== null)
355
- console.log(` superseded_by: #${n.supersededBy}`);
356
- if (n.supersededAt)
357
- console.log(` superseded_at: ${n.supersededAt}`);
358
- if (n.closedAt)
359
- console.log(` closed_at: ${n.closedAt}`);
360
- if (n.memoryId)
361
- console.log(` memory: ${n.memoryId}`);
362
- console.log(` created: ${n.createdAt}`);
335
+ const results = customerNotesModule.loadCustomerNotes(hippoRoot, tenantId, opts);
336
+ if (results.length === 0) {
337
+ console.log('No customer notes.');
363
338
  return;
364
339
  }
365
- if (subcommand === 'supersede') {
366
- const idRaw = args[1];
367
- if (!idRaw) {
368
- printError('Usage: hippo note supersede <id> --text "<note>" [--change "<summary>"]');
369
- process.exit(1);
370
- }
371
- const id = parsePositiveNoteId(idRaw);
372
- const textRaw = flags['text'];
373
- if (typeof textRaw !== 'string' || !textRaw.trim()) {
374
- printError('hippo note supersede requires --text "<note>" for the new version.');
375
- process.exit(1);
376
- }
377
- const existing = customerNotesModule.loadCustomerNoteById(hippoRoot, tenantId, id);
378
- if (!existing) {
379
- printError(`Customer note ${id} not found.`);
380
- process.exit(1);
381
- }
382
- const changeRaw = flags['change'];
383
- try {
384
- const created = customerNotesModule.saveCustomerNote(hippoRoot, tenantId, {
385
- customer: existing.customer,
386
- note: textRaw,
387
- changeSummary: typeof changeRaw === 'string' && changeRaw ? changeRaw : undefined,
388
- supersedesNoteId: id,
389
- extraTags: extractPathTags(process.cwd()),
390
- });
391
- console.log(`Customer note #${created.id} recorded (v${created.version}), superseding #${id}.`);
392
- if (created.memoryId)
393
- console.log(` memory: ${created.memoryId}`);
394
- }
395
- catch (e) {
396
- printError(e.message);
397
- process.exit(1);
398
- }
399
- return;
340
+ console.log(`Found ${results.length} customer notes:\n`);
341
+ for (const n of results)
342
+ printNoteRow(n);
343
+ }
344
+ function noteGet(hippoRoot, tenantId, args) {
345
+ const idRaw = args[1];
346
+ if (!idRaw) {
347
+ printError('Usage: hippo note get <id>');
348
+ process.exit(1);
400
349
  }
401
- if (subcommand === 'close') {
402
- const idRaw = args[1];
403
- if (!idRaw) {
404
- printError('Usage: hippo note close <id>');
405
- process.exit(1);
406
- }
407
- const id = parsePositiveNoteId(idRaw);
408
- const closed = customerNotesModule.closeCustomerNote(hippoRoot, tenantId, id);
409
- console.log(`Customer note #${closed.id} closed.`);
410
- return;
350
+ const id = parsePositiveNoteId(idRaw);
351
+ const n = customerNotesModule.loadCustomerNoteById(hippoRoot, tenantId, id);
352
+ if (!n) {
353
+ printError(`Customer note ${id} not found.`);
354
+ process.exit(1);
355
+ }
356
+ console.log(`Customer note #${n.id}`);
357
+ console.log(` customer: ${n.customer}`);
358
+ console.log(` status: ${n.status}`);
359
+ console.log(` version: ${n.version}`);
360
+ console.log(` note: ${n.note}`);
361
+ if (n.changeSummary)
362
+ console.log(` change_summary: ${n.changeSummary}`);
363
+ if (n.supersededBy !== null)
364
+ console.log(` superseded_by: #${n.supersededBy}`);
365
+ if (n.supersededAt)
366
+ console.log(` superseded_at: ${n.supersededAt}`);
367
+ if (n.closedAt)
368
+ console.log(` closed_at: ${n.closedAt}`);
369
+ if (n.memoryId)
370
+ console.log(` memory: ${n.memoryId}`);
371
+ console.log(` created: ${n.createdAt}`);
372
+ }
373
+ function noteSupersede(hippoRoot, tenantId, args, flags) {
374
+ const idRaw = args[1];
375
+ if (!idRaw) {
376
+ printError('Usage: hippo note supersede <id> --text "<note>" [--change "<summary>"]');
377
+ process.exit(1);
378
+ }
379
+ const id = parsePositiveNoteId(idRaw);
380
+ const textRaw = flags['text'];
381
+ if (typeof textRaw !== 'string' || !textRaw.trim()) {
382
+ printError('hippo note supersede requires --text "<note>" for the new version.');
383
+ process.exit(1);
384
+ }
385
+ const existing = customerNotesModule.loadCustomerNoteById(hippoRoot, tenantId, id);
386
+ if (!existing) {
387
+ printError(`Customer note ${id} not found.`);
388
+ process.exit(1);
411
389
  }
390
+ const changeRaw = flags['change'];
391
+ try {
392
+ const created = customerNotesModule.saveCustomerNote(hippoRoot, tenantId, {
393
+ customer: existing.customer,
394
+ note: textRaw,
395
+ changeSummary: typeof changeRaw === 'string' && changeRaw ? changeRaw : undefined,
396
+ supersedesNoteId: id,
397
+ extraTags: extractPathTags(process.cwd()),
398
+ });
399
+ console.log(`Customer note #${created.id} recorded (v${created.version}), superseding #${id}.`);
400
+ if (created.memoryId)
401
+ console.log(` memory: ${created.memoryId}`);
402
+ }
403
+ catch (e) {
404
+ printError(e.message);
405
+ process.exit(1);
406
+ }
407
+ }
408
+ function noteClose(hippoRoot, tenantId, args) {
409
+ const idRaw = args[1];
410
+ if (!idRaw) {
411
+ printError('Usage: hippo note close <id>');
412
+ process.exit(1);
413
+ }
414
+ const id = parsePositiveNoteId(idRaw);
415
+ const closed = customerNotesModule.closeCustomerNote(hippoRoot, tenantId, id);
416
+ console.log(`Customer note #${closed.id} closed.`);
417
+ }
418
+ export function cmdCustomerNote(hippoRoot, args, flags) {
419
+ requireInit(hippoRoot);
420
+ const tenantId = resolveTenantId({});
421
+ const subcommand = args[0] ?? '';
422
+ if (subcommand === 'list')
423
+ return noteList(hippoRoot, tenantId, flags);
424
+ if (subcommand === 'get')
425
+ return noteGet(hippoRoot, tenantId, args);
426
+ if (subcommand === 'supersede')
427
+ return noteSupersede(hippoRoot, tenantId, args, flags);
428
+ if (subcommand === 'close')
429
+ return noteClose(hippoRoot, tenantId, args);
412
430
  // Default subcommand: new (create). Accept both `note new "<customer>"` and the
413
431
  // bare `note "<customer>"` form: for the `new` keyword the customer is args[1].
414
432
  const customer = subcommand === 'new' ? (args[1] ?? '') : subcommand;