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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +305 -0
  3. package/dist/autolearn.d.ts +34 -0
  4. package/dist/autolearn.d.ts.map +1 -0
  5. package/dist/autolearn.js +119 -0
  6. package/dist/autolearn.js.map +1 -0
  7. package/dist/cli.d.ts +21 -0
  8. package/dist/cli.d.ts.map +1 -0
  9. package/dist/cli.js +956 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/config.d.ts +16 -0
  12. package/dist/config.d.ts.map +1 -0
  13. package/dist/config.js +35 -0
  14. package/dist/config.js.map +1 -0
  15. package/dist/consolidate.d.ts +24 -0
  16. package/dist/consolidate.d.ts.map +1 -0
  17. package/dist/consolidate.js +133 -0
  18. package/dist/consolidate.js.map +1 -0
  19. package/dist/embeddings.d.ts +39 -0
  20. package/dist/embeddings.d.ts.map +1 -0
  21. package/dist/embeddings.js +184 -0
  22. package/dist/embeddings.js.map +1 -0
  23. package/dist/index.d.ts +11 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +14 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/memory.d.ts +59 -0
  28. package/dist/memory.d.ts.map +1 -0
  29. package/dist/memory.js +118 -0
  30. package/dist/memory.js.map +1 -0
  31. package/dist/search.d.ts +51 -0
  32. package/dist/search.d.ts.map +1 -0
  33. package/dist/search.js +239 -0
  34. package/dist/search.js.map +1 -0
  35. package/dist/shared.d.ts +38 -0
  36. package/dist/shared.d.ts.map +1 -0
  37. package/dist/shared.js +111 -0
  38. package/dist/shared.js.map +1 -0
  39. package/dist/store.d.ts +70 -0
  40. package/dist/store.d.ts.map +1 -0
  41. package/dist/store.js +244 -0
  42. package/dist/store.js.map +1 -0
  43. package/dist/yaml.d.ts +14 -0
  44. package/dist/yaml.d.ts.map +1 -0
  45. package/dist/yaml.js +81 -0
  46. package/dist/yaml.js.map +1 -0
  47. 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