hippo-memory 0.8.1 → 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 +135 -7
- package/dist/cli.d.ts +6 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +853 -213
- package/dist/cli.js.map +1 -1
- package/dist/consolidate.d.ts.map +1 -1
- package/dist/consolidate.js +12 -5
- package/dist/consolidate.js.map +1 -1
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +149 -100
- package/dist/db.js.map +1 -1
- package/dist/handoff.d.ts +29 -0
- package/dist/handoff.d.ts.map +1 -0
- package/dist/handoff.js +30 -0
- package/dist/handoff.js.map +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.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 +19 -0
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +55 -2
- package/dist/search.js.map +1 -1
- package/dist/store.d.ts +18 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +209 -99
- package/dist/store.js.map +1 -1
- package/dist/working-memory.d.ts +59 -0
- package/dist/working-memory.d.ts.map +1 -0
- package/dist/working-memory.js +149 -0
- package/dist/working-memory.js.map +1 -0
- package/extensions/openclaw-plugin/index.ts +569 -495
- package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
- package/extensions/openclaw-plugin/package.json +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -5,13 +5,15 @@
|
|
|
5
5
|
* Commands:
|
|
6
6
|
* hippo init [--global]
|
|
7
7
|
* hippo remember <text> [--tag <t>] [--error] [--pin] [--global]
|
|
8
|
-
* hippo recall <query> [--budget <n>] [--json]
|
|
8
|
+
* hippo recall <query> [--budget <n>] [--json] [--why]
|
|
9
9
|
* hippo sleep [--dry-run]
|
|
10
10
|
* hippo status
|
|
11
11
|
* hippo outcome --good | --bad [--id <id>]
|
|
12
12
|
* hippo conflicts [--status <status>] [--json]
|
|
13
13
|
* hippo snapshot <save|show|clear>
|
|
14
|
-
* hippo session <log|show>
|
|
14
|
+
* hippo session <log|show|latest|resume>
|
|
15
|
+
* hippo handoff <create|latest|show>
|
|
16
|
+
* hippo current <show>
|
|
15
17
|
* hippo forget <id>
|
|
16
18
|
* hippo inspect <id>
|
|
17
19
|
* hippo embed [--status]
|
|
@@ -19,22 +21,34 @@
|
|
|
19
21
|
* hippo learn --git [--days <n>] [--repos <paths>]
|
|
20
22
|
* hippo promote <id>
|
|
21
23
|
* hippo sync
|
|
24
|
+
* hippo decide "<decision>" [--context "<why>"] [--supersedes <id>]
|
|
25
|
+
* hippo wm <push|read|clear|flush>
|
|
22
26
|
*/
|
|
23
27
|
import * as path from 'path';
|
|
24
28
|
import * as fs from 'fs';
|
|
29
|
+
import * as os from 'os';
|
|
25
30
|
import { execSync } from 'child_process';
|
|
26
|
-
import { createMemory, calculateStrength, deriveHalfLife, resolveConfidence, applyOutcome, computeSchemaFit, Layer, } from './memory.js';
|
|
27
|
-
import { getHippoRoot, isInitialized, initStore, writeEntry, readEntry, deleteEntry, loadAllEntries, loadSearchEntries, loadIndex, saveIndex, loadStats, updateStats, saveActiveTaskSnapshot, loadActiveTaskSnapshot, clearActiveTaskSnapshot, appendSessionEvent, listSessionEvents, listMemoryConflicts, resolveConflict, } from './store.js';
|
|
28
|
-
import { markRetrieved, estimateTokens, hybridSearch } from './search.js';
|
|
31
|
+
import { createMemory, calculateStrength, deriveHalfLife, resolveConfidence, applyOutcome, computeSchemaFit, Layer, DECISION_HALF_LIFE_DAYS, } from './memory.js';
|
|
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';
|
|
33
|
+
import { markRetrieved, estimateTokens, hybridSearch, explainMatch } from './search.js';
|
|
29
34
|
import { consolidate } from './consolidate.js';
|
|
30
35
|
import { isEmbeddingAvailable, embedAll, embedMemory, loadEmbeddingIndex, } from './embeddings.js';
|
|
31
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';
|
|
32
39
|
import { getGlobalRoot, initGlobal, promoteToGlobal, shareMemory, listPeers, autoShare, transferScore, searchBothHybrid, syncGlobalToLocal, } from './shared.js';
|
|
33
40
|
import { importChatGPT, importClaude, importCursor, importGenericFile, importMarkdown, } from './importers.js';
|
|
34
41
|
import { cmdCapture } from './capture.js';
|
|
42
|
+
import { wmPush, wmRead, wmClear, wmFlush } from './working-memory.js';
|
|
35
43
|
// ---------------------------------------------------------------------------
|
|
36
44
|
// Helpers
|
|
37
45
|
// ---------------------------------------------------------------------------
|
|
46
|
+
function parseLimitFlag(value) {
|
|
47
|
+
if (!value)
|
|
48
|
+
return Infinity;
|
|
49
|
+
const parsed = parseInt(String(value), 10);
|
|
50
|
+
return Number.isFinite(parsed) && parsed >= 1 ? parsed : Infinity;
|
|
51
|
+
}
|
|
38
52
|
function requireInit(hippoRoot) {
|
|
39
53
|
if (!isInitialized(hippoRoot)) {
|
|
40
54
|
console.error('No .hippo directory found. Run `hippo init` first.');
|
|
@@ -57,8 +71,8 @@ function parseArgs(argv) {
|
|
|
57
71
|
i++;
|
|
58
72
|
}
|
|
59
73
|
else {
|
|
60
|
-
// Check if it's a repeatable flag (tag)
|
|
61
|
-
if (key === 'tag') {
|
|
74
|
+
// Check if it's a repeatable flag (tag, artifact)
|
|
75
|
+
if (key === 'tag' || key === 'artifact') {
|
|
62
76
|
if (Array.isArray(flags[key])) {
|
|
63
77
|
flags[key].push(next);
|
|
64
78
|
}
|
|
@@ -127,6 +141,7 @@ function autoInstallHooks(quiet) {
|
|
|
127
141
|
{ files: ['AGENTS.md', '.codex'], hook: 'codex' },
|
|
128
142
|
{ files: ['.cursorrules', '.cursor/rules'], hook: 'cursor' },
|
|
129
143
|
{ files: ['.openclaw', 'AGENTS.md'], hook: 'openclaw' },
|
|
144
|
+
{ files: ['.opencode', 'opencode.json'], hook: 'opencode' },
|
|
130
145
|
];
|
|
131
146
|
// Track which hook files we've already touched to avoid double-patching AGENTS.md
|
|
132
147
|
const installed = new Set();
|
|
@@ -163,6 +178,12 @@ function autoInstallHooks(quiet) {
|
|
|
163
178
|
}
|
|
164
179
|
installed.add(targetPath);
|
|
165
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
|
+
}
|
|
166
187
|
}
|
|
167
188
|
}
|
|
168
189
|
/**
|
|
@@ -248,6 +269,12 @@ function cmdRemember(hippoRoot, text, flags) {
|
|
|
248
269
|
confidence,
|
|
249
270
|
schema_fit: schemaFit,
|
|
250
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
|
+
}
|
|
251
278
|
writeEntry(targetRoot, entry);
|
|
252
279
|
updateStats(targetRoot, { remembered: 1 });
|
|
253
280
|
const prefix = useGlobal ? '[global] ' : '';
|
|
@@ -267,7 +294,9 @@ function cmdRemember(hippoRoot, text, flags) {
|
|
|
267
294
|
async function cmdRecall(hippoRoot, query, flags) {
|
|
268
295
|
requireInit(hippoRoot);
|
|
269
296
|
const budget = parseInt(String(flags['budget'] ?? '4000'), 10);
|
|
297
|
+
const limit = parseLimitFlag(flags['limit']);
|
|
270
298
|
const asJson = Boolean(flags['json']);
|
|
299
|
+
const showWhy = Boolean(flags['why']);
|
|
271
300
|
const globalRoot = getGlobalRoot();
|
|
272
301
|
const localEntries = loadSearchEntries(hippoRoot, query);
|
|
273
302
|
const globalEntries = isInitialized(globalRoot) ? loadSearchEntries(globalRoot, query) : [];
|
|
@@ -280,6 +309,9 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
280
309
|
else {
|
|
281
310
|
results = await hybridSearch(query, localEntries, { budget, hippoRoot });
|
|
282
311
|
}
|
|
312
|
+
if (limit < results.length) {
|
|
313
|
+
results = results.slice(0, limit);
|
|
314
|
+
}
|
|
283
315
|
if (results.length === 0) {
|
|
284
316
|
if (asJson) {
|
|
285
317
|
console.log(JSON.stringify({ query, results: [], total: 0 }));
|
|
@@ -301,14 +333,27 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
301
333
|
saveIndex(hippoRoot, localIndex);
|
|
302
334
|
updateStats(hippoRoot, { recalled: results.length });
|
|
303
335
|
if (asJson) {
|
|
304
|
-
const output = results.map((r) =>
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
336
|
+
const output = results.map((r) => {
|
|
337
|
+
const isGlobal = isInitialized(globalRoot) && !localIndex.entries[r.entry.id];
|
|
338
|
+
const base = {
|
|
339
|
+
id: r.entry.id,
|
|
340
|
+
score: r.score,
|
|
341
|
+
strength: r.entry.strength,
|
|
342
|
+
tokens: r.tokens,
|
|
343
|
+
tags: r.entry.tags,
|
|
344
|
+
content: r.entry.content,
|
|
345
|
+
};
|
|
346
|
+
if (showWhy) {
|
|
347
|
+
const explanation = explainMatch(query, r);
|
|
348
|
+
base.layer = r.entry.layer;
|
|
349
|
+
base.confidence = resolveConfidence(r.entry);
|
|
350
|
+
base.source = isGlobal ? 'global' : 'local';
|
|
351
|
+
base.reason = explanation.reason;
|
|
352
|
+
base.bm25 = r.bm25;
|
|
353
|
+
base.cosine = r.cosine;
|
|
354
|
+
}
|
|
355
|
+
return base;
|
|
356
|
+
});
|
|
312
357
|
console.log(JSON.stringify({ query, budget, results: output, total: output.length }));
|
|
313
358
|
return;
|
|
314
359
|
}
|
|
@@ -319,9 +364,16 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
319
364
|
const conf = resolveConfidence(e);
|
|
320
365
|
const confLabel = conf === 'stale' || conf === 'inferred' ? `[${conf}] \u26A0\uFE0F` : `[${conf}]`;
|
|
321
366
|
const strengthBar = '\u2588'.repeat(Math.round(e.strength * 10)) + '\u2591'.repeat(10 - Math.round(e.strength * 10));
|
|
322
|
-
const
|
|
367
|
+
const isGlobal = isInitialized(globalRoot) && !localIndex.entries[e.id];
|
|
368
|
+
const globalMark = isGlobal ? ' [global]' : '';
|
|
369
|
+
const sourceMark = isGlobal ? ' [global]' : ' [local]';
|
|
323
370
|
console.log(`--- ${e.id} [${e.layer}] ${confLabel}${globalMark} score=${fmt(r.score, 3)} strength=${fmt(e.strength)}`);
|
|
324
371
|
console.log(` [${strengthBar}] tags: ${e.tags.join(', ') || 'none'} | retrieved: ${e.retrieval_count}x`);
|
|
372
|
+
if (showWhy) {
|
|
373
|
+
const explanation = explainMatch(query, r);
|
|
374
|
+
console.log(` source:${sourceMark} | layer: [${e.layer}] | confidence: [${conf}]`);
|
|
375
|
+
console.log(` reason: ${explanation.reason}`);
|
|
376
|
+
}
|
|
325
377
|
console.log();
|
|
326
378
|
console.log(e.content);
|
|
327
379
|
console.log();
|
|
@@ -682,12 +734,226 @@ function cmdSession(hippoRoot, args, flags) {
|
|
|
682
734
|
printSessionEvents(events);
|
|
683
735
|
return;
|
|
684
736
|
}
|
|
685
|
-
|
|
737
|
+
if (subcommand === 'latest') {
|
|
738
|
+
const snapshot = loadActiveTaskSnapshot(hippoRoot);
|
|
739
|
+
const events = listSessionEvents(hippoRoot, {
|
|
740
|
+
session_id: sessionId || snapshot?.session_id || undefined,
|
|
741
|
+
limit,
|
|
742
|
+
});
|
|
743
|
+
if (flags['json']) {
|
|
744
|
+
console.log(JSON.stringify({ snapshot: snapshot ?? null, events }, null, 2));
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
if (snapshot) {
|
|
748
|
+
printActiveTaskSnapshot(snapshot);
|
|
749
|
+
}
|
|
750
|
+
else {
|
|
751
|
+
console.log('No active task snapshot.');
|
|
752
|
+
console.log('');
|
|
753
|
+
}
|
|
754
|
+
printSessionEvents(events);
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
if (subcommand === 'resume') {
|
|
758
|
+
const handoff = loadLatestHandoff(hippoRoot, sessionId || undefined);
|
|
759
|
+
if (!handoff) {
|
|
760
|
+
console.log('No handoff to resume from.');
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
const lines = [
|
|
764
|
+
'## Session Handoff (resumed)',
|
|
765
|
+
'',
|
|
766
|
+
`- Session: ${handoff.sessionId}`,
|
|
767
|
+
`- Updated: ${handoff.updatedAt}`,
|
|
768
|
+
];
|
|
769
|
+
if (handoff.taskId)
|
|
770
|
+
lines.push(`- Task: ${handoff.taskId}`);
|
|
771
|
+
if (handoff.repoRoot)
|
|
772
|
+
lines.push(`- Repo: ${handoff.repoRoot}`);
|
|
773
|
+
lines.push('', '### Summary', handoff.summary);
|
|
774
|
+
if (handoff.nextAction) {
|
|
775
|
+
lines.push('', '### Next action', handoff.nextAction);
|
|
776
|
+
}
|
|
777
|
+
if (handoff.artifacts && handoff.artifacts.length > 0) {
|
|
778
|
+
lines.push('', '### Artifacts');
|
|
779
|
+
for (const artifact of handoff.artifacts) {
|
|
780
|
+
lines.push(`- ${artifact}`);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
lines.push('');
|
|
784
|
+
console.log(lines.join('\n'));
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
console.error('Usage: hippo session <log|show|latest|resume>');
|
|
788
|
+
process.exit(1);
|
|
789
|
+
}
|
|
790
|
+
function printHandoff(handoff) {
|
|
791
|
+
console.log('## Session Handoff\n');
|
|
792
|
+
console.log(`- Session: ${handoff.sessionId}`);
|
|
793
|
+
console.log(`- Updated: ${handoff.updatedAt}`);
|
|
794
|
+
if (handoff.taskId)
|
|
795
|
+
console.log(`- Task: ${handoff.taskId}`);
|
|
796
|
+
if (handoff.repoRoot)
|
|
797
|
+
console.log(`- Repo: ${handoff.repoRoot}`);
|
|
798
|
+
console.log('');
|
|
799
|
+
console.log('### Summary');
|
|
800
|
+
console.log(handoff.summary);
|
|
801
|
+
if (handoff.nextAction) {
|
|
802
|
+
console.log('');
|
|
803
|
+
console.log('### Next action');
|
|
804
|
+
console.log(handoff.nextAction);
|
|
805
|
+
}
|
|
806
|
+
if (handoff.artifacts && handoff.artifacts.length > 0) {
|
|
807
|
+
console.log('');
|
|
808
|
+
console.log('### Artifacts');
|
|
809
|
+
for (const artifact of handoff.artifacts) {
|
|
810
|
+
console.log(`- ${artifact}`);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
console.log('');
|
|
814
|
+
}
|
|
815
|
+
function cmdHandoff(hippoRoot, args, flags) {
|
|
816
|
+
requireInit(hippoRoot);
|
|
817
|
+
const subcommand = args[0] ?? 'latest';
|
|
818
|
+
if (subcommand === 'create') {
|
|
819
|
+
const summary = String(flags['summary'] ?? '').trim();
|
|
820
|
+
if (!summary) {
|
|
821
|
+
console.error('Usage: hippo handoff create --summary "..." [--next "..."] [--session <id>] [--task <id>] [--artifact <path>...]');
|
|
822
|
+
process.exit(1);
|
|
823
|
+
}
|
|
824
|
+
const sessionId = String(flags['session'] ?? flags['id'] ?? '').trim() || `fallback-${Date.now()}-${process.pid}`;
|
|
825
|
+
const nextAction = String(flags['next'] ?? '').trim() || undefined;
|
|
826
|
+
const taskId = String(flags['task'] ?? '').trim() || undefined;
|
|
827
|
+
const artifactFlag = flags['artifact'];
|
|
828
|
+
const artifacts = Array.isArray(artifactFlag)
|
|
829
|
+
? artifactFlag
|
|
830
|
+
: (typeof artifactFlag === 'string' ? [artifactFlag] : []);
|
|
831
|
+
const handoff = saveSessionHandoff(hippoRoot, {
|
|
832
|
+
version: 1,
|
|
833
|
+
sessionId,
|
|
834
|
+
repoRoot: process.cwd(),
|
|
835
|
+
taskId,
|
|
836
|
+
summary,
|
|
837
|
+
nextAction,
|
|
838
|
+
artifacts,
|
|
839
|
+
});
|
|
840
|
+
console.log(`Created session handoff for session ${handoff.sessionId}`);
|
|
841
|
+
console.log(` Summary: ${handoff.summary}`);
|
|
842
|
+
if (handoff.nextAction)
|
|
843
|
+
console.log(` Next: ${handoff.nextAction}`);
|
|
844
|
+
if (handoff.artifacts && handoff.artifacts.length > 0) {
|
|
845
|
+
console.log(` Artifacts: ${handoff.artifacts.join(', ')}`);
|
|
846
|
+
}
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
if (subcommand === 'latest') {
|
|
850
|
+
const sessionId = String(flags['session'] ?? flags['id'] ?? '').trim() || undefined;
|
|
851
|
+
const handoff = loadLatestHandoff(hippoRoot, sessionId);
|
|
852
|
+
if (!handoff) {
|
|
853
|
+
if (flags['json']) {
|
|
854
|
+
console.log(JSON.stringify({ handoff: null }));
|
|
855
|
+
}
|
|
856
|
+
else {
|
|
857
|
+
console.log('No session handoff found.');
|
|
858
|
+
}
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
if (flags['json']) {
|
|
862
|
+
console.log(JSON.stringify({ handoff }, null, 2));
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
printHandoff(handoff);
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
if (subcommand === 'show') {
|
|
869
|
+
const idArg = args[1];
|
|
870
|
+
if (!idArg) {
|
|
871
|
+
console.error('Usage: hippo handoff show <id> [--json]');
|
|
872
|
+
process.exit(1);
|
|
873
|
+
}
|
|
874
|
+
const handoffId = parseInt(idArg, 10);
|
|
875
|
+
if (!Number.isFinite(handoffId) || handoffId <= 0) {
|
|
876
|
+
console.error(`Invalid handoff ID: ${idArg}`);
|
|
877
|
+
process.exit(1);
|
|
878
|
+
}
|
|
879
|
+
const handoff = loadHandoffById(hippoRoot, handoffId);
|
|
880
|
+
if (!handoff) {
|
|
881
|
+
if (flags['json']) {
|
|
882
|
+
console.log(JSON.stringify({ handoff: null }));
|
|
883
|
+
}
|
|
884
|
+
else {
|
|
885
|
+
console.log(`No handoff found with ID ${handoffId}.`);
|
|
886
|
+
}
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
if (flags['json']) {
|
|
890
|
+
console.log(JSON.stringify({ handoff }, null, 2));
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
printHandoff(handoff);
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
console.error('Usage: hippo handoff <create|latest|show>');
|
|
897
|
+
process.exit(1);
|
|
898
|
+
}
|
|
899
|
+
function cmdCurrent(hippoRoot, args, flags) {
|
|
900
|
+
requireInit(hippoRoot);
|
|
901
|
+
const subcommand = args[0] ?? 'show';
|
|
902
|
+
if (subcommand === 'show') {
|
|
903
|
+
const asJson = Boolean(flags['json']);
|
|
904
|
+
const snapshot = loadActiveTaskSnapshot(hippoRoot);
|
|
905
|
+
const sessionId = snapshot?.session_id ?? undefined;
|
|
906
|
+
const events = listSessionEvents(hippoRoot, {
|
|
907
|
+
session_id: sessionId,
|
|
908
|
+
limit: 5,
|
|
909
|
+
});
|
|
910
|
+
if (asJson) {
|
|
911
|
+
console.log(JSON.stringify({
|
|
912
|
+
snapshot: snapshot ?? null,
|
|
913
|
+
events: events.map((ev) => ({
|
|
914
|
+
id: ev.id,
|
|
915
|
+
session_id: ev.session_id,
|
|
916
|
+
event_type: ev.event_type,
|
|
917
|
+
content: ev.content,
|
|
918
|
+
created_at: ev.created_at,
|
|
919
|
+
})),
|
|
920
|
+
}));
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
if (!snapshot && events.length === 0) {
|
|
924
|
+
console.log('No active task or recent session events.');
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
console.log('# Current State\n');
|
|
928
|
+
if (snapshot) {
|
|
929
|
+
console.log(`Task: ${snapshot.task}`);
|
|
930
|
+
console.log(`Status: ${snapshot.status} | Source: ${snapshot.source} | Updated: ${snapshot.updated_at}`);
|
|
931
|
+
if (snapshot.session_id) {
|
|
932
|
+
console.log(`Session: ${snapshot.session_id}`);
|
|
933
|
+
}
|
|
934
|
+
console.log(`Summary: ${snapshot.summary}`);
|
|
935
|
+
console.log(`Next: ${snapshot.next_step}`);
|
|
936
|
+
}
|
|
937
|
+
else {
|
|
938
|
+
console.log('No active task snapshot.');
|
|
939
|
+
}
|
|
940
|
+
if (events.length > 0) {
|
|
941
|
+
console.log('');
|
|
942
|
+
console.log('Recent events:');
|
|
943
|
+
for (const ev of events) {
|
|
944
|
+
const ts = ev.created_at.slice(0, 19).replace('T', ' ');
|
|
945
|
+
console.log(` [${ts}] (${ev.event_type}) ${ev.content}`);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
console.error('Usage: hippo current <show>');
|
|
686
951
|
process.exit(1);
|
|
687
952
|
}
|
|
688
953
|
async function cmdContext(hippoRoot, args, flags) {
|
|
689
954
|
requireInit(hippoRoot);
|
|
690
955
|
const budget = parseInt(String(flags['budget'] ?? '1500'), 10);
|
|
956
|
+
const limit = parseLimitFlag(flags['limit']);
|
|
691
957
|
// If budget is 0, skip entirely (zero token cost)
|
|
692
958
|
if (budget <= 0)
|
|
693
959
|
return;
|
|
@@ -765,6 +1031,10 @@ async function cmdContext(hippoRoot, args, flags) {
|
|
|
765
1031
|
selectedItems = results;
|
|
766
1032
|
totalTokens = results.reduce((sum, r) => sum + r.tokens, 0);
|
|
767
1033
|
}
|
|
1034
|
+
if (limit < selectedItems.length) {
|
|
1035
|
+
selectedItems = selectedItems.slice(0, limit);
|
|
1036
|
+
totalTokens = selectedItems.reduce((sum, r) => sum + r.tokens, 0);
|
|
1037
|
+
}
|
|
768
1038
|
if (selectedItems.length === 0 && !activeSnapshot && recentSessionEvents.length === 0)
|
|
769
1039
|
return;
|
|
770
1040
|
// Mark retrieved and persist
|
|
@@ -960,14 +1230,27 @@ function learnFromRepo(hippoRoot, repoPath, days, label) {
|
|
|
960
1230
|
skipped++;
|
|
961
1231
|
continue;
|
|
962
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
|
+
}
|
|
963
1240
|
const schemaFitVal = computeSchemaFit(lesson, gitLearnTags, existingForSchema);
|
|
964
1241
|
const entry = createMemory(lesson, {
|
|
965
1242
|
layer: Layer.Episodic,
|
|
966
|
-
tags: gitLearnTags,
|
|
1243
|
+
tags: [...gitLearnTags],
|
|
967
1244
|
source: 'git-learn',
|
|
968
1245
|
confidence: 'observed',
|
|
969
1246
|
schema_fit: schemaFitVal,
|
|
970
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
|
+
}
|
|
971
1254
|
writeEntry(hippoRoot, entry);
|
|
972
1255
|
updateStats(hippoRoot, { remembered: 1 });
|
|
973
1256
|
if (isEmbeddingAvailable()) {
|
|
@@ -1136,97 +1419,125 @@ const HOOKS = {
|
|
|
1136
1419
|
'claude-code': {
|
|
1137
1420
|
file: 'CLAUDE.md',
|
|
1138
1421
|
description: 'Claude Code',
|
|
1139
|
-
content: `
|
|
1140
|
-
## Project Memory (Hippo)
|
|
1141
|
-
|
|
1142
|
-
Before starting work, load relevant context:
|
|
1143
|
-
\`\`\`bash
|
|
1144
|
-
hippo context --auto --budget 1500
|
|
1145
|
-
\`\`\`
|
|
1146
|
-
|
|
1147
|
-
When you learn something important:
|
|
1148
|
-
\`\`\`bash
|
|
1149
|
-
hippo remember "<lesson>"
|
|
1150
|
-
\`\`\`
|
|
1151
|
-
|
|
1152
|
-
When you hit an error or discover a gotcha:
|
|
1153
|
-
\`\`\`bash
|
|
1154
|
-
hippo remember "<what went wrong and why>" --error
|
|
1155
|
-
\`\`\`
|
|
1156
|
-
|
|
1157
|
-
After significant discussions or decisions, capture context:
|
|
1158
|
-
\`\`\`bash
|
|
1159
|
-
hippo capture --stdin <<< 'summary of what was decided'
|
|
1160
|
-
\`\`\`
|
|
1161
|
-
|
|
1162
|
-
After completing work successfully:
|
|
1163
|
-
\`\`\`bash
|
|
1164
|
-
hippo outcome --good
|
|
1165
|
-
\`\`\`
|
|
1422
|
+
content: `
|
|
1423
|
+
## Project Memory (Hippo)
|
|
1424
|
+
|
|
1425
|
+
Before starting work, load relevant context:
|
|
1426
|
+
\`\`\`bash
|
|
1427
|
+
hippo context --auto --budget 1500
|
|
1428
|
+
\`\`\`
|
|
1429
|
+
|
|
1430
|
+
When you learn something important:
|
|
1431
|
+
\`\`\`bash
|
|
1432
|
+
hippo remember "<lesson>"
|
|
1433
|
+
\`\`\`
|
|
1434
|
+
|
|
1435
|
+
When you hit an error or discover a gotcha:
|
|
1436
|
+
\`\`\`bash
|
|
1437
|
+
hippo remember "<what went wrong and why>" --error
|
|
1438
|
+
\`\`\`
|
|
1439
|
+
|
|
1440
|
+
After significant discussions or decisions, capture context:
|
|
1441
|
+
\`\`\`bash
|
|
1442
|
+
hippo capture --stdin <<< 'summary of what was decided'
|
|
1443
|
+
\`\`\`
|
|
1444
|
+
|
|
1445
|
+
After completing work successfully:
|
|
1446
|
+
\`\`\`bash
|
|
1447
|
+
hippo outcome --good
|
|
1448
|
+
\`\`\`
|
|
1166
1449
|
`.trim(),
|
|
1167
1450
|
},
|
|
1168
1451
|
'codex': {
|
|
1169
1452
|
file: 'AGENTS.md',
|
|
1170
1453
|
description: 'OpenAI Codex',
|
|
1171
|
-
content: `
|
|
1172
|
-
## Project Memory (Hippo)
|
|
1173
|
-
|
|
1174
|
-
At the start of every task, run:
|
|
1175
|
-
\`\`\`bash
|
|
1176
|
-
hippo context --auto --budget 1500
|
|
1177
|
-
\`\`\`
|
|
1178
|
-
Read the output before writing any code.
|
|
1179
|
-
|
|
1180
|
-
On errors or unexpected behaviour:
|
|
1181
|
-
\`\`\`bash
|
|
1182
|
-
hippo remember "<description of what went wrong>" --error
|
|
1183
|
-
\`\`\`
|
|
1184
|
-
|
|
1185
|
-
On task completion:
|
|
1186
|
-
\`\`\`bash
|
|
1187
|
-
hippo outcome --good
|
|
1188
|
-
\`\`\`
|
|
1454
|
+
content: `
|
|
1455
|
+
## Project Memory (Hippo)
|
|
1456
|
+
|
|
1457
|
+
At the start of every task, run:
|
|
1458
|
+
\`\`\`bash
|
|
1459
|
+
hippo context --auto --budget 1500
|
|
1460
|
+
\`\`\`
|
|
1461
|
+
Read the output before writing any code.
|
|
1462
|
+
|
|
1463
|
+
On errors or unexpected behaviour:
|
|
1464
|
+
\`\`\`bash
|
|
1465
|
+
hippo remember "<description of what went wrong>" --error
|
|
1466
|
+
\`\`\`
|
|
1467
|
+
|
|
1468
|
+
On task completion:
|
|
1469
|
+
\`\`\`bash
|
|
1470
|
+
hippo outcome --good
|
|
1471
|
+
\`\`\`
|
|
1189
1472
|
`.trim(),
|
|
1190
1473
|
},
|
|
1191
1474
|
'cursor': {
|
|
1192
1475
|
file: '.cursorrules',
|
|
1193
1476
|
description: 'Cursor',
|
|
1194
|
-
content: `
|
|
1195
|
-
# Project Memory (Hippo)
|
|
1196
|
-
# Before each task, load context:
|
|
1197
|
-
# hippo context --auto --budget 1500
|
|
1198
|
-
# After errors:
|
|
1199
|
-
# hippo remember "<error description>" --error
|
|
1200
|
-
# After completing:
|
|
1201
|
-
# hippo outcome --good
|
|
1477
|
+
content: `
|
|
1478
|
+
# Project Memory (Hippo)
|
|
1479
|
+
# Before each task, load context:
|
|
1480
|
+
# hippo context --auto --budget 1500
|
|
1481
|
+
# After errors:
|
|
1482
|
+
# hippo remember "<error description>" --error
|
|
1483
|
+
# After completing:
|
|
1484
|
+
# hippo outcome --good
|
|
1202
1485
|
`.trim(),
|
|
1203
1486
|
},
|
|
1204
1487
|
'openclaw': {
|
|
1205
1488
|
file: 'AGENTS.md',
|
|
1206
1489
|
description: 'OpenClaw',
|
|
1207
|
-
content: `
|
|
1208
|
-
## Project Memory (Hippo)
|
|
1209
|
-
|
|
1210
|
-
At the start of every session, run:
|
|
1211
|
-
\`\`\`bash
|
|
1212
|
-
hippo context --auto --budget 1500
|
|
1213
|
-
\`\`\`
|
|
1214
|
-
Read the output before writing any code.
|
|
1215
|
-
|
|
1216
|
-
On errors or unexpected behaviour:
|
|
1217
|
-
\`\`\`bash
|
|
1218
|
-
hippo remember "<description of what went wrong>" --error
|
|
1219
|
-
\`\`\`
|
|
1220
|
-
|
|
1221
|
-
On task completion:
|
|
1222
|
-
\`\`\`bash
|
|
1223
|
-
hippo outcome --good
|
|
1224
|
-
\`\`\`
|
|
1225
|
-
|
|
1226
|
-
After significant coding sessions:
|
|
1227
|
-
\`\`\`bash
|
|
1228
|
-
hippo learn --git
|
|
1229
|
-
\`\`\`
|
|
1490
|
+
content: `
|
|
1491
|
+
## Project Memory (Hippo)
|
|
1492
|
+
|
|
1493
|
+
At the start of every session, run:
|
|
1494
|
+
\`\`\`bash
|
|
1495
|
+
hippo context --auto --budget 1500
|
|
1496
|
+
\`\`\`
|
|
1497
|
+
Read the output before writing any code.
|
|
1498
|
+
|
|
1499
|
+
On errors or unexpected behaviour:
|
|
1500
|
+
\`\`\`bash
|
|
1501
|
+
hippo remember "<description of what went wrong>" --error
|
|
1502
|
+
\`\`\`
|
|
1503
|
+
|
|
1504
|
+
On task completion:
|
|
1505
|
+
\`\`\`bash
|
|
1506
|
+
hippo outcome --good
|
|
1507
|
+
\`\`\`
|
|
1508
|
+
|
|
1509
|
+
After significant coding sessions:
|
|
1510
|
+
\`\`\`bash
|
|
1511
|
+
hippo learn --git
|
|
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
|
+
\`\`\`
|
|
1230
1541
|
`.trim(),
|
|
1231
1542
|
},
|
|
1232
1543
|
};
|
|
@@ -1276,6 +1587,12 @@ function cmdHook(args, flags) {
|
|
|
1276
1587
|
fs.writeFileSync(filepath, block + '\n', 'utf8');
|
|
1277
1588
|
console.log(`Created ${hook.file} with Hippo hook`);
|
|
1278
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
|
+
}
|
|
1279
1596
|
return;
|
|
1280
1597
|
}
|
|
1281
1598
|
if (subcommand === 'uninstall') {
|
|
@@ -1298,6 +1615,12 @@ function cmdHook(args, flags) {
|
|
|
1298
1615
|
const cleaned = existing.replace(re, '\n').replace(/\n{3,}/g, '\n\n').trim();
|
|
1299
1616
|
fs.writeFileSync(filepath, cleaned + '\n', 'utf8');
|
|
1300
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
|
+
}
|
|
1301
1624
|
return;
|
|
1302
1625
|
}
|
|
1303
1626
|
console.error('Usage: hippo hook <install|uninstall|list> [target]');
|
|
@@ -1306,126 +1629,334 @@ function cmdHook(args, flags) {
|
|
|
1306
1629
|
function escapeRegex(s) {
|
|
1307
1630
|
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1308
1631
|
}
|
|
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
|
+
// ---------------------------------------------------------------------------
|
|
1721
|
+
// Working Memory
|
|
1722
|
+
// ---------------------------------------------------------------------------
|
|
1723
|
+
function cmdWm(hippoRoot, args, flags) {
|
|
1724
|
+
requireInit(hippoRoot);
|
|
1725
|
+
const subcommand = args[0] ?? '';
|
|
1726
|
+
if (subcommand === 'push') {
|
|
1727
|
+
const scope = String(flags['scope'] ?? 'default').trim();
|
|
1728
|
+
const content = String(flags['content'] ?? '').trim();
|
|
1729
|
+
const importance = parseFloat(String(flags['importance'] ?? '0.5'));
|
|
1730
|
+
const sessionId = flags['session'] ? String(flags['session']).trim() : undefined;
|
|
1731
|
+
const taskId = flags['task'] ? String(flags['task']).trim() : undefined;
|
|
1732
|
+
if (!content) {
|
|
1733
|
+
console.error('Usage: hippo wm push --scope <scope> --content "..." [--importance 0.8] [--session <id>] [--task <id>]');
|
|
1734
|
+
process.exit(1);
|
|
1735
|
+
}
|
|
1736
|
+
const id = wmPush(hippoRoot, {
|
|
1737
|
+
scope,
|
|
1738
|
+
content,
|
|
1739
|
+
importance: Number.isFinite(importance) ? importance : 0.5,
|
|
1740
|
+
sessionId,
|
|
1741
|
+
taskId,
|
|
1742
|
+
});
|
|
1743
|
+
console.log(`Pushed working memory #${id} (scope=${scope}, importance=${Number.isFinite(importance) ? importance : 0.5})`);
|
|
1744
|
+
return;
|
|
1745
|
+
}
|
|
1746
|
+
if (subcommand === 'read') {
|
|
1747
|
+
const scope = flags['scope'] ? String(flags['scope']).trim() : undefined;
|
|
1748
|
+
const sessionId = flags['session'] ? String(flags['session']).trim() : undefined;
|
|
1749
|
+
const limit = parseInt(String(flags['limit'] ?? '20'), 10) || 20;
|
|
1750
|
+
const items = wmRead(hippoRoot, { scope, sessionId, limit });
|
|
1751
|
+
if (flags['json']) {
|
|
1752
|
+
console.log(JSON.stringify({ items }, null, 2));
|
|
1753
|
+
return;
|
|
1754
|
+
}
|
|
1755
|
+
if (items.length === 0) {
|
|
1756
|
+
console.log('No working memory entries.');
|
|
1757
|
+
return;
|
|
1758
|
+
}
|
|
1759
|
+
console.log(`Working memory (${items.length} entries):\n`);
|
|
1760
|
+
for (const item of items) {
|
|
1761
|
+
const sessionLabel = item.sessionId ? ` session=${item.sessionId}` : '';
|
|
1762
|
+
const taskLabel = item.taskId ? ` task=${item.taskId}` : '';
|
|
1763
|
+
console.log(` #${item.id} [${item.scope}] importance=${item.importance}${sessionLabel}${taskLabel}`);
|
|
1764
|
+
console.log(` ${item.content}`);
|
|
1765
|
+
console.log(` created=${item.createdAt}`);
|
|
1766
|
+
console.log('');
|
|
1767
|
+
}
|
|
1768
|
+
return;
|
|
1769
|
+
}
|
|
1770
|
+
if (subcommand === 'clear') {
|
|
1771
|
+
const scope = flags['scope'] ? String(flags['scope']).trim() : undefined;
|
|
1772
|
+
const sessionId = flags['session'] ? String(flags['session']).trim() : undefined;
|
|
1773
|
+
const count = wmClear(hippoRoot, { scope, sessionId });
|
|
1774
|
+
console.log(`Cleared ${count} working memory entries.`);
|
|
1775
|
+
return;
|
|
1776
|
+
}
|
|
1777
|
+
if (subcommand === 'flush') {
|
|
1778
|
+
const scope = flags['scope'] ? String(flags['scope']).trim() : undefined;
|
|
1779
|
+
const sessionId = flags['session'] ? String(flags['session']).trim() : undefined;
|
|
1780
|
+
const count = wmFlush(hippoRoot, { scope, sessionId });
|
|
1781
|
+
console.log(`Flushed ${count} working memory entries.`);
|
|
1782
|
+
return;
|
|
1783
|
+
}
|
|
1784
|
+
console.error('Usage: hippo wm <push|read|clear|flush>');
|
|
1785
|
+
process.exit(1);
|
|
1786
|
+
}
|
|
1309
1787
|
function printUsage() {
|
|
1310
|
-
console.log(`
|
|
1311
|
-
Hippo - biologically-inspired memory system for AI agents
|
|
1312
|
-
|
|
1313
|
-
Usage: hippo <command> [options]
|
|
1314
|
-
|
|
1315
|
-
Commands:
|
|
1316
|
-
init Create .hippo/ structure in current directory
|
|
1317
|
-
--global Init the global store at ~/.hippo/
|
|
1318
|
-
--no-hooks Skip auto-detecting and installing agent hooks
|
|
1319
|
-
--no-schedule Skip auto-creating daily learn+sleep cron job
|
|
1320
|
-
remember <text> Store a memory
|
|
1321
|
-
--tag <tag> Add a tag (repeatable)
|
|
1322
|
-
--error Tag as error (boosts retention)
|
|
1323
|
-
--pin Pin memory (never decays)
|
|
1324
|
-
--verified Set confidence: verified (default)
|
|
1325
|
-
--observed Set confidence: observed
|
|
1326
|
-
--inferred Set confidence: inferred
|
|
1327
|
-
--global Store in global ~/.hippo/ store
|
|
1328
|
-
recall <query> Search and retrieve memories (local + global)
|
|
1329
|
-
--budget <n> Token budget (default: 4000)
|
|
1330
|
-
--json Output as JSON
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
--
|
|
1334
|
-
--
|
|
1335
|
-
--
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
--
|
|
1342
|
-
--
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
--
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
--
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
--
|
|
1353
|
-
--
|
|
1354
|
-
--
|
|
1355
|
-
--
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
--
|
|
1364
|
-
--
|
|
1365
|
-
--
|
|
1366
|
-
--
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
--
|
|
1370
|
-
--
|
|
1371
|
-
--
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
--
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
--
|
|
1400
|
-
--
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
--
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1788
|
+
console.log(`
|
|
1789
|
+
Hippo - biologically-inspired memory system for AI agents
|
|
1790
|
+
|
|
1791
|
+
Usage: hippo <command> [options]
|
|
1792
|
+
|
|
1793
|
+
Commands:
|
|
1794
|
+
init Create .hippo/ structure in current directory
|
|
1795
|
+
--global Init the global store at ~/.hippo/
|
|
1796
|
+
--no-hooks Skip auto-detecting and installing agent hooks
|
|
1797
|
+
--no-schedule Skip auto-creating daily learn+sleep cron job
|
|
1798
|
+
remember <text> Store a memory
|
|
1799
|
+
--tag <tag> Add a tag (repeatable)
|
|
1800
|
+
--error Tag as error (boosts retention)
|
|
1801
|
+
--pin Pin memory (never decays)
|
|
1802
|
+
--verified Set confidence: verified (default)
|
|
1803
|
+
--observed Set confidence: observed
|
|
1804
|
+
--inferred Set confidence: inferred
|
|
1805
|
+
--global Store in global ~/.hippo/ store
|
|
1806
|
+
recall <query> Search and retrieve memories (local + global)
|
|
1807
|
+
--budget <n> Token budget (default: 4000)
|
|
1808
|
+
--json Output as JSON
|
|
1809
|
+
--why Show match reasons and source annotations
|
|
1810
|
+
context Smart context injection for AI agents
|
|
1811
|
+
--auto Auto-detect task from git state
|
|
1812
|
+
--budget <n> Token budget (default: 1500)
|
|
1813
|
+
--format <fmt> Output format: markdown (default) or json
|
|
1814
|
+
--framing <mode> Framing: observe (default), suggest, assert
|
|
1815
|
+
sleep Run consolidation pass
|
|
1816
|
+
--dry-run Preview without writing
|
|
1817
|
+
status Show memory health stats
|
|
1818
|
+
outcome Apply feedback to last recall
|
|
1819
|
+
--good Memories were helpful
|
|
1820
|
+
--bad Memories were irrelevant
|
|
1821
|
+
--id <id> Target a specific memory
|
|
1822
|
+
conflicts List detected open memory conflicts
|
|
1823
|
+
--status <status> Filter by status (default: open)
|
|
1824
|
+
--json Output as JSON
|
|
1825
|
+
resolve <conflict_id> Resolve a memory conflict
|
|
1826
|
+
--keep <memory_id> Memory to keep (required)
|
|
1827
|
+
--forget Delete the losing memory (default: halve half-life)
|
|
1828
|
+
snapshot <sub> Persist or inspect the current active task
|
|
1829
|
+
snapshot save Save active task state
|
|
1830
|
+
--task <task>
|
|
1831
|
+
--summary <summary>
|
|
1832
|
+
--next-step <step>
|
|
1833
|
+
--source <source> Optional source label
|
|
1834
|
+
--session <id> Link snapshot to a session trail
|
|
1835
|
+
snapshot show Show the active task snapshot
|
|
1836
|
+
--json Output as JSON
|
|
1837
|
+
snapshot clear Clear the active task snapshot
|
|
1838
|
+
--status <status> Mark final status (default: cleared)
|
|
1839
|
+
session <sub> Append or inspect short-term session history
|
|
1840
|
+
session log Append a structured session event
|
|
1841
|
+
--id <session-id>
|
|
1842
|
+
--content <text>
|
|
1843
|
+
--type <type> Event type (default: note)
|
|
1844
|
+
--task <task> Optional task label
|
|
1845
|
+
--source <source> Optional source label
|
|
1846
|
+
session show Show recent events for a session or task
|
|
1847
|
+
--id <session-id>
|
|
1848
|
+
--task <task>
|
|
1849
|
+
--limit <n> Event limit (default: 8)
|
|
1850
|
+
--json Output as JSON
|
|
1851
|
+
session latest Show latest task snapshot + events
|
|
1852
|
+
--id <session-id> Filter by session
|
|
1853
|
+
--json Output as JSON
|
|
1854
|
+
session resume Re-inject latest handoff as context output
|
|
1855
|
+
--id <session-id> Filter by session
|
|
1856
|
+
handoff <sub> Manage session handoffs for continuity
|
|
1857
|
+
handoff create Create a new session handoff
|
|
1858
|
+
--summary <text> Handoff summary (required)
|
|
1859
|
+
--next <text> Next action for successor
|
|
1860
|
+
--session <id> Session ID (auto-generated if omitted)
|
|
1861
|
+
--task <id> Associated task ID
|
|
1862
|
+
--artifact <path> Related file path (repeatable)
|
|
1863
|
+
handoff latest Show the most recent handoff
|
|
1864
|
+
--session <id> Filter by session
|
|
1865
|
+
--json Output as JSON
|
|
1866
|
+
handoff show <id> Show a specific handoff by ID
|
|
1867
|
+
current <sub> Show compact current state for agent injection
|
|
1868
|
+
current show Active task + recent session events (default)
|
|
1869
|
+
--json Output as JSON
|
|
1870
|
+
forget <id> Force remove a memory
|
|
1871
|
+
inspect <id> Show full memory detail
|
|
1872
|
+
embed Embed all memories for semantic search
|
|
1873
|
+
--status Show embedding coverage
|
|
1874
|
+
watch "<command>" Run command, auto-learn from failures
|
|
1875
|
+
learn Learn lessons from repository history
|
|
1876
|
+
--git Scan recent git commits for lessons
|
|
1877
|
+
--days <n> Scan this many days back (default: 7)
|
|
1878
|
+
--repos <paths> Comma-separated repo paths to scan
|
|
1879
|
+
promote <id> Copy a local memory to the global store
|
|
1880
|
+
share <id> Share a memory with attribution + transfer scoring
|
|
1881
|
+
--force Share even if transfer score is low
|
|
1882
|
+
--auto Auto-share all high-transfer-score memories
|
|
1883
|
+
--dry-run Preview what would be shared
|
|
1884
|
+
--min-score <n> Minimum transfer score (default: 0.6)
|
|
1885
|
+
peers List projects contributing to global store
|
|
1886
|
+
sync Pull global memories into local project
|
|
1887
|
+
import Import memories from other AI tools
|
|
1888
|
+
--chatgpt <path> Import from ChatGPT memory export (JSON or txt)
|
|
1889
|
+
--claude <path> Import from CLAUDE.md or Claude memory.json
|
|
1890
|
+
--cursor <path> Import from .cursorrules or .cursor/rules
|
|
1891
|
+
--file <path> Import from any markdown or text file
|
|
1892
|
+
--markdown <path> Import from structured MEMORY.md / AGENTS.md
|
|
1893
|
+
--dry-run Preview without writing
|
|
1894
|
+
--global Write to global store (~/.hippo/)
|
|
1895
|
+
--tag <tag> Add extra tag (repeatable)
|
|
1896
|
+
export [file] Export all memories (default: stdout)
|
|
1897
|
+
--format <fmt> Output format: json (default) or markdown
|
|
1898
|
+
capture Extract memories from conversation text
|
|
1899
|
+
--stdin Read from piped input
|
|
1900
|
+
--file <path> Read from a file
|
|
1901
|
+
--last-session (placeholder) Read from agent session logs
|
|
1902
|
+
--dry-run Preview without writing
|
|
1903
|
+
--global Write to global store (~/.hippo/)
|
|
1904
|
+
hook <sub> [target] Manage framework integrations
|
|
1905
|
+
hook list Show available hooks
|
|
1906
|
+
hook install <target> Install hook (claude-code|codex|cursor|openclaw|opencode)
|
|
1907
|
+
claude-code also installs Stop hook (hippo sleep on exit)
|
|
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
|
|
1914
|
+
wm <sub> Working memory — bounded buffer for current state
|
|
1915
|
+
wm push Push a working memory entry
|
|
1916
|
+
--scope <scope> Scope name (default: default)
|
|
1917
|
+
--content <text> Content to store (required)
|
|
1918
|
+
--importance <n> Priority 0-1 (default: 0.5)
|
|
1919
|
+
--session <id> Session ID
|
|
1920
|
+
--task <id> Task ID
|
|
1921
|
+
wm read Read working memory entries
|
|
1922
|
+
--scope <scope> Filter by scope
|
|
1923
|
+
--session <id> Filter by session
|
|
1924
|
+
--limit <n> Max entries (default: 20)
|
|
1925
|
+
--json Output as JSON
|
|
1926
|
+
wm clear Clear working memory entries
|
|
1927
|
+
--scope <scope> Filter by scope
|
|
1928
|
+
--session <id> Filter by session
|
|
1929
|
+
wm flush Flush working memory (session end)
|
|
1930
|
+
--scope <scope> Filter by scope
|
|
1931
|
+
--session <id> Filter by session
|
|
1932
|
+
dashboard Open web dashboard for memory health
|
|
1933
|
+
--port <n> Port to serve on (default: 3333)
|
|
1934
|
+
mcp Start MCP server (stdio transport)
|
|
1935
|
+
|
|
1936
|
+
Examples:
|
|
1937
|
+
hippo init
|
|
1938
|
+
hippo remember "FRED cache can silently drop series" --tag error
|
|
1939
|
+
hippo recall "data pipeline issues" --budget 2000
|
|
1940
|
+
hippo context --auto --budget 1500
|
|
1941
|
+
hippo conflicts
|
|
1942
|
+
hippo session log --id sess_123 --task "Ship feature" --type progress --content "Build is green, next step is docs"
|
|
1943
|
+
hippo session latest --json
|
|
1944
|
+
hippo session resume
|
|
1945
|
+
hippo snapshot save --task "Ship feature" --summary "Tests are green" --next-step "Open the PR" --session sess_123
|
|
1946
|
+
hippo handoff create --summary "PR is open, tests green" --next "Merge after review" --session sess_123 --artifact src/foo.ts
|
|
1947
|
+
hippo embed --status
|
|
1948
|
+
hippo watch "npm run build"
|
|
1949
|
+
hippo learn --git --days 30
|
|
1950
|
+
hippo promote mem_abc123
|
|
1951
|
+
hippo sync
|
|
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
|
|
1957
|
+
hippo sleep --dry-run
|
|
1958
|
+
hippo outcome --good
|
|
1959
|
+
hippo status
|
|
1429
1960
|
`);
|
|
1430
1961
|
}
|
|
1431
1962
|
// ---------------------------------------------------------------------------
|
|
@@ -1477,6 +2008,12 @@ async function main() {
|
|
|
1477
2008
|
case 'session':
|
|
1478
2009
|
cmdSession(hippoRoot, args, flags);
|
|
1479
2010
|
break;
|
|
2011
|
+
case 'handoff':
|
|
2012
|
+
cmdHandoff(hippoRoot, args, flags);
|
|
2013
|
+
break;
|
|
2014
|
+
case 'current':
|
|
2015
|
+
cmdCurrent(hippoRoot, args, flags);
|
|
2016
|
+
break;
|
|
1480
2017
|
case 'forget': {
|
|
1481
2018
|
const id = args[0];
|
|
1482
2019
|
if (!id) {
|
|
@@ -1592,6 +2129,37 @@ async function main() {
|
|
|
1592
2129
|
case 'import':
|
|
1593
2130
|
cmdImport(hippoRoot, args, flags);
|
|
1594
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
|
+
}
|
|
1595
2163
|
case 'capture': {
|
|
1596
2164
|
let captureSource = null;
|
|
1597
2165
|
let captureFile;
|
|
@@ -1625,12 +2193,84 @@ async function main() {
|
|
|
1625
2193
|
await new Promise(() => { }); // run until Ctrl+C
|
|
1626
2194
|
break;
|
|
1627
2195
|
}
|
|
2196
|
+
case 'wm':
|
|
2197
|
+
cmdWm(hippoRoot, args, flags);
|
|
2198
|
+
break;
|
|
1628
2199
|
case 'mcp':
|
|
1629
2200
|
// Start MCP server over stdio - dynamically import to keep main CLI lean
|
|
1630
2201
|
await import('./mcp/server.js');
|
|
1631
2202
|
// Server runs until stdin closes, so we never reach here
|
|
1632
2203
|
await new Promise(() => { }); // hang forever
|
|
1633
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
|
+
}
|
|
1634
2274
|
case 'help':
|
|
1635
2275
|
case '--help':
|
|
1636
2276
|
case '-h':
|