hippo-memory 0.9.0 → 0.10.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 +64 -5
- package/dist/cli.d.ts +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +273 -3
- package/dist/cli.js.map +1 -1
- package/dist/invalidation.d.ts +23 -0
- package/dist/invalidation.d.ts.map +1 -0
- package/dist/invalidation.js +94 -0
- package/dist/invalidation.js.map +1 -0
- package/dist/memory.d.ts +1 -0
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +1 -0
- package/dist/memory.js.map +1 -1
- package/dist/path-context.d.ts +12 -0
- package/dist/path-context.d.ts.map +1 -0
- package/dist/path-context.js +32 -0
- package/dist/path-context.js.map +1 -0
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +20 -1
- package/dist/search.js.map +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/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
[](./LICENSE)
|
|
7
7
|
|
|
8
8
|
```
|
|
9
|
-
Works with: Claude Code, Codex, Cursor, OpenClaw, any CLI agent
|
|
9
|
+
Works with: Claude Code, Codex, Cursor, OpenClaw, OpenCode, any CLI agent
|
|
10
10
|
Imports from: ChatGPT, Claude (CLAUDE.md), Cursor (.cursorrules), any markdown
|
|
11
11
|
Storage: SQLite backbone + markdown/YAML mirrors. Git-trackable and human-readable.
|
|
12
12
|
Dependencies: Zero runtime deps. Requires Node.js 22.5+. Optional embeddings via @xenova/transformers.
|
|
@@ -43,6 +43,16 @@ hippo recall "data pipeline issues" --budget 2000
|
|
|
43
43
|
|
|
44
44
|
That's it. You have a memory system.
|
|
45
45
|
|
|
46
|
+
### What's new in v0.10.0
|
|
47
|
+
|
|
48
|
+
- **Active invalidation.** `hippo learn --git` detects migration and breaking-change commits and actively weakens memories referencing the old pattern. Manual invalidation via `hippo invalidate "REST API" --reason "migrated to GraphQL"`.
|
|
49
|
+
- **Architectural decisions.** `hippo decide` stores one-off decisions with 90-day half-life and verified confidence. Supports `--context` for reasoning and `--supersedes` to chain decisions when the architecture evolves.
|
|
50
|
+
- **Decision recall boost.** 1.2x scoring multiplier for decision-tagged memories so they surface despite low retrieval frequency.
|
|
51
|
+
|
|
52
|
+
### What's new in v0.9.1
|
|
53
|
+
|
|
54
|
+
- **Auto-sleep on session exit.** `hippo hook install claude-code` now installs a Stop hook in `~/.claude/settings.json` so `hippo sleep` runs automatically when Claude Code exits. `hippo init` does this too when Claude Code is detected. No cron needed, no manual sleep.
|
|
55
|
+
|
|
46
56
|
### What's new in v0.9.0
|
|
47
57
|
|
|
48
58
|
- **Working memory layer** (`hippo wm push/read/clear/flush`). Bounded buffer (max 20 per scope) with importance-based eviction. Current-state notes live separately from long-term memory.
|
|
@@ -76,7 +86,7 @@ hippo init
|
|
|
76
86
|
# Auto-installed claude-code hook in CLAUDE.md
|
|
77
87
|
```
|
|
78
88
|
|
|
79
|
-
If you have a `CLAUDE.md`, it patches it. `AGENTS.md` for Codex/OpenClaw. `.cursorrules` for Cursor. No manual `hook install` needed. Your agent starts using Hippo on its next session.
|
|
89
|
+
If you have a `CLAUDE.md`, it patches it. `AGENTS.md` for Codex/OpenClaw/OpenCode. `.cursorrules` for Cursor. No manual `hook install` needed. Your agent starts using Hippo on its next session.
|
|
80
90
|
|
|
81
91
|
It also sets up a daily cron job (6:15am) that runs `hippo learn --git` and `hippo sleep` automatically. Memories get captured from your commits and consolidated every day without you thinking about it.
|
|
82
92
|
|
|
@@ -274,6 +284,44 @@ hippo recall "cache issues" # again next week
|
|
|
274
284
|
|
|
275
285
|
---
|
|
276
286
|
|
|
287
|
+
### Active invalidation
|
|
288
|
+
|
|
289
|
+
When you migrate from one tool to another, old memories about the replaced tool should die immediately. Hippo detects migration and breaking-change commits during `hippo learn --git` and actively weakens matching memories.
|
|
290
|
+
|
|
291
|
+
```bash
|
|
292
|
+
hippo learn --git
|
|
293
|
+
# feat: migrate from webpack to vite
|
|
294
|
+
# Invalidated 3 memories referencing "webpack"
|
|
295
|
+
# Learned: migrate from webpack to vite
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
You can also invalidate manually:
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
hippo invalidate "REST API" --reason "migrated to GraphQL"
|
|
302
|
+
# Invalidated 5 memories referencing "REST API".
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
### Architectural decisions
|
|
308
|
+
|
|
309
|
+
One-off decisions don't repeat, so they can't earn their keep through retrieval alone. `hippo decide` stores them with a 90-day half-life and verified confidence so they survive long enough to matter.
|
|
310
|
+
|
|
311
|
+
```bash
|
|
312
|
+
hippo decide "Use PostgreSQL for all new services" --context "JSONB support"
|
|
313
|
+
# Decision recorded: mem_a1b2c3
|
|
314
|
+
|
|
315
|
+
# Later, when the decision changes:
|
|
316
|
+
hippo decide "Use CockroachDB for global services" \
|
|
317
|
+
--context "Need multi-region" \
|
|
318
|
+
--supersedes mem_a1b2c3
|
|
319
|
+
# Superseded mem_a1b2c3 (half-life halved, marked stale)
|
|
320
|
+
# Decision recorded: mem_d4e5f6
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
277
325
|
### Error memories stick
|
|
278
326
|
|
|
279
327
|
Tag a memory as an error and it gets 2x the half-life automatically.
|
|
@@ -502,8 +550,13 @@ hippo watch "npm run build"
|
|
|
502
550
|
| `hippo share --auto --dry-run` | Preview what would be shared |
|
|
503
551
|
| `hippo peers` | List projects contributing to global store |
|
|
504
552
|
| `hippo sync` | Pull global memories into local project |
|
|
553
|
+
| `hippo invalidate "<pattern>"` | Actively weaken memories matching an old pattern |
|
|
554
|
+
| `hippo invalidate "<pattern>" --reason "<why>"` | Include what replaced it |
|
|
555
|
+
| `hippo decide "<decision>"` | Record architectural decision (90-day half-life) |
|
|
556
|
+
| `hippo decide "<decision>" --context "<why>"` | Include reasoning |
|
|
557
|
+
| `hippo decide "<decision>" --supersedes <id>` | Supersede a previous decision |
|
|
505
558
|
| `hippo hook list` | Show available framework hooks |
|
|
506
|
-
| `hippo hook install <target>` | Install hook (claude-code
|
|
559
|
+
| `hippo hook install <target>` | Install hook (claude-code also adds Stop hook for auto-sleep) |
|
|
507
560
|
| `hippo hook uninstall <target>` | Remove hook |
|
|
508
561
|
| `hippo handoff create --summary "..."` | Create a session handoff |
|
|
509
562
|
| `hippo handoff latest` | Show the most recent handoff |
|
|
@@ -529,10 +582,11 @@ hippo watch "npm run build"
|
|
|
529
582
|
|
|
530
583
|
| Framework | Detected by | Patches |
|
|
531
584
|
|-----------|------------|---------|
|
|
532
|
-
| Claude Code | `CLAUDE.md` or `.claude/settings.json` | `CLAUDE.md` |
|
|
585
|
+
| Claude Code | `CLAUDE.md` or `.claude/settings.json` | `CLAUDE.md` + Stop hook in `settings.json` |
|
|
533
586
|
| Codex | `AGENTS.md` or `.codex` | `AGENTS.md` |
|
|
534
587
|
| Cursor | `.cursorrules` or `.cursor/rules` | `.cursorrules` |
|
|
535
588
|
| OpenClaw | `.openclaw` or `AGENTS.md` | `AGENTS.md` |
|
|
589
|
+
| OpenCode | `.opencode/` or `opencode.json` | `AGENTS.md` |
|
|
536
590
|
|
|
537
591
|
No extra commands needed. Just `hippo init` and your agent knows about Hippo.
|
|
538
592
|
|
|
@@ -541,10 +595,11 @@ No extra commands needed. Just `hippo init` and your agent knows about Hippo.
|
|
|
541
595
|
If you prefer explicit control:
|
|
542
596
|
|
|
543
597
|
```bash
|
|
544
|
-
hippo hook install claude-code # patches CLAUDE.md
|
|
598
|
+
hippo hook install claude-code # patches CLAUDE.md + adds Stop hook to settings.json
|
|
545
599
|
hippo hook install codex # patches AGENTS.md
|
|
546
600
|
hippo hook install cursor # patches .cursorrules
|
|
547
601
|
hippo hook install openclaw # patches AGENTS.md
|
|
602
|
+
hippo hook install opencode # patches AGENTS.md
|
|
548
603
|
```
|
|
549
604
|
|
|
550
605
|
This adds a `<!-- hippo:start -->` ... `<!-- hippo:end -->` block that tells the agent to:
|
|
@@ -552,6 +607,8 @@ This adds a `<!-- hippo:start -->` ... `<!-- hippo:end -->` block that tells the
|
|
|
552
607
|
2. Run `hippo remember "<lesson>" --error` on errors
|
|
553
608
|
3. Run `hippo outcome --good` on completion
|
|
554
609
|
|
|
610
|
+
For Claude Code, it also adds a Stop hook to `~/.claude/settings.json` so `hippo sleep` runs automatically when the session exits.
|
|
611
|
+
|
|
555
612
|
To remove: `hippo hook uninstall claude-code`
|
|
556
613
|
|
|
557
614
|
### What the hook adds (Claude Code example)
|
|
@@ -630,6 +687,8 @@ The 7 mechanisms in full: [PLAN.md#core-principles](PLAN.md#core-principles)
|
|
|
630
687
|
|
|
631
688
|
For how these mechanisms connect to LLM training, continual learning, and open research problems: **[RESEARCH.md](RESEARCH.md)**
|
|
632
689
|
|
|
690
|
+
**Related work:** [HippoRAG](https://arxiv.org/abs/2405.14831) (Gutierrez et al., 2024) applies hippocampal indexing to RAG via knowledge graphs. Complementary approach — HippoRAG optimizes retrieval quality, Hippo optimizes memory lifecycle. Same brain region, different mechanisms.
|
|
691
|
+
|
|
633
692
|
---
|
|
634
693
|
|
|
635
694
|
## Comparison
|
package/dist/cli.d.ts
CHANGED
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG"}
|
package/dist/cli.js
CHANGED
|
@@ -21,17 +21,21 @@
|
|
|
21
21
|
* hippo learn --git [--days <n>] [--repos <paths>]
|
|
22
22
|
* hippo promote <id>
|
|
23
23
|
* hippo sync
|
|
24
|
+
* hippo decide "<decision>" [--context "<why>"] [--supersedes <id>]
|
|
24
25
|
* hippo wm <push|read|clear|flush>
|
|
25
26
|
*/
|
|
26
27
|
import * as path from 'path';
|
|
27
28
|
import * as fs from 'fs';
|
|
29
|
+
import * as os from 'os';
|
|
28
30
|
import { execSync } from 'child_process';
|
|
29
|
-
import { createMemory, calculateStrength, deriveHalfLife, resolveConfidence, applyOutcome, computeSchemaFit, Layer, } from './memory.js';
|
|
31
|
+
import { createMemory, calculateStrength, deriveHalfLife, resolveConfidence, applyOutcome, computeSchemaFit, Layer, DECISION_HALF_LIFE_DAYS, } from './memory.js';
|
|
30
32
|
import { getHippoRoot, isInitialized, initStore, writeEntry, readEntry, deleteEntry, loadAllEntries, loadSearchEntries, loadIndex, saveIndex, loadStats, updateStats, saveActiveTaskSnapshot, loadActiveTaskSnapshot, clearActiveTaskSnapshot, appendSessionEvent, listSessionEvents, listMemoryConflicts, resolveConflict, saveSessionHandoff, loadLatestHandoff, loadHandoffById, } from './store.js';
|
|
31
33
|
import { markRetrieved, estimateTokens, hybridSearch, explainMatch } from './search.js';
|
|
32
34
|
import { consolidate } from './consolidate.js';
|
|
33
35
|
import { isEmbeddingAvailable, embedAll, embedMemory, loadEmbeddingIndex, } from './embeddings.js';
|
|
34
36
|
import { captureError, extractLessons, deduplicateLesson, runWatched, fetchGitLog, isGitRepo, } from './autolearn.js';
|
|
37
|
+
import { extractInvalidationTarget, invalidateMatching } from './invalidation.js';
|
|
38
|
+
import { extractPathTags } from './path-context.js';
|
|
35
39
|
import { getGlobalRoot, initGlobal, promoteToGlobal, shareMemory, listPeers, autoShare, transferScore, searchBothHybrid, syncGlobalToLocal, } from './shared.js';
|
|
36
40
|
import { importChatGPT, importClaude, importCursor, importGenericFile, importMarkdown, } from './importers.js';
|
|
37
41
|
import { cmdCapture } from './capture.js';
|
|
@@ -137,6 +141,7 @@ function autoInstallHooks(quiet) {
|
|
|
137
141
|
{ files: ['AGENTS.md', '.codex'], hook: 'codex' },
|
|
138
142
|
{ files: ['.cursorrules', '.cursor/rules'], hook: 'cursor' },
|
|
139
143
|
{ files: ['.openclaw', 'AGENTS.md'], hook: 'openclaw' },
|
|
144
|
+
{ files: ['.opencode', 'opencode.json'], hook: 'opencode' },
|
|
140
145
|
];
|
|
141
146
|
// Track which hook files we've already touched to avoid double-patching AGENTS.md
|
|
142
147
|
const installed = new Set();
|
|
@@ -173,6 +178,12 @@ function autoInstallHooks(quiet) {
|
|
|
173
178
|
}
|
|
174
179
|
installed.add(targetPath);
|
|
175
180
|
console.log(` Auto-installed ${hook} hook in ${hookDef.file}`);
|
|
181
|
+
// For claude-code, also install the Stop hook in settings.json
|
|
182
|
+
if (hook === 'claude-code') {
|
|
183
|
+
if (installClaudeCodeStopHook()) {
|
|
184
|
+
console.log(` Auto-installed hippo sleep Stop hook in Claude Code settings.json`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
176
187
|
}
|
|
177
188
|
}
|
|
178
189
|
/**
|
|
@@ -258,6 +269,12 @@ function cmdRemember(hippoRoot, text, flags) {
|
|
|
258
269
|
confidence,
|
|
259
270
|
schema_fit: schemaFit,
|
|
260
271
|
});
|
|
272
|
+
// Auto-tag with path context
|
|
273
|
+
const pathTags = extractPathTags(process.cwd());
|
|
274
|
+
for (const pt of pathTags) {
|
|
275
|
+
if (!entry.tags.includes(pt))
|
|
276
|
+
entry.tags.push(pt);
|
|
277
|
+
}
|
|
261
278
|
writeEntry(targetRoot, entry);
|
|
262
279
|
updateStats(targetRoot, { remembered: 1 });
|
|
263
280
|
const prefix = useGlobal ? '[global] ' : '';
|
|
@@ -1213,14 +1230,27 @@ function learnFromRepo(hippoRoot, repoPath, days, label) {
|
|
|
1213
1230
|
skipped++;
|
|
1214
1231
|
continue;
|
|
1215
1232
|
}
|
|
1233
|
+
const target = extractInvalidationTarget(lesson);
|
|
1234
|
+
if (target) {
|
|
1235
|
+
const invResult = invalidateMatching(hippoRoot, target);
|
|
1236
|
+
if (invResult.invalidated > 0) {
|
|
1237
|
+
console.log(`${prefix} Invalidated ${invResult.invalidated} memories referencing "${target.from}"`);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1216
1240
|
const schemaFitVal = computeSchemaFit(lesson, gitLearnTags, existingForSchema);
|
|
1217
1241
|
const entry = createMemory(lesson, {
|
|
1218
1242
|
layer: Layer.Episodic,
|
|
1219
|
-
tags: gitLearnTags,
|
|
1243
|
+
tags: [...gitLearnTags],
|
|
1220
1244
|
source: 'git-learn',
|
|
1221
1245
|
confidence: 'observed',
|
|
1222
1246
|
schema_fit: schemaFitVal,
|
|
1223
1247
|
});
|
|
1248
|
+
// Auto-tag with path context from the repo being learned
|
|
1249
|
+
const learnPathTags = extractPathTags(repoPath);
|
|
1250
|
+
for (const pt of learnPathTags) {
|
|
1251
|
+
if (!entry.tags.includes(pt))
|
|
1252
|
+
entry.tags.push(pt);
|
|
1253
|
+
}
|
|
1224
1254
|
writeEntry(hippoRoot, entry);
|
|
1225
1255
|
updateStats(hippoRoot, { remembered: 1 });
|
|
1226
1256
|
if (isEmbeddingAvailable()) {
|
|
@@ -1480,6 +1510,34 @@ After significant coding sessions:
|
|
|
1480
1510
|
\`\`\`bash
|
|
1481
1511
|
hippo learn --git
|
|
1482
1512
|
\`\`\`
|
|
1513
|
+
`.trim(),
|
|
1514
|
+
},
|
|
1515
|
+
'opencode': {
|
|
1516
|
+
file: 'AGENTS.md',
|
|
1517
|
+
description: 'OpenCode',
|
|
1518
|
+
content: `
|
|
1519
|
+
## Project Memory (Hippo)
|
|
1520
|
+
|
|
1521
|
+
At the start of every task, run:
|
|
1522
|
+
\`\`\`bash
|
|
1523
|
+
hippo context --auto --budget 1500
|
|
1524
|
+
\`\`\`
|
|
1525
|
+
Read the output before writing any code.
|
|
1526
|
+
|
|
1527
|
+
When you learn something important or hit an error:
|
|
1528
|
+
\`\`\`bash
|
|
1529
|
+
hippo remember "<lesson>" --error
|
|
1530
|
+
\`\`\`
|
|
1531
|
+
|
|
1532
|
+
When stuck or repeating yourself, check if this happened before:
|
|
1533
|
+
\`\`\`bash
|
|
1534
|
+
hippo recall "<what's going wrong>" --budget 2000
|
|
1535
|
+
\`\`\`
|
|
1536
|
+
|
|
1537
|
+
On task completion:
|
|
1538
|
+
\`\`\`bash
|
|
1539
|
+
hippo outcome --good
|
|
1540
|
+
\`\`\`
|
|
1483
1541
|
`.trim(),
|
|
1484
1542
|
},
|
|
1485
1543
|
};
|
|
@@ -1529,6 +1587,12 @@ function cmdHook(args, flags) {
|
|
|
1529
1587
|
fs.writeFileSync(filepath, block + '\n', 'utf8');
|
|
1530
1588
|
console.log(`Created ${hook.file} with Hippo hook`);
|
|
1531
1589
|
}
|
|
1590
|
+
// For claude-code, also install the Stop hook in settings.json
|
|
1591
|
+
if (target === 'claude-code') {
|
|
1592
|
+
if (installClaudeCodeStopHook()) {
|
|
1593
|
+
console.log(`Installed hippo sleep Stop hook in Claude Code settings.json`);
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1532
1596
|
return;
|
|
1533
1597
|
}
|
|
1534
1598
|
if (subcommand === 'uninstall') {
|
|
@@ -1551,6 +1615,12 @@ function cmdHook(args, flags) {
|
|
|
1551
1615
|
const cleaned = existing.replace(re, '\n').replace(/\n{3,}/g, '\n\n').trim();
|
|
1552
1616
|
fs.writeFileSync(filepath, cleaned + '\n', 'utf8');
|
|
1553
1617
|
console.log(`Removed Hippo hook from ${hook.file}`);
|
|
1618
|
+
// For claude-code, also remove the Stop hook from settings.json
|
|
1619
|
+
if (target === 'claude-code') {
|
|
1620
|
+
if (uninstallClaudeCodeStopHook()) {
|
|
1621
|
+
console.log(`Removed hippo sleep Stop hook from Claude Code settings.json`);
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1554
1624
|
return;
|
|
1555
1625
|
}
|
|
1556
1626
|
console.error('Usage: hippo hook <install|uninstall|list> [target]');
|
|
@@ -1560,6 +1630,94 @@ function escapeRegex(s) {
|
|
|
1560
1630
|
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1561
1631
|
}
|
|
1562
1632
|
// ---------------------------------------------------------------------------
|
|
1633
|
+
// Claude Code settings.json Stop hook (hippo sleep on session end)
|
|
1634
|
+
// ---------------------------------------------------------------------------
|
|
1635
|
+
const HIPPO_STOP_HOOK_MARKER = 'hippo sleep';
|
|
1636
|
+
/**
|
|
1637
|
+
* Resolve the Claude Code user-level settings.json path (~/.claude/settings.json).
|
|
1638
|
+
* Always targets the global config so the Stop hook runs for all sessions.
|
|
1639
|
+
*/
|
|
1640
|
+
function resolveClaudeSettingsPath() {
|
|
1641
|
+
const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
1642
|
+
return path.join(home, '.claude', 'settings.json');
|
|
1643
|
+
}
|
|
1644
|
+
/**
|
|
1645
|
+
* Check if hippo sleep Stop hook is already installed in Claude Code settings.
|
|
1646
|
+
*/
|
|
1647
|
+
function hasClaudeCodeStopHook(settings) {
|
|
1648
|
+
const hooks = settings.hooks;
|
|
1649
|
+
if (!hooks?.Stop)
|
|
1650
|
+
return false;
|
|
1651
|
+
return JSON.stringify(hooks.Stop).includes(HIPPO_STOP_HOOK_MARKER);
|
|
1652
|
+
}
|
|
1653
|
+
/**
|
|
1654
|
+
* Install a Claude Code Stop hook that runs `hippo sleep` at session end.
|
|
1655
|
+
* Merges into existing settings.json without clobbering other hooks.
|
|
1656
|
+
*/
|
|
1657
|
+
function installClaudeCodeStopHook() {
|
|
1658
|
+
const settingsPath = resolveClaudeSettingsPath();
|
|
1659
|
+
const dir = path.dirname(settingsPath);
|
|
1660
|
+
if (!fs.existsSync(dir))
|
|
1661
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1662
|
+
let settings = {};
|
|
1663
|
+
if (fs.existsSync(settingsPath)) {
|
|
1664
|
+
try {
|
|
1665
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
1666
|
+
}
|
|
1667
|
+
catch {
|
|
1668
|
+
console.error(` Warning: could not parse ${settingsPath}, skipping Stop hook install`);
|
|
1669
|
+
return false;
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
if (hasClaudeCodeStopHook(settings))
|
|
1673
|
+
return false;
|
|
1674
|
+
// Ensure hooks.Stop array exists
|
|
1675
|
+
if (!settings.hooks)
|
|
1676
|
+
settings.hooks = {};
|
|
1677
|
+
const hooks = settings.hooks;
|
|
1678
|
+
if (!Array.isArray(hooks.Stop))
|
|
1679
|
+
hooks.Stop = [];
|
|
1680
|
+
// Append hippo sleep hook entry (silent: runs every turn, must not produce errors)
|
|
1681
|
+
hooks.Stop.push({
|
|
1682
|
+
hooks: [
|
|
1683
|
+
{
|
|
1684
|
+
type: 'command',
|
|
1685
|
+
command: 'hippo sleep 2>/dev/null || true',
|
|
1686
|
+
timeout: 30,
|
|
1687
|
+
},
|
|
1688
|
+
],
|
|
1689
|
+
});
|
|
1690
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
|
|
1691
|
+
return true;
|
|
1692
|
+
}
|
|
1693
|
+
/**
|
|
1694
|
+
* Remove the hippo sleep Stop hook from Claude Code settings.json.
|
|
1695
|
+
*/
|
|
1696
|
+
function uninstallClaudeCodeStopHook() {
|
|
1697
|
+
const settingsPath = resolveClaudeSettingsPath();
|
|
1698
|
+
if (!fs.existsSync(settingsPath))
|
|
1699
|
+
return false;
|
|
1700
|
+
let settings;
|
|
1701
|
+
try {
|
|
1702
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
1703
|
+
}
|
|
1704
|
+
catch {
|
|
1705
|
+
return false;
|
|
1706
|
+
}
|
|
1707
|
+
if (!hasClaudeCodeStopHook(settings))
|
|
1708
|
+
return false;
|
|
1709
|
+
const hooks = settings.hooks;
|
|
1710
|
+
hooks.Stop = hooks.Stop.filter((entry) => !JSON.stringify(entry).includes(HIPPO_STOP_HOOK_MARKER));
|
|
1711
|
+
// Clean up empty Stop array
|
|
1712
|
+
if (hooks.Stop.length === 0)
|
|
1713
|
+
delete hooks.Stop;
|
|
1714
|
+
// Clean up empty hooks object
|
|
1715
|
+
if (Object.keys(hooks).length === 0)
|
|
1716
|
+
delete settings.hooks;
|
|
1717
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
|
|
1718
|
+
return true;
|
|
1719
|
+
}
|
|
1720
|
+
// ---------------------------------------------------------------------------
|
|
1563
1721
|
// Working Memory
|
|
1564
1722
|
// ---------------------------------------------------------------------------
|
|
1565
1723
|
function cmdWm(hippoRoot, args, flags) {
|
|
@@ -1735,6 +1893,8 @@ Commands:
|
|
|
1735
1893
|
--dry-run Preview without writing
|
|
1736
1894
|
--global Write to global store (~/.hippo/)
|
|
1737
1895
|
--tag <tag> Add extra tag (repeatable)
|
|
1896
|
+
export [file] Export all memories (default: stdout)
|
|
1897
|
+
--format <fmt> Output format: json (default) or markdown
|
|
1738
1898
|
capture Extract memories from conversation text
|
|
1739
1899
|
--stdin Read from piped input
|
|
1740
1900
|
--file <path> Read from a file
|
|
@@ -1743,8 +1903,14 @@ Commands:
|
|
|
1743
1903
|
--global Write to global store (~/.hippo/)
|
|
1744
1904
|
hook <sub> [target] Manage framework integrations
|
|
1745
1905
|
hook list Show available hooks
|
|
1746
|
-
hook install <target> Install hook (claude-code|codex|cursor|openclaw)
|
|
1906
|
+
hook install <target> Install hook (claude-code|codex|cursor|openclaw|opencode)
|
|
1907
|
+
claude-code also installs Stop hook (hippo sleep on exit)
|
|
1747
1908
|
hook uninstall <target> Remove hook
|
|
1909
|
+
decide "<decision>" Record an architectural decision (90-day half-life)
|
|
1910
|
+
--context "<why>" Why this decision was made
|
|
1911
|
+
--supersedes <id> Supersede a previous decision (weakens it)
|
|
1912
|
+
invalidate "<pattern>" Actively weaken memories matching an old pattern
|
|
1913
|
+
--reason "<why>" Optional: what replaced it
|
|
1748
1914
|
wm <sub> Working memory — bounded buffer for current state
|
|
1749
1915
|
wm push Push a working memory entry
|
|
1750
1916
|
--scope <scope> Scope name (default: default)
|
|
@@ -1784,6 +1950,10 @@ Examples:
|
|
|
1784
1950
|
hippo promote mem_abc123
|
|
1785
1951
|
hippo sync
|
|
1786
1952
|
hippo hook install claude-code
|
|
1953
|
+
hippo decide "Use PostgreSQL for new services" --context "JSONB support"
|
|
1954
|
+
hippo invalidate "REST API" --reason "migrated to GraphQL"
|
|
1955
|
+
hippo export memories.json
|
|
1956
|
+
hippo export --format markdown memories.md
|
|
1787
1957
|
hippo sleep --dry-run
|
|
1788
1958
|
hippo outcome --good
|
|
1789
1959
|
hippo status
|
|
@@ -1959,6 +2129,37 @@ async function main() {
|
|
|
1959
2129
|
case 'import':
|
|
1960
2130
|
cmdImport(hippoRoot, args, flags);
|
|
1961
2131
|
break;
|
|
2132
|
+
case 'export': {
|
|
2133
|
+
requireInit(hippoRoot);
|
|
2134
|
+
const format = flags['format'] || 'json';
|
|
2135
|
+
const outputPath = args[0] || null;
|
|
2136
|
+
const entries = loadAllEntries(hippoRoot);
|
|
2137
|
+
let output;
|
|
2138
|
+
if (format === 'markdown' || format === 'md') {
|
|
2139
|
+
output = entries.map(e => {
|
|
2140
|
+
const meta = [
|
|
2141
|
+
`id: ${e.id}`,
|
|
2142
|
+
`created: ${e.created}`,
|
|
2143
|
+
`tags: ${e.tags.join(', ')}`,
|
|
2144
|
+
`confidence: ${e.confidence}`,
|
|
2145
|
+
`half_life: ${e.half_life_days}d`,
|
|
2146
|
+
`strength: ${e.strength.toFixed(2)}`,
|
|
2147
|
+
].join(' | ');
|
|
2148
|
+
return `### ${e.id}\n\n${e.content}\n\n_${meta}_`;
|
|
2149
|
+
}).join('\n\n---\n\n');
|
|
2150
|
+
}
|
|
2151
|
+
else {
|
|
2152
|
+
output = JSON.stringify(entries, null, 2);
|
|
2153
|
+
}
|
|
2154
|
+
if (outputPath) {
|
|
2155
|
+
fs.writeFileSync(outputPath, output, 'utf8');
|
|
2156
|
+
console.log(`Exported ${entries.length} memories to ${outputPath}`);
|
|
2157
|
+
}
|
|
2158
|
+
else {
|
|
2159
|
+
console.log(output);
|
|
2160
|
+
}
|
|
2161
|
+
break;
|
|
2162
|
+
}
|
|
1962
2163
|
case 'capture': {
|
|
1963
2164
|
let captureSource = null;
|
|
1964
2165
|
let captureFile;
|
|
@@ -2001,6 +2202,75 @@ async function main() {
|
|
|
2001
2202
|
// Server runs until stdin closes, so we never reach here
|
|
2002
2203
|
await new Promise(() => { }); // hang forever
|
|
2003
2204
|
break;
|
|
2205
|
+
case 'invalidate': {
|
|
2206
|
+
requireInit(hippoRoot);
|
|
2207
|
+
const target = args[0];
|
|
2208
|
+
if (!target) {
|
|
2209
|
+
console.error('Usage: hippo invalidate "<old pattern>" [--reason "<why>"]');
|
|
2210
|
+
process.exit(1);
|
|
2211
|
+
}
|
|
2212
|
+
const reason = flags['reason'] || null;
|
|
2213
|
+
const invTarget = {
|
|
2214
|
+
from: target,
|
|
2215
|
+
to: reason,
|
|
2216
|
+
type: 'migration',
|
|
2217
|
+
};
|
|
2218
|
+
const result = invalidateMatching(hippoRoot, invTarget);
|
|
2219
|
+
if (result.invalidated === 0) {
|
|
2220
|
+
console.log(`No memories matched "${target}".`);
|
|
2221
|
+
}
|
|
2222
|
+
else {
|
|
2223
|
+
console.log(`Invalidated ${result.invalidated} memories referencing "${target}".`);
|
|
2224
|
+
result.targets.forEach(id => console.log(` ${id}`));
|
|
2225
|
+
}
|
|
2226
|
+
break;
|
|
2227
|
+
}
|
|
2228
|
+
case 'decide': {
|
|
2229
|
+
requireInit(hippoRoot);
|
|
2230
|
+
const text = args[0];
|
|
2231
|
+
if (!text) {
|
|
2232
|
+
console.error('Usage: hippo decide "<decision>" [--context "<why>"] [--supersedes <id>]');
|
|
2233
|
+
process.exit(1);
|
|
2234
|
+
}
|
|
2235
|
+
const context = flags['context'] || '';
|
|
2236
|
+
const supersedesId = flags['supersedes'] || null;
|
|
2237
|
+
// Build content with context
|
|
2238
|
+
const decisionContent = context ? `${text}\n\nContext: ${context}` : text;
|
|
2239
|
+
// Handle supersession
|
|
2240
|
+
if (supersedesId) {
|
|
2241
|
+
const oldEntry = readEntry(hippoRoot, supersedesId);
|
|
2242
|
+
if (!oldEntry) {
|
|
2243
|
+
console.error(`Memory ${supersedesId} not found.`);
|
|
2244
|
+
process.exit(1);
|
|
2245
|
+
}
|
|
2246
|
+
oldEntry.half_life_days = Math.max(1, Math.floor(oldEntry.half_life_days / 2));
|
|
2247
|
+
oldEntry.confidence = 'stale';
|
|
2248
|
+
if (!oldEntry.tags.includes('superseded'))
|
|
2249
|
+
oldEntry.tags.push('superseded');
|
|
2250
|
+
writeEntry(hippoRoot, oldEntry);
|
|
2251
|
+
console.log(`Superseded ${supersedesId} (half-life halved, marked stale)`);
|
|
2252
|
+
}
|
|
2253
|
+
// Create decision memory
|
|
2254
|
+
const mem = createMemory(decisionContent, {
|
|
2255
|
+
tags: ['decision'],
|
|
2256
|
+
layer: Layer.Semantic,
|
|
2257
|
+
confidence: 'verified',
|
|
2258
|
+
source: 'decision',
|
|
2259
|
+
});
|
|
2260
|
+
mem.half_life_days = DECISION_HALF_LIFE_DAYS;
|
|
2261
|
+
// Auto-tag with path context
|
|
2262
|
+
const decisionPathTags = extractPathTags(process.cwd());
|
|
2263
|
+
for (const pt of decisionPathTags) {
|
|
2264
|
+
if (!mem.tags.includes(pt))
|
|
2265
|
+
mem.tags.push(pt);
|
|
2266
|
+
}
|
|
2267
|
+
writeEntry(hippoRoot, mem);
|
|
2268
|
+
console.log(`Decision recorded: ${mem.id}`);
|
|
2269
|
+
if (supersedesId) {
|
|
2270
|
+
console.log(` Supersedes: ${supersedesId}`);
|
|
2271
|
+
}
|
|
2272
|
+
break;
|
|
2273
|
+
}
|
|
2004
2274
|
case 'help':
|
|
2005
2275
|
case '--help':
|
|
2006
2276
|
case '-h':
|