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 CHANGED
@@ -6,7 +6,7 @@
6
6
  [![license](https://img.shields.io/badge/license-MIT-blue)](./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, codex, cursor, openclaw) |
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
@@ -21,6 +21,7 @@
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
  export {};
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;;;;;GAuBG"}
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':