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.
- package/README.md +28 -53
- package/dist/agent-memories/apply.d.ts +1 -1
- package/dist/agent-memories/claude-code.d.ts +3 -1
- package/dist/agent-memories/claude-code.js +53 -9
- package/dist/agent-memories/sync.d.ts +3 -3
- package/dist/agent-memories/sync.js +14 -6
- package/dist/agent-memories/types.d.ts +0 -2
- package/dist/api/assemble.js +55 -54
- package/dist/api/context-select.d.ts +49 -0
- package/dist/api/context-select.js +344 -0
- package/dist/api/context.d.ts +2 -2
- package/dist/api/context.js +195 -522
- package/dist/api/drill-down.js +36 -33
- package/dist/api/promote.js +55 -66
- package/dist/api/recall.js +303 -438
- package/dist/api/sleep.js +203 -218
- package/dist/capture/compact.d.ts +1 -1
- package/dist/capture/compact.js +2 -2
- package/dist/cli/briefs.js +324 -306
- package/dist/cli/context.js +44 -34
- package/dist/cli/continuity.js +283 -271
- package/dist/cli/curate.js +35 -34
- package/dist/cli/decisions.js +333 -333
- package/dist/cli/explain.js +66 -60
- package/dist/cli/maintenance.js +62 -51
- package/dist/cli/playbooks.js +387 -370
- package/dist/cli/projects.js +8 -5
- package/dist/cli/recall.js +28 -43
- package/dist/cli/remember.js +113 -70
- package/dist/cli/session-hooks.js +100 -90
- package/dist/cli/setup.js +267 -246
- package/dist/cli/status.js +73 -64
- package/dist/cli/transfer.js +85 -99
- package/dist/compaction-record.d.ts +0 -2
- package/dist/compaction-record.js +1 -1
- package/dist/customer-notes.js +77 -68
- package/dist/dag.js +222 -186
- package/dist/decisions.js +93 -76
- package/dist/doctor.js +11 -7
- package/dist/goals.js +99 -86
- package/dist/incidents.js +45 -38
- package/dist/policies.js +85 -68
- package/dist/processes.js +87 -71
- package/dist/project-briefs.js +135 -108
- package/dist/project-merge.d.ts +12 -4
- package/dist/project-merge.js +130 -45
- package/dist/shared.d.ts +9 -0
- package/dist/shared.js +10 -8
- package/dist/skills.js +81 -65
- package/dist/store/search-rows.d.ts +2 -2
- package/dist/store/search-rows.js +15 -9
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
- package/extensions/openclaw-plugin/package.json +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/dist/cli/briefs.js
CHANGED
|
@@ -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
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
55
|
+
opts.status = status;
|
|
93
56
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
if (
|
|
163
|
-
|
|
164
|
-
|
|
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
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
232
|
-
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
const
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
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
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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;
|