hippo-memory 1.27.0 → 1.29.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 +4 -2
- package/dist/api.d.ts +25 -1
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +103 -4
- package/dist/api.js.map +1 -1
- package/dist/capture.d.ts +49 -2
- package/dist/capture.d.ts.map +1 -1
- package/dist/capture.js +464 -1
- package/dist/capture.js.map +1 -1
- package/dist/cli.d.ts +5 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +266 -21
- package/dist/cli.js.map +1 -1
- package/dist/dag.d.ts.map +1 -1
- package/dist/dag.js +10 -1
- package/dist/dag.js.map +1 -1
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +75 -1
- package/dist/db.js.map +1 -1
- package/dist/embedding-provider.d.ts +1 -1
- package/dist/embedding-provider.js +1 -1
- package/dist/embeddings.d.ts +2 -2
- package/dist/embeddings.d.ts.map +1 -1
- package/dist/embeddings.js +38 -37
- package/dist/embeddings.js.map +1 -1
- package/dist/hooks.d.ts +24 -0
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +85 -1
- package/dist/hooks.js.map +1 -1
- package/dist/invalidation.d.ts +20 -2
- package/dist/invalidation.d.ts.map +1 -1
- package/dist/invalidation.js +50 -18
- package/dist/invalidation.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +17 -4
- package/dist/mcp/server.js.map +1 -1
- package/dist/postinstall.js +35 -2
- package/dist/postinstall.js.map +1 -1
- package/dist/recall-trace.d.ts +117 -0
- package/dist/recall-trace.d.ts.map +1 -0
- package/dist/recall-trace.js +179 -0
- package/dist/recall-trace.js.map +1 -0
- package/dist/rerankers/cross-encoder.d.ts +1 -1
- package/dist/rerankers/cross-encoder.d.ts.map +1 -1
- package/dist/rerankers/cross-encoder.js +38 -10
- package/dist/rerankers/cross-encoder.js.map +1 -1
- package/dist/secret-detect.d.ts +8 -0
- package/dist/secret-detect.d.ts.map +1 -1
- package/dist/secret-detect.js +25 -0
- package/dist/secret-detect.js.map +1 -1
- package/dist/server.js +1 -1
- package/dist/server.js.map +1 -1
- package/dist/src/api.js +103 -4
- package/dist/src/api.js.map +1 -1
- package/dist/src/capture.js +464 -1
- package/dist/src/capture.js.map +1 -1
- package/dist/src/cli.js +266 -21
- package/dist/src/cli.js.map +1 -1
- package/dist/src/dag.js +10 -1
- package/dist/src/dag.js.map +1 -1
- package/dist/src/db.js +75 -1
- package/dist/src/db.js.map +1 -1
- package/dist/src/embedding-provider.js +1 -1
- package/dist/src/embeddings.js +38 -37
- package/dist/src/embeddings.js.map +1 -1
- package/dist/src/hooks.js +85 -1
- package/dist/src/hooks.js.map +1 -1
- package/dist/src/invalidation.js +50 -18
- package/dist/src/invalidation.js.map +1 -1
- package/dist/src/mcp/server.js +17 -4
- package/dist/src/mcp/server.js.map +1 -1
- package/dist/src/postinstall.js +35 -2
- package/dist/src/postinstall.js.map +1 -1
- package/dist/src/recall-trace.js +179 -0
- package/dist/src/recall-trace.js.map +1 -0
- package/dist/src/rerankers/cross-encoder.js +38 -10
- package/dist/src/rerankers/cross-encoder.js.map +1 -1
- package/dist/src/secret-detect.js +25 -0
- package/dist/src/secret-detect.js.map +1 -1
- package/dist/src/server.js +1 -1
- package/dist/src/server.js.map +1 -1
- package/dist/src/store.js +55 -4
- package/dist/src/store.js.map +1 -1
- package/dist/src/version.js +7 -6
- package/dist/src/version.js.map +1 -1
- package/dist/store.d.ts +15 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +55 -4
- package/dist/store.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +7 -6
- package/dist/version.js.map +1 -1
- package/dist-ui/assets/{index-DbMxaBZ3.js → index-Cxa4RcAh.js} +2 -2
- package/dist-ui/assets/{react-DS1lvn1H.js → react-CEUiQZj-.js} +2 -2
- package/dist-ui/index.html +2 -2
- 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 +8 -5
package/dist/cli.js
CHANGED
|
@@ -31,7 +31,7 @@ import * as fs from 'fs';
|
|
|
31
31
|
import * as os from 'os';
|
|
32
32
|
import { fileURLToPath } from 'node:url';
|
|
33
33
|
import { execFileSync, execSync, spawn } from 'child_process';
|
|
34
|
-
import { installJsonHooks, uninstallJsonHooks, resolveJsonHookPaths, detectInstalledTools, defaultSleepLogPath, ensureCodexWrapperInstalled, installCodexWrapper, uninstallCodexWrapper, resolveCodexSessionTranscript, resolveCodexWrapperPaths, installOpencodePlugin, uninstallOpencodePlugin, resolveOpencodePluginPath, } from './hooks.js';
|
|
34
|
+
import { installJsonHooks, uninstallJsonHooks, resolveJsonHookPaths, detectInstalledTools, defaultSleepLogPath, ensureCodexWrapperInstalled, installCodexWrapper, isCodexWrapperInstalled, repairCodexWrapperIfInstalled, uninstallCodexWrapper, resolveCodexSessionTranscript, resolveCodexWrapperPaths, installOpencodePlugin, uninstallOpencodePlugin, resolveOpencodePluginPath, } from './hooks.js';
|
|
35
35
|
import { createMemory, calculateStrength, calculateRewardFactor, deriveHalfLife, resolveConfidence, computeSchemaFit, Layer, } from './memory.js';
|
|
36
36
|
import { resolveProjectIdentity } from './project-identity.js';
|
|
37
37
|
import { detectSecret } from './secret-detect.js';
|
|
@@ -39,6 +39,7 @@ import { getHippoRoot, isInitialized, initStore, writeEntry, readEntry, deleteEn
|
|
|
39
39
|
import { markRetrieved, hybridSearch, physicsSearch, explainMatch, textOverlap, tokenize as tokenizeQuery } from './search.js';
|
|
40
40
|
import { compareEntryIdentity } from './compare.js';
|
|
41
41
|
import { renderTraceContent, parseSteps } from './trace.js';
|
|
42
|
+
import { writeRecallTraceAtRoot } from './recall-trace.js';
|
|
42
43
|
import { deduplicateStore } from './dedupe.js';
|
|
43
44
|
import { isEmbeddingAvailable, embedAll, embedMemory, loadEmbeddingIndex, resolveEmbeddingModel, embeddingModelRequiresReindex, } from './embeddings.js';
|
|
44
45
|
import { isEmbeddingConfigured, resolveEmbeddingProvider } from './embedding-provider.js';
|
|
@@ -55,7 +56,7 @@ import { detectScope } from './scope.js';
|
|
|
55
56
|
import { getGlobalRoot, initGlobal, shareMemory, listPeers, autoShare, transferScore, searchBothHybrid, syncGlobalToLocal, } from './shared.js';
|
|
56
57
|
import { DAILY_TASK_NAME, buildDailyRunnerCommand, listRegisteredWorkspaces, registerWorkspace, runDailyMaintenance, } from './scheduler.js';
|
|
57
58
|
import { importChatGPT, importClaude, importCursor, importGenericFile, importMarkdown, importVault, } from './importers.js';
|
|
58
|
-
import { cmdCapture } from './capture.js';
|
|
59
|
+
import { cmdCapture, cmdPreCompact, truncateCodePointSafe } from './capture.js';
|
|
59
60
|
import { auditMemories, appendAuditEvent, } from './audit.js';
|
|
60
61
|
import { listApiKeys, revokeApiKey } from './auth.js';
|
|
61
62
|
import { buildProvenanceCoverage } from './provenance-coverage.js';
|
|
@@ -214,7 +215,11 @@ async function runViaServerIfAvailable(hippoRoot, httpFn) {
|
|
|
214
215
|
throw err;
|
|
215
216
|
}
|
|
216
217
|
}
|
|
217
|
-
|
|
218
|
+
// Flags that NEVER take a value. Without this, a positional following the
|
|
219
|
+
// flag is silently swallowed as its value (`invalidate --dry-run "X"` would
|
|
220
|
+
// eat the pattern). Every existing --dry-run consumer reads it as boolean.
|
|
221
|
+
const BOOLEAN_FLAGS = new Set(['dry-run']);
|
|
222
|
+
export function parseArgs(argv) {
|
|
218
223
|
const [, , command = '', ...rest] = argv;
|
|
219
224
|
const args = [];
|
|
220
225
|
const flags = {};
|
|
@@ -228,7 +233,7 @@ function parseArgs(argv) {
|
|
|
228
233
|
if (part.startsWith('--')) {
|
|
229
234
|
const key = part.slice(2);
|
|
230
235
|
const next = rest[i + 1];
|
|
231
|
-
if (!next || next.startsWith('--')) {
|
|
236
|
+
if (!next || next.startsWith('--') || BOOLEAN_FLAGS.has(key)) {
|
|
232
237
|
// Boolean flag
|
|
233
238
|
flags[key] = true;
|
|
234
239
|
i++;
|
|
@@ -437,6 +442,12 @@ function autoInstallHooks(quiet) {
|
|
|
437
442
|
installed.add(targetPath);
|
|
438
443
|
console.log(` Auto-installed ${hook} hook in ${hookDef.file}`);
|
|
439
444
|
}
|
|
445
|
+
// The Codex session-capture wrapper swaps the codex launcher binary, so
|
|
446
|
+
// init never installs it silently — it points at the explicit opt-in
|
|
447
|
+
// command instead (issue #133).
|
|
448
|
+
if (hook === 'codex' && !isCodexWrapperInstalled()) {
|
|
449
|
+
console.log(' Codex detected. To capture Codex sessions: hippo hook install codex');
|
|
450
|
+
}
|
|
440
451
|
// For Claude Code, also install SessionEnd+SessionStart entries in its
|
|
441
452
|
// settings.json. Keeps `hippo init` in lockstep with `hippo hook install
|
|
442
453
|
// claude-code` and `hippo setup`.
|
|
@@ -451,6 +462,12 @@ function autoInstallHooks(quiet) {
|
|
|
451
462
|
if (result.installedUserPromptSubmit) {
|
|
452
463
|
console.log(` Auto-installed hippo pinned-inject UserPromptSubmit hook in ${hook} settings`);
|
|
453
464
|
}
|
|
465
|
+
if (result.installedPreCompact) {
|
|
466
|
+
console.log(` Auto-installed hippo pre-compact PreCompact hook in ${hook} settings`);
|
|
467
|
+
}
|
|
468
|
+
if (result.installedCompactResume) {
|
|
469
|
+
console.log(` Auto-installed hippo compact-resume SessionStart(compact) hook in ${hook} settings`);
|
|
470
|
+
}
|
|
454
471
|
if (result.migratedFromStop) {
|
|
455
472
|
console.log(` Migrated legacy Stop hook → SessionEnd (no longer runs every turn)`);
|
|
456
473
|
}
|
|
@@ -1478,6 +1495,24 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
1478
1495
|
|| sessionHandoff !== null
|
|
1479
1496
|
|| recentSessionEvents.length > 0;
|
|
1480
1497
|
if (results.length === 0) {
|
|
1498
|
+
// LC1 F1 structural fix (docs/plans/2026-08-02-lc1-recall-trace-persistence.md):
|
|
1499
|
+
// trace the zero-result recall too (result_count 0, no result rows) so
|
|
1500
|
+
// a query that reveals a coverage gap still lands in the training
|
|
1501
|
+
// corpus. Deliberately does NOT touch `localIndex` at all — this path
|
|
1502
|
+
// never advances last_retrieval_ids (no memories to update), and
|
|
1503
|
+
// writeRecallTraceAtRoot no longer stamps last_trace_id on its own
|
|
1504
|
+
// (that only happens via the caller folding the returned id into
|
|
1505
|
+
// localIndex before a SAME saveIndex call, which this path never
|
|
1506
|
+
// reaches). By construction the two can't desync. Fail-soft
|
|
1507
|
+
// internally; never throws.
|
|
1508
|
+
writeRecallTraceAtRoot(hippoRoot, {
|
|
1509
|
+
tenantId,
|
|
1510
|
+
sessionId: sessionId || null,
|
|
1511
|
+
pipeline: 'cli',
|
|
1512
|
+
query,
|
|
1513
|
+
explainMode: showWhy,
|
|
1514
|
+
results: [],
|
|
1515
|
+
});
|
|
1481
1516
|
if (asJson) {
|
|
1482
1517
|
const out = {
|
|
1483
1518
|
query,
|
|
@@ -1565,6 +1600,27 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
1565
1600
|
}
|
|
1566
1601
|
// Track last retrieval IDs for outcome command
|
|
1567
1602
|
localIndex.last_retrieval_ids = updated.map((u) => u.id);
|
|
1603
|
+
// LC1 F1 structural fix (docs/plans/2026-08-02-lc1-recall-trace-persistence.md):
|
|
1604
|
+
// ONE trace at hippoRoot (where last_retrieval_ids and outcome
|
|
1605
|
+
// attribution live). Write the trace FIRST, then fold its id into
|
|
1606
|
+
// `localIndex` so the SAME saveIndex call below persists
|
|
1607
|
+
// last_retrieval_ids + last_trace_id atomically (LOCKSTEP INVARIANT —
|
|
1608
|
+
// see writeRecallTraceAtRoot JSDoc). The globalRoot audit emit
|
|
1609
|
+
// (emitCliAudit above) is untouched — no second trace row. Fail-soft
|
|
1610
|
+
// internally; never throws.
|
|
1611
|
+
const traceId = writeRecallTraceAtRoot(hippoRoot, {
|
|
1612
|
+
tenantId,
|
|
1613
|
+
sessionId: sessionId || null,
|
|
1614
|
+
pipeline: 'cli',
|
|
1615
|
+
query,
|
|
1616
|
+
explainMode: showWhy,
|
|
1617
|
+
results: results.map((r) => ({
|
|
1618
|
+
memoryId: r.entry.id,
|
|
1619
|
+
score: r.score,
|
|
1620
|
+
rerankSteps: r.rerankTrace,
|
|
1621
|
+
})),
|
|
1622
|
+
});
|
|
1623
|
+
localIndex.last_trace_id = traceId !== null ? String(traceId) : null;
|
|
1568
1624
|
saveIndex(hippoRoot, localIndex);
|
|
1569
1625
|
updateStats(hippoRoot, { recalled: results.length });
|
|
1570
1626
|
if (asJson) {
|
|
@@ -2595,6 +2651,104 @@ function cmdLastSleep(flags) {
|
|
|
2595
2651
|
catch { /* non-fatal */ }
|
|
2596
2652
|
}
|
|
2597
2653
|
}
|
|
2654
|
+
/**
|
|
2655
|
+
* SessionStart(compact) injector. Prints the active task snapshot + recent
|
|
2656
|
+
* session trail so working state that would otherwise be lost to
|
|
2657
|
+
* compaction summarisation survives into the new context window. No pinned
|
|
2658
|
+
* memories here — the UserPromptSubmit hook already re-injects those every
|
|
2659
|
+
* turn, so duplicating them here would double token cost for nothing.
|
|
2660
|
+
*
|
|
2661
|
+
* Same exit-0/crash-safety contract as `hippo pre-compact` (critic round
|
|
2662
|
+
* 2): every path exits 0. A malformed payload or a store read failure
|
|
2663
|
+
* degrades to empty stdout, never a thrown error — a failing SessionStart
|
|
2664
|
+
* hook must not pollute session startup.
|
|
2665
|
+
*/
|
|
2666
|
+
// X8: session-event content is capped at print time only — the shared
|
|
2667
|
+
// printSessionEvents stays untouched for every other caller.
|
|
2668
|
+
const COMPACT_RESUME_EVENT_CONTENT_CAP = 400;
|
|
2669
|
+
function cmdCompactResume(hippoRoot, stdinText) {
|
|
2670
|
+
try {
|
|
2671
|
+
// X3: gate on the non-exiting isInitialized check before any
|
|
2672
|
+
// store-opening call (loadActiveTaskSnapshot/listSessionEvents both
|
|
2673
|
+
// call initStore internally, which would silently create a store in a
|
|
2674
|
+
// project that never ran `hippo init` — this hook fires globally).
|
|
2675
|
+
if (!isInitialized(hippoRoot)) {
|
|
2676
|
+
process.exit(0);
|
|
2677
|
+
}
|
|
2678
|
+
// The matcher is an optimization, not a dependency: older Claude Code
|
|
2679
|
+
// that ignores `matcher: 'compact'` would run this on every SessionStart,
|
|
2680
|
+
// so we also gate on payload.source here. A payload that parses but
|
|
2681
|
+
// carries a different source (e.g. 'startup') means the matcher-based
|
|
2682
|
+
// gate failed to apply — stay silent rather than print stale state.
|
|
2683
|
+
const nonEmptyStdin = !!stdinText && stdinText.trim() !== '';
|
|
2684
|
+
let suppressOutput = false;
|
|
2685
|
+
let payloadSessionId = null;
|
|
2686
|
+
if (nonEmptyStdin) {
|
|
2687
|
+
let payload = null;
|
|
2688
|
+
try {
|
|
2689
|
+
payload = JSON.parse(stdinText.trim());
|
|
2690
|
+
}
|
|
2691
|
+
catch {
|
|
2692
|
+
payload = null;
|
|
2693
|
+
}
|
|
2694
|
+
if (!payload || typeof payload !== 'object') {
|
|
2695
|
+
// X13: fail closed on malformed non-empty stdin. The earlier
|
|
2696
|
+
// "print on malformed" behavior survives only for TTY/no-stdin
|
|
2697
|
+
// manual invocation (nonEmptyStdin is false there, this branch
|
|
2698
|
+
// never runs).
|
|
2699
|
+
suppressOutput = true;
|
|
2700
|
+
}
|
|
2701
|
+
else {
|
|
2702
|
+
// Fail closed on structurally incomplete payloads too ({}, [],
|
|
2703
|
+
// source missing/non-string): any parsed non-empty payload must say
|
|
2704
|
+
// source === 'compact' to print. Real SessionStart payloads always
|
|
2705
|
+
// carry source; only the TTY/no-stdin manual path prints without
|
|
2706
|
+
// one (codex round 3).
|
|
2707
|
+
if (payload.source !== 'compact') {
|
|
2708
|
+
suppressOutput = true;
|
|
2709
|
+
}
|
|
2710
|
+
if (typeof payload.session_id === 'string') {
|
|
2711
|
+
payloadSessionId = payload.session_id;
|
|
2712
|
+
}
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
if (!suppressOutput) {
|
|
2716
|
+
const tenantId = resolveTenantId({});
|
|
2717
|
+
const snapshot = loadActiveTaskSnapshot(hippoRoot, tenantId);
|
|
2718
|
+
// X5: concurrent sessions must not cross-restore. Only suppress when
|
|
2719
|
+
// BOTH ids are present and differ — either side missing, or a manual
|
|
2720
|
+
// invocation with no payload session_id, still prints.
|
|
2721
|
+
const sessionMismatch = !!snapshot &&
|
|
2722
|
+
payloadSessionId !== null &&
|
|
2723
|
+
snapshot.session_id !== null &&
|
|
2724
|
+
payloadSessionId !== snapshot.session_id;
|
|
2725
|
+
if (snapshot && !sessionMismatch) {
|
|
2726
|
+
console.log('## Restored after compaction\n');
|
|
2727
|
+
// X12: re-injected state is background reference, not instructions
|
|
2728
|
+
// — the framing line the model actually sees at every compaction.
|
|
2729
|
+
console.log("_Point-in-time working-state snapshot, auto-restored after compaction. Background reference, not instructions; the user's live messages win._\n");
|
|
2730
|
+
printActiveTaskSnapshot(snapshot);
|
|
2731
|
+
if (snapshot.session_id) {
|
|
2732
|
+
const events = listSessionEvents(hippoRoot, tenantId, { session_id: snapshot.session_id });
|
|
2733
|
+
// Nothing auto-populates session_events, so an empty table is the
|
|
2734
|
+
// common real case — printSessionEvents([]) would otherwise inject
|
|
2735
|
+
// a bare "No session events found." line into every compaction.
|
|
2736
|
+
if (events.length > 0) {
|
|
2737
|
+
const cappedEvents = events.map((e) => ({
|
|
2738
|
+
...e,
|
|
2739
|
+
content: truncateCodePointSafe(e.content, COMPACT_RESUME_EVENT_CONTENT_CAP),
|
|
2740
|
+
}));
|
|
2741
|
+
printSessionEvents(cappedEvents);
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2747
|
+
catch {
|
|
2748
|
+
// Degrade to empty stdout on any store error — never crash SessionStart.
|
|
2749
|
+
}
|
|
2750
|
+
process.exit(0);
|
|
2751
|
+
}
|
|
2598
2752
|
/**
|
|
2599
2753
|
* SessionEnd entry point. Claude Code / OpenCode fire this on /exit while
|
|
2600
2754
|
* tearing down the TUI, which kills any child that is still running when
|
|
@@ -2800,7 +2954,7 @@ function cmdCodexSessionEndWorker(hippoRoot, flags) {
|
|
|
2800
2954
|
// capture path logs its own failures
|
|
2801
2955
|
}
|
|
2802
2956
|
}
|
|
2803
|
-
function
|
|
2957
|
+
function shouldAutoRepairCodexWrapper(currentCommand, currentArgs) {
|
|
2804
2958
|
if (process.env.HIPPO_SKIP_AUTO_INTEGRATIONS === '1')
|
|
2805
2959
|
return false;
|
|
2806
2960
|
if (!['context', 'remember', 'recall', 'sleep', 'capture', 'outcome', 'status', 'init'].includes(currentCommand)) {
|
|
@@ -2810,11 +2964,16 @@ function shouldAutoInstallCodexWrapper(currentCommand, currentArgs) {
|
|
|
2810
2964
|
return false;
|
|
2811
2965
|
return true;
|
|
2812
2966
|
}
|
|
2813
|
-
|
|
2814
|
-
|
|
2967
|
+
// Repair-only: keeps the wrapper healthy for users who opted in via `hippo
|
|
2968
|
+
// hook install codex` (a Codex update can restore the real binary over our
|
|
2969
|
+
// shim). Never first-installs — silently swapping the codex binary on routine
|
|
2970
|
+
// commands is a consent violation and reads as binary hijacking to
|
|
2971
|
+
// supply-chain scanners (issue #133).
|
|
2972
|
+
function maybeRepairCodexWrapper(currentCommand, currentArgs) {
|
|
2973
|
+
if (!shouldAutoRepairCodexWrapper(currentCommand, currentArgs))
|
|
2815
2974
|
return;
|
|
2816
2975
|
try {
|
|
2817
|
-
|
|
2976
|
+
repairCodexWrapperIfInstalled();
|
|
2818
2977
|
}
|
|
2819
2978
|
catch {
|
|
2820
2979
|
// best-effort only
|
|
@@ -5301,8 +5460,8 @@ async function cmdEmbed(hippoRoot, flags) {
|
|
|
5301
5460
|
return;
|
|
5302
5461
|
}
|
|
5303
5462
|
if (embedProvider.kind === 'local') {
|
|
5304
|
-
console.log('Embeddings not available. Install @
|
|
5305
|
-
console.log(' npm install @
|
|
5463
|
+
console.log('Embeddings not available. Install @huggingface/transformers to enable:');
|
|
5464
|
+
console.log(' npm install @huggingface/transformers');
|
|
5306
5465
|
}
|
|
5307
5466
|
else {
|
|
5308
5467
|
console.error(`Embedding provider '${embedProvider.kind}' is configured but ${embedProvider.keyEnv} is not set.`);
|
|
@@ -5879,6 +6038,12 @@ function cmdHook(args, flags) {
|
|
|
5879
6038
|
if (result.installedUserPromptSubmit) {
|
|
5880
6039
|
console.log(`Installed hippo pinned-inject UserPromptSubmit hook in ${result.target} settings`);
|
|
5881
6040
|
}
|
|
6041
|
+
if (result.installedPreCompact) {
|
|
6042
|
+
console.log(`Installed hippo pre-compact PreCompact hook in ${result.target} settings`);
|
|
6043
|
+
}
|
|
6044
|
+
if (result.installedCompactResume) {
|
|
6045
|
+
console.log(`Installed hippo compact-resume SessionStart(compact) hook in ${result.target} settings`);
|
|
6046
|
+
}
|
|
5882
6047
|
if (result.migratedFromStop) {
|
|
5883
6048
|
console.log(`Migrated legacy Stop hook → SessionEnd (was running every turn; now fires once on session exit)`);
|
|
5884
6049
|
}
|
|
@@ -5995,6 +6160,10 @@ function cmdSetup(flags) {
|
|
|
5995
6160
|
bits.push('SessionStart');
|
|
5996
6161
|
if (result.installedUserPromptSubmit)
|
|
5997
6162
|
bits.push('UserPromptSubmit (pinned-inject)');
|
|
6163
|
+
if (result.installedPreCompact)
|
|
6164
|
+
bits.push('PreCompact (pre-compact)');
|
|
6165
|
+
if (result.installedCompactResume)
|
|
6166
|
+
bits.push('SessionStart(compact) (compact-resume)');
|
|
5998
6167
|
if (result.migratedFromStop)
|
|
5999
6168
|
bits.push('migrated legacy Stop');
|
|
6000
6169
|
if (result.migratedSplitSessionEnd)
|
|
@@ -6115,7 +6284,9 @@ function installClaudeCodeSessionEndHook() {
|
|
|
6115
6284
|
return {
|
|
6116
6285
|
installed: result.installedSessionEnd ||
|
|
6117
6286
|
result.installedSessionStart ||
|
|
6118
|
-
result.installedUserPromptSubmit
|
|
6287
|
+
result.installedUserPromptSubmit ||
|
|
6288
|
+
result.installedPreCompact ||
|
|
6289
|
+
result.installedCompactResume,
|
|
6119
6290
|
migratedFromStop: result.migratedFromStop,
|
|
6120
6291
|
};
|
|
6121
6292
|
}
|
|
@@ -6297,7 +6468,7 @@ function cmdDrillDown(hippoRoot, summaryId, flags) {
|
|
|
6297
6468
|
const rawDepth = typeof flags['depth'] === 'string' ? Number(flags['depth']) : undefined;
|
|
6298
6469
|
let depth;
|
|
6299
6470
|
if (rawDepth !== undefined) {
|
|
6300
|
-
if (!Number.
|
|
6471
|
+
if (!Number.isInteger(rawDepth) || rawDepth < 1 || rawDepth > 10) {
|
|
6301
6472
|
console.error(`--depth must be an integer between 1 and 10 (got ${flags['depth']})`);
|
|
6302
6473
|
process.exit(2);
|
|
6303
6474
|
}
|
|
@@ -7318,18 +7489,23 @@ Commands:
|
|
|
7318
7489
|
--dry-run Preview without writing
|
|
7319
7490
|
--global Write to global store ($HIPPO_HOME or ~/.hippo/)
|
|
7320
7491
|
setup One-shot: detect installed AI tools and install all
|
|
7321
|
-
available SessionEnd+SessionStart hooks
|
|
7492
|
+
available SessionEnd+SessionStart+PreCompact hooks
|
|
7322
7493
|
--all Install for every JSON-hook tool, even if not detected
|
|
7323
7494
|
--dry-run Show what would be installed without writing
|
|
7324
7495
|
--no-schedule Skip installing or repairing the daily runner
|
|
7325
7496
|
last-sleep Print the last 'hippo sleep --log-file' output and clear it
|
|
7326
7497
|
--path <p> Log path (default: ~/.hippo/logs/last-sleep.log)
|
|
7327
7498
|
--keep Print without clearing
|
|
7499
|
+
pre-compact PreCompact hook: snapshot + capture the tail before compaction
|
|
7500
|
+
--log-file <p> Diagnostic log path (default: ~/.hippo/logs/pre-compact.log)
|
|
7501
|
+
compact-resume SessionStart(compact) hook: re-print the snapshot + session trail
|
|
7328
7502
|
codex-run [-- ...args] Launch real Codex behind Hippo's session-end wrapper
|
|
7329
7503
|
hook <sub> [target] Manage framework integrations
|
|
7330
7504
|
hook list Show available hooks
|
|
7331
7505
|
hook install <target> Install hook (claude-code|codex|cursor|openclaw|opencode|pi)
|
|
7332
7506
|
claude-code/opencode install SessionEnd+SessionStart;
|
|
7507
|
+
claude-code also installs PreCompact +
|
|
7508
|
+
SessionStart(compact) for mid-session continuity;
|
|
7333
7509
|
codex wraps the detected launcher in place
|
|
7334
7510
|
hook uninstall <target> Remove hook
|
|
7335
7511
|
decide "<decision>" Record a decision (first-class object + memory mirror)
|
|
@@ -7409,6 +7585,13 @@ Commands:
|
|
|
7409
7585
|
graph extract Rebuild the entity/relation graph from consolidated objects
|
|
7410
7586
|
(decisions/policies/customer-notes/project-briefs); idempotent
|
|
7411
7587
|
invalidate "<pattern>" Actively weaken memories matching an old pattern
|
|
7588
|
+
(content overlap, or a tag EXACTLY equal to the
|
|
7589
|
+
full pattern - never token-level tag matching)
|
|
7590
|
+
--id <memory-id> Invalidate exactly one memory (instead of a pattern)
|
|
7591
|
+
--dry-run Preview what would be hit; writes nothing
|
|
7592
|
+
Note: a pattern equal to the system tag
|
|
7593
|
+
'invalidated' re-weakens previously invalidated
|
|
7594
|
+
memories - preview with --dry-run first
|
|
7412
7595
|
--reason "<why>" Optional: what replaced it
|
|
7413
7596
|
wm <sub> Working memory — bounded buffer for current state
|
|
7414
7597
|
wm push Push a working memory entry
|
|
@@ -7500,7 +7683,9 @@ Examples:
|
|
|
7500
7683
|
hippo note new "Acme Corp" --text "Renewal call: wants SSO before Q3; champion is the VP Eng"
|
|
7501
7684
|
hippo note list --customer "Acme Corp" --status active
|
|
7502
7685
|
hippo graph extract
|
|
7686
|
+
hippo invalidate "REST API" --dry-run
|
|
7503
7687
|
hippo invalidate "REST API" --reason "migrated to GraphQL"
|
|
7688
|
+
hippo invalidate --id mem_a1b2c3d4e5f6 --reason "superseded by new policy"
|
|
7504
7689
|
hippo export memories.json
|
|
7505
7690
|
hippo export --format markdown memories.md
|
|
7506
7691
|
hippo sleep --dry-run
|
|
@@ -7529,7 +7714,7 @@ async function main() {
|
|
|
7529
7714
|
console.log(version);
|
|
7530
7715
|
process.exit(0);
|
|
7531
7716
|
}
|
|
7532
|
-
|
|
7717
|
+
maybeRepairCodexWrapper(command, args);
|
|
7533
7718
|
/** Global --scope well-formedness guard (v1.26.2). parseArgs stores a value-less
|
|
7534
7719
|
* flag as boolean true; downstream the 14 consumer sites either coerced that to
|
|
7535
7720
|
* the literal scope string 'true' (recall filter/unlock input, wm session scope,
|
|
@@ -7681,6 +7866,37 @@ async function main() {
|
|
|
7681
7866
|
case '__session-end-worker':
|
|
7682
7867
|
cmdSessionEndWorker(hippoRoot, flags);
|
|
7683
7868
|
break;
|
|
7869
|
+
case 'pre-compact': {
|
|
7870
|
+
// Same TTY guard as `capture --last-session`: skip reading stdin when
|
|
7871
|
+
// it's an interactive terminal so a manual invocation never hangs.
|
|
7872
|
+
let stdinText;
|
|
7873
|
+
if (!process.stdin.isTTY) {
|
|
7874
|
+
try {
|
|
7875
|
+
stdinText = fs.readFileSync(0, 'utf8');
|
|
7876
|
+
}
|
|
7877
|
+
catch {
|
|
7878
|
+
stdinText = undefined;
|
|
7879
|
+
}
|
|
7880
|
+
}
|
|
7881
|
+
await cmdPreCompact(hippoRoot, {
|
|
7882
|
+
stdinText,
|
|
7883
|
+
logFile: typeof flags['log-file'] === 'string' ? flags['log-file'] : undefined,
|
|
7884
|
+
});
|
|
7885
|
+
break;
|
|
7886
|
+
}
|
|
7887
|
+
case 'compact-resume': {
|
|
7888
|
+
let stdinText;
|
|
7889
|
+
if (!process.stdin.isTTY) {
|
|
7890
|
+
try {
|
|
7891
|
+
stdinText = fs.readFileSync(0, 'utf8');
|
|
7892
|
+
}
|
|
7893
|
+
catch {
|
|
7894
|
+
stdinText = undefined;
|
|
7895
|
+
}
|
|
7896
|
+
}
|
|
7897
|
+
cmdCompactResume(hippoRoot, stdinText);
|
|
7898
|
+
break;
|
|
7899
|
+
}
|
|
7684
7900
|
case 'codex-run':
|
|
7685
7901
|
cmdCodexRun(hippoRoot, args);
|
|
7686
7902
|
break;
|
|
@@ -8094,24 +8310,53 @@ async function main() {
|
|
|
8094
8310
|
case 'invalidate': {
|
|
8095
8311
|
requireInit(hippoRoot);
|
|
8096
8312
|
const target = args[0];
|
|
8097
|
-
if (
|
|
8098
|
-
|
|
8313
|
+
if (flags['id'] === true) {
|
|
8314
|
+
// Value-less --id must never silently fall through to pattern mode
|
|
8315
|
+
// (pattern mode writes broadly; an ignored --id reverses user intent).
|
|
8316
|
+
console.error('--id requires a memory id');
|
|
8317
|
+
process.exit(1);
|
|
8318
|
+
}
|
|
8319
|
+
const onlyId = typeof flags['id'] === 'string' ? flags['id'] : undefined;
|
|
8320
|
+
if (typeof flags['dry-run'] === 'string') {
|
|
8321
|
+
// Unreachable via argv (dry-run is in BOOLEAN_FLAGS); guards
|
|
8322
|
+
// programmatically-built flags objects.
|
|
8323
|
+
console.error('--dry-run takes no value');
|
|
8324
|
+
process.exit(1);
|
|
8325
|
+
}
|
|
8326
|
+
const dryRun = flags['dry-run'] === true;
|
|
8327
|
+
if ((target && onlyId) || (!target && !onlyId)) {
|
|
8328
|
+
console.error('Usage: hippo invalidate "<old pattern>" [--dry-run] [--reason "<why>"]');
|
|
8329
|
+
console.error(' hippo invalidate --id <memory-id> [--dry-run] [--reason "<why>"]');
|
|
8330
|
+
console.error('Pass a pattern OR --id, not both. Tag matching is EXACT: the full pattern must equal a tag.');
|
|
8099
8331
|
process.exit(1);
|
|
8100
8332
|
}
|
|
8101
8333
|
const reason = flags['reason'] || null;
|
|
8102
8334
|
const invTarget = {
|
|
8103
|
-
from: target
|
|
8335
|
+
from: target ?? `id:${onlyId}`,
|
|
8104
8336
|
to: reason,
|
|
8105
8337
|
type: 'migration',
|
|
8106
8338
|
};
|
|
8107
|
-
const result = invalidateMatching(hippoRoot, invTarget, resolveTenantId({}));
|
|
8108
|
-
|
|
8109
|
-
|
|
8339
|
+
const result = invalidateMatching(hippoRoot, invTarget, resolveTenantId({}), { dryRun, onlyId });
|
|
8340
|
+
const label = target ? `"${target}"` : `--id ${onlyId}`;
|
|
8341
|
+
if (result.dryRun) {
|
|
8342
|
+
if (result.invalidated === 0) {
|
|
8343
|
+
console.log(`DRY RUN - no memories would match ${label}.`);
|
|
8344
|
+
}
|
|
8345
|
+
else {
|
|
8346
|
+
console.log(`DRY RUN - ${result.invalidated} memories WOULD be invalidated:`);
|
|
8347
|
+
result.preview.forEach(p => console.log(` ${p.id} ${p.headline}`));
|
|
8348
|
+
}
|
|
8349
|
+
}
|
|
8350
|
+
else if (result.invalidated === 0) {
|
|
8351
|
+
console.log(`No memories matched ${label}.`);
|
|
8110
8352
|
}
|
|
8111
8353
|
else {
|
|
8112
|
-
console.log(`Invalidated ${result.invalidated} memories referencing
|
|
8354
|
+
console.log(`Invalidated ${result.invalidated} memories referencing ${label}.`);
|
|
8113
8355
|
result.targets.forEach(id => console.log(` ${id}`));
|
|
8114
8356
|
}
|
|
8357
|
+
if (result.skippedPinned.length > 0) {
|
|
8358
|
+
console.log(`Skipped ${result.skippedPinned.length} pinned: ${result.skippedPinned.join(', ')}`);
|
|
8359
|
+
}
|
|
8115
8360
|
break;
|
|
8116
8361
|
}
|
|
8117
8362
|
case 'decide':
|