hippo-memory 0.2.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/LICENSE +21 -0
- package/README.md +305 -0
- package/dist/autolearn.d.ts +34 -0
- package/dist/autolearn.d.ts.map +1 -0
- package/dist/autolearn.js +119 -0
- package/dist/autolearn.js.map +1 -0
- package/dist/cli.d.ts +21 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +956 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +16 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +35 -0
- package/dist/config.js.map +1 -0
- package/dist/consolidate.d.ts +24 -0
- package/dist/consolidate.d.ts.map +1 -0
- package/dist/consolidate.js +133 -0
- package/dist/consolidate.js.map +1 -0
- package/dist/embeddings.d.ts +39 -0
- package/dist/embeddings.d.ts.map +1 -0
- package/dist/embeddings.js +184 -0
- package/dist/embeddings.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/memory.d.ts +59 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +118 -0
- package/dist/memory.js.map +1 -0
- package/dist/search.d.ts +51 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +239 -0
- package/dist/search.js.map +1 -0
- package/dist/shared.d.ts +38 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/shared.js +111 -0
- package/dist/shared.js.map +1 -0
- package/dist/store.d.ts +70 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +244 -0
- package/dist/store.js.map +1 -0
- package/dist/yaml.d.ts +14 -0
- package/dist/yaml.d.ts.map +1 -0
- package/dist/yaml.js +81 -0
- package/dist/yaml.js.map +1 -0
- package/package.json +58 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,956 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Hippo CLI - biologically-inspired memory system for AI agents.
|
|
4
|
+
*
|
|
5
|
+
* Commands:
|
|
6
|
+
* hippo init [--global]
|
|
7
|
+
* hippo remember <text> [--tag <t>] [--error] [--pin] [--global]
|
|
8
|
+
* hippo recall <query> [--budget <n>] [--json]
|
|
9
|
+
* hippo sleep [--dry-run]
|
|
10
|
+
* hippo status
|
|
11
|
+
* hippo outcome --good | --bad [--id <id>]
|
|
12
|
+
* hippo forget <id>
|
|
13
|
+
* hippo inspect <id>
|
|
14
|
+
* hippo embed [--status]
|
|
15
|
+
* hippo watch "<command>"
|
|
16
|
+
* hippo learn --git [--days <n>]
|
|
17
|
+
* hippo promote <id>
|
|
18
|
+
* hippo sync
|
|
19
|
+
*/
|
|
20
|
+
import * as path from 'path';
|
|
21
|
+
import * as fs from 'fs';
|
|
22
|
+
import { execSync } from 'child_process';
|
|
23
|
+
import { createMemory, calculateStrength, applyOutcome, Layer, } from './memory.js';
|
|
24
|
+
import { getHippoRoot, isInitialized, initStore, writeEntry, readEntry, deleteEntry, loadAllEntries, loadIndex, saveIndex, loadStats, updateStats, } from './store.js';
|
|
25
|
+
import { search, markRetrieved, estimateTokens } from './search.js';
|
|
26
|
+
import { consolidate } from './consolidate.js';
|
|
27
|
+
import { isEmbeddingAvailable, embedAll, embedMemory, loadEmbeddingIndex, } from './embeddings.js';
|
|
28
|
+
import { captureError, extractLessons, deduplicateLesson, runWatched, fetchGitLog, } from './autolearn.js';
|
|
29
|
+
import { getGlobalRoot, initGlobal, promoteToGlobal, searchBoth, syncGlobalToLocal, } from './shared.js';
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Helpers
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
function requireInit(hippoRoot) {
|
|
34
|
+
if (!isInitialized(hippoRoot)) {
|
|
35
|
+
console.error('No .hippo directory found. Run `hippo init` first.');
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function parseArgs(argv) {
|
|
40
|
+
const [, , command = '', ...rest] = argv;
|
|
41
|
+
const args = [];
|
|
42
|
+
const flags = {};
|
|
43
|
+
let i = 0;
|
|
44
|
+
while (i < rest.length) {
|
|
45
|
+
const part = rest[i];
|
|
46
|
+
if (part.startsWith('--')) {
|
|
47
|
+
const key = part.slice(2);
|
|
48
|
+
const next = rest[i + 1];
|
|
49
|
+
if (!next || next.startsWith('--')) {
|
|
50
|
+
// Boolean flag
|
|
51
|
+
flags[key] = true;
|
|
52
|
+
i++;
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
// Check if it's a repeatable flag (tag)
|
|
56
|
+
if (key === 'tag') {
|
|
57
|
+
if (Array.isArray(flags[key])) {
|
|
58
|
+
flags[key].push(next);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
flags[key] = [next];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
flags[key] = next;
|
|
66
|
+
}
|
|
67
|
+
i += 2;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
args.push(part);
|
|
72
|
+
i++;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return { command, args, flags };
|
|
76
|
+
}
|
|
77
|
+
function fmt(n, digits = 2) {
|
|
78
|
+
return n.toFixed(digits);
|
|
79
|
+
}
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// Commands
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
function cmdInit(hippoRoot, flags) {
|
|
84
|
+
if (flags['global']) {
|
|
85
|
+
const globalRoot = getGlobalRoot();
|
|
86
|
+
if (isInitialized(globalRoot)) {
|
|
87
|
+
console.log('Already initialized global store at', globalRoot);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
initGlobal();
|
|
91
|
+
console.log('Initialized global Hippo store at', globalRoot);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (isInitialized(hippoRoot)) {
|
|
95
|
+
console.log('Already initialized at', hippoRoot);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
initStore(hippoRoot);
|
|
99
|
+
console.log('Initialized Hippo at', hippoRoot);
|
|
100
|
+
console.log(' Directories: buffer/ episodic/ semantic/ conflicts/');
|
|
101
|
+
console.log(' Files: index.json stats.json');
|
|
102
|
+
}
|
|
103
|
+
function cmdRemember(hippoRoot, text, flags) {
|
|
104
|
+
const useGlobal = Boolean(flags['global']);
|
|
105
|
+
const targetRoot = useGlobal ? getGlobalRoot() : hippoRoot;
|
|
106
|
+
if (useGlobal) {
|
|
107
|
+
initGlobal();
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
requireInit(hippoRoot);
|
|
111
|
+
}
|
|
112
|
+
const rawTags = Array.isArray(flags['tag']) ? flags['tag'] : [];
|
|
113
|
+
if (flags['error'])
|
|
114
|
+
rawTags.push('error');
|
|
115
|
+
const entry = createMemory(text, {
|
|
116
|
+
layer: Layer.Episodic,
|
|
117
|
+
tags: rawTags,
|
|
118
|
+
pinned: Boolean(flags['pin']),
|
|
119
|
+
source: useGlobal ? 'cli-global' : 'cli',
|
|
120
|
+
});
|
|
121
|
+
writeEntry(targetRoot, entry);
|
|
122
|
+
updateStats(targetRoot, { remembered: 1 });
|
|
123
|
+
const prefix = useGlobal ? '[global] ' : '';
|
|
124
|
+
console.log(`${prefix}Remembered [${entry.id}]`);
|
|
125
|
+
console.log(` Layer: ${entry.layer} | Strength: ${fmt(entry.strength)} | Half-life: ${entry.half_life_days}d`);
|
|
126
|
+
if (entry.tags.length > 0)
|
|
127
|
+
console.log(` Tags: ${entry.tags.join(', ')}`);
|
|
128
|
+
if (entry.pinned)
|
|
129
|
+
console.log(' Pinned (no decay)');
|
|
130
|
+
// Auto-embed if available
|
|
131
|
+
if (isEmbeddingAvailable()) {
|
|
132
|
+
embedMemory(targetRoot, entry).catch(() => {
|
|
133
|
+
// Silently ignore embedding errors
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function cmdRecall(hippoRoot, query, flags) {
|
|
138
|
+
requireInit(hippoRoot);
|
|
139
|
+
const budget = parseInt(String(flags['budget'] ?? '4000'), 10);
|
|
140
|
+
const asJson = Boolean(flags['json']);
|
|
141
|
+
const globalRoot = getGlobalRoot();
|
|
142
|
+
const localEntries = loadAllEntries(hippoRoot);
|
|
143
|
+
const globalEntries = isInitialized(globalRoot) ? loadAllEntries(globalRoot) : [];
|
|
144
|
+
const hasGlobal = globalEntries.length > 0;
|
|
145
|
+
let results;
|
|
146
|
+
if (hasGlobal) {
|
|
147
|
+
// Use searchBoth for merged results
|
|
148
|
+
const merged = searchBoth(query, hippoRoot, globalRoot, { budget });
|
|
149
|
+
results = merged;
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
results = search(query, localEntries, { budget });
|
|
153
|
+
}
|
|
154
|
+
if (results.length === 0) {
|
|
155
|
+
if (asJson) {
|
|
156
|
+
console.log(JSON.stringify({ query, results: [], total: 0 }));
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
console.log('No memories found for:', query);
|
|
160
|
+
}
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
// Update retrieval metadata and persist
|
|
164
|
+
const updated = markRetrieved(results.map((r) => r.entry));
|
|
165
|
+
for (const u of updated) {
|
|
166
|
+
// Determine which store this entry belongs to
|
|
167
|
+
const localIndex = loadIndex(hippoRoot);
|
|
168
|
+
const targetRoot = localIndex.entries[u.id] ? hippoRoot : (isInitialized(globalRoot) ? globalRoot : hippoRoot);
|
|
169
|
+
writeEntry(targetRoot, u);
|
|
170
|
+
}
|
|
171
|
+
// Track last retrieval IDs for outcome command
|
|
172
|
+
const index = loadIndex(hippoRoot);
|
|
173
|
+
index.last_retrieval_ids = updated.map((u) => u.id);
|
|
174
|
+
saveIndex(hippoRoot, index);
|
|
175
|
+
updateStats(hippoRoot, { recalled: results.length });
|
|
176
|
+
if (asJson) {
|
|
177
|
+
const output = results.map((r) => ({
|
|
178
|
+
id: r.entry.id,
|
|
179
|
+
score: r.score,
|
|
180
|
+
strength: r.entry.strength,
|
|
181
|
+
tokens: r.tokens,
|
|
182
|
+
tags: r.entry.tags,
|
|
183
|
+
content: r.entry.content,
|
|
184
|
+
}));
|
|
185
|
+
console.log(JSON.stringify({ query, budget, results: output, total: output.length }));
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const totalTokens = results.reduce((sum, r) => sum + r.tokens, 0);
|
|
189
|
+
console.log(`Found ${results.length} memories (${totalTokens} tokens) for: "${query}"\n`);
|
|
190
|
+
for (const r of results) {
|
|
191
|
+
const e = r.entry;
|
|
192
|
+
const strengthBar = '\u2588'.repeat(Math.round(e.strength * 10)) + '\u2591'.repeat(10 - Math.round(e.strength * 10));
|
|
193
|
+
const globalMark = (isInitialized(globalRoot) && !loadIndex(hippoRoot).entries[e.id]) ? ' [global]' : '';
|
|
194
|
+
console.log(`--- ${e.id} [${e.layer}]${globalMark} score=${fmt(r.score, 3)} strength=${fmt(e.strength)}`);
|
|
195
|
+
console.log(` [${strengthBar}] tags: ${e.tags.join(', ') || 'none'} | retrieved: ${e.retrieval_count}x`);
|
|
196
|
+
console.log();
|
|
197
|
+
console.log(e.content);
|
|
198
|
+
console.log();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
function cmdSleep(hippoRoot, flags) {
|
|
202
|
+
requireInit(hippoRoot);
|
|
203
|
+
const dryRun = Boolean(flags['dry-run']);
|
|
204
|
+
console.log(`Running consolidation${dryRun ? ' (dry run)' : ''}...`);
|
|
205
|
+
const result = consolidate(hippoRoot, { dryRun });
|
|
206
|
+
console.log(`\nResults:`);
|
|
207
|
+
console.log(` Active memories: ${result.decayed}`);
|
|
208
|
+
console.log(` Removed (decayed): ${result.removed}`);
|
|
209
|
+
console.log(` Merged episodic: ${result.merged}`);
|
|
210
|
+
console.log(` New semantic: ${result.semanticCreated}`);
|
|
211
|
+
if (result.details.length > 0) {
|
|
212
|
+
console.log('\nDetails:');
|
|
213
|
+
for (const d of result.details) {
|
|
214
|
+
console.log(d);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (dryRun)
|
|
218
|
+
console.log('\n(dry run - nothing written)');
|
|
219
|
+
}
|
|
220
|
+
function cmdStatus(hippoRoot) {
|
|
221
|
+
requireInit(hippoRoot);
|
|
222
|
+
const entries = loadAllEntries(hippoRoot);
|
|
223
|
+
const stats = loadStats(hippoRoot);
|
|
224
|
+
const now = new Date();
|
|
225
|
+
const byLayer = {
|
|
226
|
+
[Layer.Buffer]: 0,
|
|
227
|
+
[Layer.Episodic]: 0,
|
|
228
|
+
[Layer.Semantic]: 0,
|
|
229
|
+
};
|
|
230
|
+
let totalStrength = 0;
|
|
231
|
+
let pinned = 0;
|
|
232
|
+
let atRisk = 0; // strength < 0.2
|
|
233
|
+
for (const e of entries) {
|
|
234
|
+
const s = calculateStrength(e, now);
|
|
235
|
+
byLayer[e.layer] = (byLayer[e.layer] ?? 0) + 1;
|
|
236
|
+
totalStrength += s;
|
|
237
|
+
if (e.pinned)
|
|
238
|
+
pinned++;
|
|
239
|
+
if (s < 0.2)
|
|
240
|
+
atRisk++;
|
|
241
|
+
}
|
|
242
|
+
const avgStrength = entries.length > 0 ? totalStrength / entries.length : 0;
|
|
243
|
+
console.log('Hippo Status');
|
|
244
|
+
console.log('---------------------------');
|
|
245
|
+
console.log(`Total memories: ${entries.length}`);
|
|
246
|
+
console.log(` Buffer: ${byLayer[Layer.Buffer]}`);
|
|
247
|
+
console.log(` Episodic: ${byLayer[Layer.Episodic]}`);
|
|
248
|
+
console.log(` Semantic: ${byLayer[Layer.Semantic]}`);
|
|
249
|
+
console.log(`Pinned: ${pinned}`);
|
|
250
|
+
console.log(`At risk (<0.2): ${atRisk}`);
|
|
251
|
+
console.log(`Avg strength: ${fmt(avgStrength)}`);
|
|
252
|
+
console.log('');
|
|
253
|
+
console.log(`Total remembered: ${stats['total_remembered'] ?? 0}`);
|
|
254
|
+
console.log(`Total recalled: ${stats['total_recalled'] ?? 0}`);
|
|
255
|
+
console.log(`Total forgotten: ${stats['total_forgotten'] ?? 0}`);
|
|
256
|
+
const runs = stats['consolidation_runs'] ?? [];
|
|
257
|
+
if (Array.isArray(runs) && runs.length > 0) {
|
|
258
|
+
const last = runs[runs.length - 1];
|
|
259
|
+
console.log(`Last sleep: ${last['timestamp']}`);
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
console.log(`Last sleep: never`);
|
|
263
|
+
}
|
|
264
|
+
// Embedding status
|
|
265
|
+
const embAvail = isEmbeddingAvailable();
|
|
266
|
+
console.log('');
|
|
267
|
+
console.log(`Embeddings: ${embAvail ? 'available' : 'not installed (BM25 only)'}`);
|
|
268
|
+
if (embAvail) {
|
|
269
|
+
const embIndex = loadEmbeddingIndex(hippoRoot);
|
|
270
|
+
const embCount = Object.keys(embIndex).length;
|
|
271
|
+
console.log(`Embedded: ${embCount}/${entries.length} memories`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
function cmdOutcome(hippoRoot, flags) {
|
|
275
|
+
requireInit(hippoRoot);
|
|
276
|
+
const good = Boolean(flags['good']);
|
|
277
|
+
const bad = Boolean(flags['bad']);
|
|
278
|
+
if (!good && !bad) {
|
|
279
|
+
console.error('Specify --good or --bad');
|
|
280
|
+
process.exit(1);
|
|
281
|
+
}
|
|
282
|
+
const specificId = flags['id'] ? String(flags['id']) : null;
|
|
283
|
+
const index = loadIndex(hippoRoot);
|
|
284
|
+
const ids = specificId ? [specificId] : index.last_retrieval_ids;
|
|
285
|
+
if (ids.length === 0) {
|
|
286
|
+
console.log('No recent recall to apply outcome to. Use --id <id> to target a specific memory.');
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
let updated = 0;
|
|
290
|
+
for (const id of ids) {
|
|
291
|
+
const entry = readEntry(hippoRoot, id);
|
|
292
|
+
if (!entry)
|
|
293
|
+
continue;
|
|
294
|
+
const upd = applyOutcome(entry, good);
|
|
295
|
+
writeEntry(hippoRoot, upd);
|
|
296
|
+
updated++;
|
|
297
|
+
}
|
|
298
|
+
console.log(`Applied ${good ? 'positive' : 'negative'} outcome to ${updated} memor${updated === 1 ? 'y' : 'ies'}`);
|
|
299
|
+
}
|
|
300
|
+
function cmdForget(hippoRoot, id) {
|
|
301
|
+
requireInit(hippoRoot);
|
|
302
|
+
const ok = deleteEntry(hippoRoot, id);
|
|
303
|
+
if (ok) {
|
|
304
|
+
updateStats(hippoRoot, { forgotten: 1 });
|
|
305
|
+
console.log(`Forgot ${id}`);
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
console.error(`Memory not found: ${id}`);
|
|
309
|
+
process.exit(1);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
function cmdInspect(hippoRoot, id) {
|
|
313
|
+
requireInit(hippoRoot);
|
|
314
|
+
const entry = readEntry(hippoRoot, id);
|
|
315
|
+
if (!entry) {
|
|
316
|
+
console.error(`Memory not found: ${id}`);
|
|
317
|
+
process.exit(1);
|
|
318
|
+
}
|
|
319
|
+
const now = new Date();
|
|
320
|
+
const currentStrength = calculateStrength(entry, now);
|
|
321
|
+
const lastRetrieved = new Date(entry.last_retrieved);
|
|
322
|
+
const created = new Date(entry.created);
|
|
323
|
+
const ageDays = (now.getTime() - created.getTime()) / (1000 * 60 * 60 * 24);
|
|
324
|
+
const daysSince = (now.getTime() - lastRetrieved.getTime()) / (1000 * 60 * 60 * 24);
|
|
325
|
+
console.log(`Memory: ${entry.id}`);
|
|
326
|
+
console.log('---------------------------');
|
|
327
|
+
console.log(`Layer: ${entry.layer}`);
|
|
328
|
+
console.log(`Created: ${entry.created} (${fmt(ageDays, 1)}d ago)`);
|
|
329
|
+
console.log(`Last retrieved: ${entry.last_retrieved} (${fmt(daysSince, 1)}d ago)`);
|
|
330
|
+
console.log(`Retrieval count: ${entry.retrieval_count}`);
|
|
331
|
+
console.log(`Strength (live): ${fmt(currentStrength)} (stored: ${fmt(entry.strength)})`);
|
|
332
|
+
console.log(`Half-life: ${entry.half_life_days}d`);
|
|
333
|
+
console.log(`Emotional: ${entry.emotional_valence}`);
|
|
334
|
+
console.log(`Schema fit: ${entry.schema_fit}`);
|
|
335
|
+
console.log(`Pinned: ${entry.pinned}`);
|
|
336
|
+
console.log(`Tags: ${entry.tags.join(', ') || 'none'}`);
|
|
337
|
+
console.log(`Outcome score: ${entry.outcome_score ?? 'none'}`);
|
|
338
|
+
if (entry.conflicts_with.length > 0) {
|
|
339
|
+
console.log(`Conflicts with: ${entry.conflicts_with.join(', ')}`);
|
|
340
|
+
}
|
|
341
|
+
console.log('');
|
|
342
|
+
console.log('Content:');
|
|
343
|
+
console.log('-'.repeat(40));
|
|
344
|
+
console.log(entry.content);
|
|
345
|
+
}
|
|
346
|
+
function cmdContext(hippoRoot, args, flags) {
|
|
347
|
+
requireInit(hippoRoot);
|
|
348
|
+
const budget = parseInt(String(flags['budget'] ?? '1500'), 10);
|
|
349
|
+
// If budget is 0, skip entirely (zero token cost)
|
|
350
|
+
if (budget <= 0)
|
|
351
|
+
return;
|
|
352
|
+
// Determine query: explicit args, --auto (git diff), or fallback
|
|
353
|
+
let query = args.join(' ').trim();
|
|
354
|
+
if (!query && flags['auto']) {
|
|
355
|
+
query = autoDetectContext();
|
|
356
|
+
}
|
|
357
|
+
if (!query) {
|
|
358
|
+
// Fallback: return strongest memories regardless of query
|
|
359
|
+
query = '*';
|
|
360
|
+
}
|
|
361
|
+
const globalRoot = getGlobalRoot();
|
|
362
|
+
const hasGlobal = isInitialized(globalRoot);
|
|
363
|
+
const localEntries = loadAllEntries(hippoRoot);
|
|
364
|
+
const globalEntries = hasGlobal ? loadAllEntries(globalRoot) : [];
|
|
365
|
+
const allEntries = [...localEntries];
|
|
366
|
+
if (allEntries.length === 0 && globalEntries.length === 0)
|
|
367
|
+
return; // no memories, zero output
|
|
368
|
+
let selectedItems = [];
|
|
369
|
+
let totalTokens = 0;
|
|
370
|
+
if (query === '*') {
|
|
371
|
+
// No query: return strongest memories by strength, up to budget
|
|
372
|
+
const now = new Date();
|
|
373
|
+
const localRanked = localEntries
|
|
374
|
+
.map((e) => ({
|
|
375
|
+
entry: e,
|
|
376
|
+
score: calculateStrength(e, now),
|
|
377
|
+
tokens: estimateTokens(e.content),
|
|
378
|
+
isGlobal: false,
|
|
379
|
+
}))
|
|
380
|
+
.sort((a, b) => b.score - a.score);
|
|
381
|
+
const globalRanked = globalEntries
|
|
382
|
+
.map((e) => ({
|
|
383
|
+
entry: e,
|
|
384
|
+
score: calculateStrength(e, now) * (1 / 1.2), // global slightly lower
|
|
385
|
+
tokens: estimateTokens(e.content),
|
|
386
|
+
isGlobal: true,
|
|
387
|
+
}))
|
|
388
|
+
.sort((a, b) => b.score - a.score);
|
|
389
|
+
const combined = [...localRanked, ...globalRanked].sort((a, b) => b.score - a.score);
|
|
390
|
+
let used = 0;
|
|
391
|
+
for (const r of combined) {
|
|
392
|
+
if (used + r.tokens > budget)
|
|
393
|
+
continue;
|
|
394
|
+
selectedItems.push(r);
|
|
395
|
+
used += r.tokens;
|
|
396
|
+
}
|
|
397
|
+
totalTokens = used;
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
let results;
|
|
401
|
+
if (hasGlobal) {
|
|
402
|
+
const merged = searchBoth(query, hippoRoot, globalRoot, { budget });
|
|
403
|
+
const localIndex = loadIndex(hippoRoot);
|
|
404
|
+
results = merged.map((r) => ({
|
|
405
|
+
entry: r.entry,
|
|
406
|
+
score: r.score,
|
|
407
|
+
tokens: r.tokens,
|
|
408
|
+
isGlobal: !localIndex.entries[r.entry.id],
|
|
409
|
+
}));
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
results = search(query, localEntries, { budget }).map((r) => ({
|
|
413
|
+
entry: r.entry,
|
|
414
|
+
score: r.score,
|
|
415
|
+
tokens: r.tokens,
|
|
416
|
+
isGlobal: false,
|
|
417
|
+
}));
|
|
418
|
+
}
|
|
419
|
+
selectedItems = results;
|
|
420
|
+
totalTokens = results.reduce((sum, r) => sum + r.tokens, 0);
|
|
421
|
+
}
|
|
422
|
+
if (selectedItems.length === 0)
|
|
423
|
+
return;
|
|
424
|
+
// Mark retrieved and persist
|
|
425
|
+
const toUpdate = selectedItems.map((s) => s.entry);
|
|
426
|
+
const updatedEntries = markRetrieved(toUpdate);
|
|
427
|
+
const localIndex = loadIndex(hippoRoot);
|
|
428
|
+
for (const u of updatedEntries) {
|
|
429
|
+
const targetRoot = localIndex.entries[u.id] ? hippoRoot : (hasGlobal ? globalRoot : hippoRoot);
|
|
430
|
+
writeEntry(targetRoot, u);
|
|
431
|
+
}
|
|
432
|
+
localIndex.last_retrieval_ids = updatedEntries.map((u) => u.id);
|
|
433
|
+
saveIndex(hippoRoot, localIndex);
|
|
434
|
+
updateStats(hippoRoot, { recalled: selectedItems.length });
|
|
435
|
+
const format = String(flags['format'] ?? 'markdown');
|
|
436
|
+
if (format === 'json') {
|
|
437
|
+
const output = selectedItems.map((r) => ({
|
|
438
|
+
id: r.entry.id,
|
|
439
|
+
score: r.score,
|
|
440
|
+
strength: r.entry.strength,
|
|
441
|
+
tags: r.entry.tags,
|
|
442
|
+
content: r.entry.content,
|
|
443
|
+
global: r.isGlobal ?? false,
|
|
444
|
+
}));
|
|
445
|
+
console.log(JSON.stringify({ query, memories: output, tokens: totalTokens }));
|
|
446
|
+
}
|
|
447
|
+
else {
|
|
448
|
+
printContextMarkdown(selectedItems.map((r) => ({
|
|
449
|
+
entry: updatedEntries.find((u) => u.id === r.entry.id) ?? r.entry,
|
|
450
|
+
score: r.score,
|
|
451
|
+
tokens: r.tokens,
|
|
452
|
+
isGlobal: r.isGlobal ?? false,
|
|
453
|
+
})), totalTokens);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
function printContextMarkdown(items, totalTokens) {
|
|
457
|
+
console.log(`## Project Memory (${items.length} entries, ${totalTokens} tokens)\n`);
|
|
458
|
+
for (const item of items) {
|
|
459
|
+
const e = item.entry;
|
|
460
|
+
const tagStr = e.tags.length > 0 ? ` [${e.tags.join(', ')}]` : '';
|
|
461
|
+
const strengthPct = Math.round(calculateStrength(e) * 100);
|
|
462
|
+
const globalPrefix = item.isGlobal ? '[global] ' : '';
|
|
463
|
+
console.log(`- **${globalPrefix}${e.content}**${tagStr} (${strengthPct}%)`);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
function autoDetectContext() {
|
|
467
|
+
// Try git diff --name-only for changed files
|
|
468
|
+
try {
|
|
469
|
+
const diff = execSync('git diff --name-only HEAD 2>&1', {
|
|
470
|
+
encoding: 'utf8',
|
|
471
|
+
timeout: 3000,
|
|
472
|
+
}).trim();
|
|
473
|
+
if (diff) {
|
|
474
|
+
// Extract meaningful terms from file paths
|
|
475
|
+
const terms = diff
|
|
476
|
+
.split('\n')
|
|
477
|
+
.flatMap((f) => f.replace(/[\/\\\.]/g, ' ').split(/\s+/))
|
|
478
|
+
.filter((t) => t.length > 2 && !['src', 'dist', 'test', 'tests', 'node_modules', 'index'].includes(t))
|
|
479
|
+
.slice(0, 10);
|
|
480
|
+
if (terms.length > 0)
|
|
481
|
+
return terms.join(' ');
|
|
482
|
+
}
|
|
483
|
+
// Try branch name
|
|
484
|
+
const branch = execSync('git branch --show-current 2>&1', {
|
|
485
|
+
encoding: 'utf8',
|
|
486
|
+
timeout: 3000,
|
|
487
|
+
}).trim();
|
|
488
|
+
if (branch && branch !== 'main' && branch !== 'master') {
|
|
489
|
+
return branch.replace(/[-_\/]/g, ' ');
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
catch {
|
|
493
|
+
// Not a git repo or git not available, fall through
|
|
494
|
+
}
|
|
495
|
+
return '';
|
|
496
|
+
}
|
|
497
|
+
// ---------------------------------------------------------------------------
|
|
498
|
+
// Embed command
|
|
499
|
+
// ---------------------------------------------------------------------------
|
|
500
|
+
async function cmdEmbed(hippoRoot, flags) {
|
|
501
|
+
requireInit(hippoRoot);
|
|
502
|
+
if (!isEmbeddingAvailable()) {
|
|
503
|
+
console.log('Embeddings not available. Install @xenova/transformers to enable:');
|
|
504
|
+
console.log(' npm install @xenova/transformers');
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
if (flags['status']) {
|
|
508
|
+
const entries = loadAllEntries(hippoRoot);
|
|
509
|
+
const embIndex = loadEmbeddingIndex(hippoRoot);
|
|
510
|
+
const embCount = Object.keys(embIndex).length;
|
|
511
|
+
console.log(`Embedding status: ${embCount}/${entries.length} memories embedded`);
|
|
512
|
+
const missing = entries.filter((e) => !embIndex[e.id]);
|
|
513
|
+
if (missing.length > 0) {
|
|
514
|
+
console.log(` ${missing.length} memories need embedding (run \`hippo embed\` to embed them)`);
|
|
515
|
+
}
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
console.log('Embedding all memories (this may take a moment on first run to download model)...');
|
|
519
|
+
const count = await embedAll(hippoRoot);
|
|
520
|
+
const entries = loadAllEntries(hippoRoot);
|
|
521
|
+
const embIndex = loadEmbeddingIndex(hippoRoot);
|
|
522
|
+
console.log(`Done. ${count} new embeddings created. ${Object.keys(embIndex).length}/${entries.length} total.`);
|
|
523
|
+
}
|
|
524
|
+
// ---------------------------------------------------------------------------
|
|
525
|
+
// Watch command
|
|
526
|
+
// ---------------------------------------------------------------------------
|
|
527
|
+
async function cmdWatch(command, hippoRoot) {
|
|
528
|
+
if (!command) {
|
|
529
|
+
console.error('Usage: hippo watch "<command>"');
|
|
530
|
+
process.exit(1);
|
|
531
|
+
}
|
|
532
|
+
const { exitCode, stderr } = await runWatched(command);
|
|
533
|
+
if (exitCode === 0) {
|
|
534
|
+
// Success: no noise
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
// Only create memory if hippo is initialized
|
|
538
|
+
if (!isInitialized(hippoRoot)) {
|
|
539
|
+
console.error('Command failed but .hippo not initialized. Run `hippo init` to enable auto-learn.');
|
|
540
|
+
process.exit(exitCode);
|
|
541
|
+
}
|
|
542
|
+
const entry = captureError(exitCode, stderr, command);
|
|
543
|
+
writeEntry(hippoRoot, entry);
|
|
544
|
+
updateStats(hippoRoot, { remembered: 1 });
|
|
545
|
+
if (isEmbeddingAvailable()) {
|
|
546
|
+
embedMemory(hippoRoot, entry).catch(() => { });
|
|
547
|
+
}
|
|
548
|
+
const preview = stderr.trim().slice(0, 80);
|
|
549
|
+
console.error(`\nHippo learned from failure: "${preview}"`);
|
|
550
|
+
process.exit(exitCode);
|
|
551
|
+
}
|
|
552
|
+
// ---------------------------------------------------------------------------
|
|
553
|
+
// Learn command
|
|
554
|
+
// ---------------------------------------------------------------------------
|
|
555
|
+
function cmdLearn(hippoRoot, flags) {
|
|
556
|
+
requireInit(hippoRoot);
|
|
557
|
+
if (!flags['git']) {
|
|
558
|
+
console.error('Usage: hippo learn --git [--days <n>]');
|
|
559
|
+
process.exit(1);
|
|
560
|
+
}
|
|
561
|
+
const days = parseInt(String(flags['days'] ?? '7'), 10);
|
|
562
|
+
const cwd = process.cwd();
|
|
563
|
+
console.log(`Scanning git log for the last ${days} days...`);
|
|
564
|
+
const gitLog = fetchGitLog(cwd, days);
|
|
565
|
+
if (!gitLog.trim()) {
|
|
566
|
+
console.log('No git history found (or not a git repository).');
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
const lessons = extractLessons(gitLog);
|
|
570
|
+
if (lessons.length === 0) {
|
|
571
|
+
console.log('No fix/revert/bug commits found in the specified period.');
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
let added = 0;
|
|
575
|
+
let skipped = 0;
|
|
576
|
+
for (const lesson of lessons) {
|
|
577
|
+
if (deduplicateLesson(hippoRoot, lesson)) {
|
|
578
|
+
skipped++;
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
const entry = createMemory(lesson, {
|
|
582
|
+
layer: Layer.Episodic,
|
|
583
|
+
tags: ['error', 'git-learned'],
|
|
584
|
+
source: 'git-learn',
|
|
585
|
+
});
|
|
586
|
+
writeEntry(hippoRoot, entry);
|
|
587
|
+
updateStats(hippoRoot, { remembered: 1 });
|
|
588
|
+
if (isEmbeddingAvailable()) {
|
|
589
|
+
embedMemory(hippoRoot, entry).catch(() => { });
|
|
590
|
+
}
|
|
591
|
+
added++;
|
|
592
|
+
}
|
|
593
|
+
console.log(`Git learn complete: ${added} new lessons added, ${skipped} duplicates skipped.`);
|
|
594
|
+
}
|
|
595
|
+
// ---------------------------------------------------------------------------
|
|
596
|
+
// Promote command
|
|
597
|
+
// ---------------------------------------------------------------------------
|
|
598
|
+
function cmdPromote(hippoRoot, id) {
|
|
599
|
+
requireInit(hippoRoot);
|
|
600
|
+
if (!id) {
|
|
601
|
+
console.error('Usage: hippo promote <id>');
|
|
602
|
+
process.exit(1);
|
|
603
|
+
}
|
|
604
|
+
try {
|
|
605
|
+
const globalEntry = promoteToGlobal(hippoRoot, id);
|
|
606
|
+
console.log(`Promoted ${id} to global store as ${globalEntry.id}`);
|
|
607
|
+
console.log(` Global store: ${getGlobalRoot()}`);
|
|
608
|
+
}
|
|
609
|
+
catch (err) {
|
|
610
|
+
console.error(`Failed to promote: ${err.message}`);
|
|
611
|
+
process.exit(1);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
// ---------------------------------------------------------------------------
|
|
615
|
+
// Sync command
|
|
616
|
+
// ---------------------------------------------------------------------------
|
|
617
|
+
function cmdSync(hippoRoot) {
|
|
618
|
+
requireInit(hippoRoot);
|
|
619
|
+
const globalRoot = getGlobalRoot();
|
|
620
|
+
if (!isInitialized(globalRoot)) {
|
|
621
|
+
console.log('No global store found. Run `hippo init --global` first.');
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
const count = syncGlobalToLocal(hippoRoot, globalRoot);
|
|
625
|
+
console.log(`Synced ${count} global memories into local project.`);
|
|
626
|
+
}
|
|
627
|
+
// ---------------------------------------------------------------------------
|
|
628
|
+
// Hook install/uninstall
|
|
629
|
+
// ---------------------------------------------------------------------------
|
|
630
|
+
const HOOK_MARKERS = {
|
|
631
|
+
start: '<!-- hippo:start -->',
|
|
632
|
+
end: '<!-- hippo:end -->',
|
|
633
|
+
};
|
|
634
|
+
const HOOKS = {
|
|
635
|
+
'claude-code': {
|
|
636
|
+
file: 'CLAUDE.md',
|
|
637
|
+
description: 'Claude Code',
|
|
638
|
+
content: `
|
|
639
|
+
## Project Memory (Hippo)
|
|
640
|
+
|
|
641
|
+
Before starting work, load relevant context:
|
|
642
|
+
\`\`\`bash
|
|
643
|
+
hippo context --auto --budget 1500
|
|
644
|
+
\`\`\`
|
|
645
|
+
|
|
646
|
+
When you learn something important:
|
|
647
|
+
\`\`\`bash
|
|
648
|
+
hippo remember "<lesson>"
|
|
649
|
+
\`\`\`
|
|
650
|
+
|
|
651
|
+
When you hit an error or discover a gotcha:
|
|
652
|
+
\`\`\`bash
|
|
653
|
+
hippo remember "<what went wrong and why>" --error
|
|
654
|
+
\`\`\`
|
|
655
|
+
|
|
656
|
+
After completing work successfully:
|
|
657
|
+
\`\`\`bash
|
|
658
|
+
hippo outcome --good
|
|
659
|
+
\`\`\`
|
|
660
|
+
`.trim(),
|
|
661
|
+
},
|
|
662
|
+
'codex': {
|
|
663
|
+
file: 'AGENTS.md',
|
|
664
|
+
description: 'OpenAI Codex',
|
|
665
|
+
content: `
|
|
666
|
+
## Project Memory (Hippo)
|
|
667
|
+
|
|
668
|
+
At the start of every task, run:
|
|
669
|
+
\`\`\`bash
|
|
670
|
+
hippo context --auto --budget 1500
|
|
671
|
+
\`\`\`
|
|
672
|
+
Read the output before writing any code.
|
|
673
|
+
|
|
674
|
+
On errors or unexpected behaviour:
|
|
675
|
+
\`\`\`bash
|
|
676
|
+
hippo remember "<description of what went wrong>" --error
|
|
677
|
+
\`\`\`
|
|
678
|
+
|
|
679
|
+
On task completion:
|
|
680
|
+
\`\`\`bash
|
|
681
|
+
hippo outcome --good
|
|
682
|
+
\`\`\`
|
|
683
|
+
`.trim(),
|
|
684
|
+
},
|
|
685
|
+
'cursor': {
|
|
686
|
+
file: '.cursorrules',
|
|
687
|
+
description: 'Cursor',
|
|
688
|
+
content: `
|
|
689
|
+
# Project Memory (Hippo)
|
|
690
|
+
# Before each task, load context:
|
|
691
|
+
# hippo context --auto --budget 1500
|
|
692
|
+
# After errors:
|
|
693
|
+
# hippo remember "<error description>" --error
|
|
694
|
+
# After completing:
|
|
695
|
+
# hippo outcome --good
|
|
696
|
+
`.trim(),
|
|
697
|
+
},
|
|
698
|
+
'openclaw': {
|
|
699
|
+
file: '.openclaw/skills/hippo/SKILL.md',
|
|
700
|
+
description: 'OpenClaw',
|
|
701
|
+
content: `
|
|
702
|
+
# Hippo Memory Skill
|
|
703
|
+
|
|
704
|
+
On session start, inject project memory:
|
|
705
|
+
\`\`\`bash
|
|
706
|
+
hippo context --auto --budget 1500
|
|
707
|
+
\`\`\`
|
|
708
|
+
|
|
709
|
+
When an error occurs:
|
|
710
|
+
\`\`\`bash
|
|
711
|
+
hippo remember "<error>" --error
|
|
712
|
+
\`\`\`
|
|
713
|
+
|
|
714
|
+
On session end:
|
|
715
|
+
\`\`\`bash
|
|
716
|
+
hippo outcome --good
|
|
717
|
+
\`\`\`
|
|
718
|
+
`.trim(),
|
|
719
|
+
},
|
|
720
|
+
};
|
|
721
|
+
function cmdHook(args, flags) {
|
|
722
|
+
const subcommand = args[0];
|
|
723
|
+
const target = args[1];
|
|
724
|
+
if (subcommand === 'list') {
|
|
725
|
+
console.log('Available hooks:\n');
|
|
726
|
+
for (const [name, hook] of Object.entries(HOOKS)) {
|
|
727
|
+
console.log(` ${name.padEnd(15)} -> ${hook.file} (${hook.description})`);
|
|
728
|
+
}
|
|
729
|
+
console.log('\nUsage: hippo hook install <name>');
|
|
730
|
+
console.log(' hippo hook uninstall <name>');
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
if (subcommand === 'install') {
|
|
734
|
+
if (!target || !HOOKS[target]) {
|
|
735
|
+
console.error(`Unknown hook target: ${target ?? '(none)'}`);
|
|
736
|
+
console.error(` Available: ${Object.keys(HOOKS).join(', ')}`);
|
|
737
|
+
process.exit(1);
|
|
738
|
+
}
|
|
739
|
+
const hook = HOOKS[target];
|
|
740
|
+
const filepath = path.resolve(process.cwd(), hook.file);
|
|
741
|
+
const dir = path.dirname(filepath);
|
|
742
|
+
if (!fs.existsSync(dir))
|
|
743
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
744
|
+
const block = `${HOOK_MARKERS.start}\n${hook.content}\n${HOOK_MARKERS.end}`;
|
|
745
|
+
if (fs.existsSync(filepath)) {
|
|
746
|
+
const existing = fs.readFileSync(filepath, 'utf8');
|
|
747
|
+
// Check if already installed
|
|
748
|
+
if (existing.includes(HOOK_MARKERS.start)) {
|
|
749
|
+
// Replace existing block
|
|
750
|
+
const re = new RegExp(`${escapeRegex(HOOK_MARKERS.start)}[\\s\\S]*?${escapeRegex(HOOK_MARKERS.end)}`, 'g');
|
|
751
|
+
const updated = existing.replace(re, block);
|
|
752
|
+
fs.writeFileSync(filepath, updated, 'utf8');
|
|
753
|
+
console.log(`Updated Hippo hook in ${hook.file}`);
|
|
754
|
+
}
|
|
755
|
+
else {
|
|
756
|
+
// Append
|
|
757
|
+
const sep = existing.endsWith('\n') ? '\n' : '\n\n';
|
|
758
|
+
fs.writeFileSync(filepath, existing + sep + block + '\n', 'utf8');
|
|
759
|
+
console.log(`Installed Hippo hook in ${hook.file} (appended)`);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
else {
|
|
763
|
+
// Create new file
|
|
764
|
+
fs.writeFileSync(filepath, block + '\n', 'utf8');
|
|
765
|
+
console.log(`Created ${hook.file} with Hippo hook`);
|
|
766
|
+
}
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
if (subcommand === 'uninstall') {
|
|
770
|
+
if (!target || !HOOKS[target]) {
|
|
771
|
+
console.error(`Unknown hook target: ${target ?? '(none)'}`);
|
|
772
|
+
process.exit(1);
|
|
773
|
+
}
|
|
774
|
+
const hook = HOOKS[target];
|
|
775
|
+
const filepath = path.resolve(process.cwd(), hook.file);
|
|
776
|
+
if (!fs.existsSync(filepath)) {
|
|
777
|
+
console.log(`${hook.file} not found, nothing to uninstall.`);
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
const existing = fs.readFileSync(filepath, 'utf8');
|
|
781
|
+
if (!existing.includes(HOOK_MARKERS.start)) {
|
|
782
|
+
console.log(`No Hippo hook found in ${hook.file}.`);
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
const re = new RegExp(`\\n?${escapeRegex(HOOK_MARKERS.start)}[\\s\\S]*?${escapeRegex(HOOK_MARKERS.end)}\\n?`, 'g');
|
|
786
|
+
const cleaned = existing.replace(re, '\n').replace(/\n{3,}/g, '\n\n').trim();
|
|
787
|
+
fs.writeFileSync(filepath, cleaned + '\n', 'utf8');
|
|
788
|
+
console.log(`Removed Hippo hook from ${hook.file}`);
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
console.error('Usage: hippo hook <install|uninstall|list> [target]');
|
|
792
|
+
process.exit(1);
|
|
793
|
+
}
|
|
794
|
+
function escapeRegex(s) {
|
|
795
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
796
|
+
}
|
|
797
|
+
function printUsage() {
|
|
798
|
+
console.log(`
|
|
799
|
+
Hippo - biologically-inspired memory system for AI agents
|
|
800
|
+
|
|
801
|
+
Usage: hippo <command> [options]
|
|
802
|
+
|
|
803
|
+
Commands:
|
|
804
|
+
init Create .hippo/ structure in current directory
|
|
805
|
+
--global Init the global store at ~/.hippo/
|
|
806
|
+
remember <text> Store a memory
|
|
807
|
+
--tag <tag> Add a tag (repeatable)
|
|
808
|
+
--error Tag as error (boosts retention)
|
|
809
|
+
--pin Pin memory (never decays)
|
|
810
|
+
--global Store in global ~/.hippo/ store
|
|
811
|
+
recall <query> Search and retrieve memories (local + global)
|
|
812
|
+
--budget <n> Token budget (default: 4000)
|
|
813
|
+
--json Output as JSON
|
|
814
|
+
context Smart context injection for AI agents
|
|
815
|
+
--auto Auto-detect task from git state
|
|
816
|
+
--budget <n> Token budget (default: 1500)
|
|
817
|
+
--format <fmt> Output format: markdown (default) or json
|
|
818
|
+
sleep Run consolidation pass
|
|
819
|
+
--dry-run Preview without writing
|
|
820
|
+
status Show memory health stats
|
|
821
|
+
outcome Apply feedback to last recall
|
|
822
|
+
--good Memories were helpful
|
|
823
|
+
--bad Memories were irrelevant
|
|
824
|
+
--id <id> Target a specific memory
|
|
825
|
+
forget <id> Force remove a memory
|
|
826
|
+
inspect <id> Show full memory detail
|
|
827
|
+
embed Embed all memories for semantic search
|
|
828
|
+
--status Show embedding coverage
|
|
829
|
+
watch "<command>" Run command, auto-learn from failures
|
|
830
|
+
learn Learn lessons from repository history
|
|
831
|
+
--git Scan recent git commits for lessons
|
|
832
|
+
--days <n> Scan this many days back (default: 7)
|
|
833
|
+
promote <id> Copy a local memory to the global store
|
|
834
|
+
sync Pull global memories into local project
|
|
835
|
+
hook <sub> [target] Manage framework integrations
|
|
836
|
+
hook list Show available hooks
|
|
837
|
+
hook install <target> Install hook (claude-code|codex|cursor|openclaw)
|
|
838
|
+
hook uninstall <target> Remove hook
|
|
839
|
+
|
|
840
|
+
Examples:
|
|
841
|
+
hippo init
|
|
842
|
+
hippo remember "FRED cache can silently drop series" --tag error
|
|
843
|
+
hippo recall "data pipeline issues" --budget 2000
|
|
844
|
+
hippo context --auto --budget 1500
|
|
845
|
+
hippo embed --status
|
|
846
|
+
hippo watch "npm run build"
|
|
847
|
+
hippo learn --git --days 30
|
|
848
|
+
hippo promote mem_abc123
|
|
849
|
+
hippo sync
|
|
850
|
+
hippo hook install claude-code
|
|
851
|
+
hippo sleep --dry-run
|
|
852
|
+
hippo outcome --good
|
|
853
|
+
hippo status
|
|
854
|
+
`);
|
|
855
|
+
}
|
|
856
|
+
// ---------------------------------------------------------------------------
|
|
857
|
+
// Entry point
|
|
858
|
+
// ---------------------------------------------------------------------------
|
|
859
|
+
const { command, args, flags } = parseArgs(process.argv);
|
|
860
|
+
const hippoRoot = getHippoRoot(process.cwd());
|
|
861
|
+
async function main() {
|
|
862
|
+
switch (command) {
|
|
863
|
+
case 'init':
|
|
864
|
+
cmdInit(hippoRoot, flags);
|
|
865
|
+
break;
|
|
866
|
+
case 'remember': {
|
|
867
|
+
const text = args.join(' ').trim();
|
|
868
|
+
if (!text) {
|
|
869
|
+
console.error('Please provide text to remember.');
|
|
870
|
+
process.exit(1);
|
|
871
|
+
}
|
|
872
|
+
cmdRemember(hippoRoot, text, flags);
|
|
873
|
+
break;
|
|
874
|
+
}
|
|
875
|
+
case 'recall': {
|
|
876
|
+
const query = args.join(' ').trim();
|
|
877
|
+
if (!query) {
|
|
878
|
+
console.error('Please provide a search query.');
|
|
879
|
+
process.exit(1);
|
|
880
|
+
}
|
|
881
|
+
cmdRecall(hippoRoot, query, flags);
|
|
882
|
+
break;
|
|
883
|
+
}
|
|
884
|
+
case 'sleep':
|
|
885
|
+
cmdSleep(hippoRoot, flags);
|
|
886
|
+
break;
|
|
887
|
+
case 'status':
|
|
888
|
+
cmdStatus(hippoRoot);
|
|
889
|
+
break;
|
|
890
|
+
case 'outcome':
|
|
891
|
+
cmdOutcome(hippoRoot, flags);
|
|
892
|
+
break;
|
|
893
|
+
case 'forget': {
|
|
894
|
+
const id = args[0];
|
|
895
|
+
if (!id) {
|
|
896
|
+
console.error('Please provide a memory ID.');
|
|
897
|
+
process.exit(1);
|
|
898
|
+
}
|
|
899
|
+
cmdForget(hippoRoot, id);
|
|
900
|
+
break;
|
|
901
|
+
}
|
|
902
|
+
case 'inspect': {
|
|
903
|
+
const id = args[0];
|
|
904
|
+
if (!id) {
|
|
905
|
+
console.error('Please provide a memory ID.');
|
|
906
|
+
process.exit(1);
|
|
907
|
+
}
|
|
908
|
+
cmdInspect(hippoRoot, id);
|
|
909
|
+
break;
|
|
910
|
+
}
|
|
911
|
+
case 'context':
|
|
912
|
+
cmdContext(hippoRoot, args, flags);
|
|
913
|
+
break;
|
|
914
|
+
case 'hook':
|
|
915
|
+
cmdHook(args, flags);
|
|
916
|
+
break;
|
|
917
|
+
case 'embed':
|
|
918
|
+
await cmdEmbed(hippoRoot, flags);
|
|
919
|
+
break;
|
|
920
|
+
case 'watch': {
|
|
921
|
+
const watchCmd = args.join(' ').trim();
|
|
922
|
+
await cmdWatch(watchCmd, hippoRoot);
|
|
923
|
+
break;
|
|
924
|
+
}
|
|
925
|
+
case 'learn':
|
|
926
|
+
cmdLearn(hippoRoot, flags);
|
|
927
|
+
break;
|
|
928
|
+
case 'promote': {
|
|
929
|
+
const id = args[0];
|
|
930
|
+
if (!id) {
|
|
931
|
+
console.error('Please provide a memory ID.');
|
|
932
|
+
process.exit(1);
|
|
933
|
+
}
|
|
934
|
+
cmdPromote(hippoRoot, id);
|
|
935
|
+
break;
|
|
936
|
+
}
|
|
937
|
+
case 'sync':
|
|
938
|
+
cmdSync(hippoRoot);
|
|
939
|
+
break;
|
|
940
|
+
case 'help':
|
|
941
|
+
case '--help':
|
|
942
|
+
case '-h':
|
|
943
|
+
case '':
|
|
944
|
+
printUsage();
|
|
945
|
+
break;
|
|
946
|
+
default:
|
|
947
|
+
console.error(`Unknown command: ${command}`);
|
|
948
|
+
printUsage();
|
|
949
|
+
process.exit(1);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
main().catch((err) => {
|
|
953
|
+
console.error('Error:', err.message ?? err);
|
|
954
|
+
process.exit(1);
|
|
955
|
+
});
|
|
956
|
+
//# sourceMappingURL=cli.js.map
|