hippo-memory 1.52.6 → 1.52.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +57 -34
  2. package/dist/api.d.ts +1 -0
  3. package/dist/api.js +36 -11
  4. package/dist/autolearn.d.ts +1 -4
  5. package/dist/autolearn.js +6 -7
  6. package/dist/capture.d.ts +3 -0
  7. package/dist/capture.js +51 -56
  8. package/dist/churn-git.js +2 -2
  9. package/dist/cli.d.ts +1 -4
  10. package/dist/cli.js +240 -150
  11. package/dist/config.js +8 -1
  12. package/dist/consolidate.d.ts +2 -0
  13. package/dist/consolidate.js +13 -6
  14. package/dist/customer-notes.js +3 -2
  15. package/dist/dag.js +5 -0
  16. package/dist/dashboard.js +4 -0
  17. package/dist/decisions.d.ts +1 -1
  18. package/dist/decisions.js +4 -3
  19. package/dist/extensions/openclaw-plugin/index.js +1 -0
  20. package/dist/extract.js +3 -0
  21. package/dist/graph-recall.js +8 -7
  22. package/dist/half-life-migration.d.ts +14 -5
  23. package/dist/half-life-migration.js +89 -21
  24. package/dist/handoff.d.ts +2 -0
  25. package/dist/hooks.d.ts +4 -4
  26. package/dist/hooks.js +10 -4
  27. package/dist/importers.d.ts +2 -1
  28. package/dist/importers.js +19 -7
  29. package/dist/incidents.d.ts +1 -1
  30. package/dist/incidents.js +4 -3
  31. package/dist/index.d.ts +4 -1
  32. package/dist/index.js +6 -1
  33. package/dist/mcp/server.js +3 -3
  34. package/dist/memory.d.ts +6 -13
  35. package/dist/memory.js +0 -10
  36. package/dist/policies.js +3 -2
  37. package/dist/predictions.js +2 -0
  38. package/dist/processes.js +3 -2
  39. package/dist/project-briefs.js +3 -2
  40. package/dist/secret-detect.js +3 -3
  41. package/dist/shared.d.ts +1 -1
  42. package/dist/shared.js +6 -1
  43. package/dist/skills.js +3 -2
  44. package/dist/store.d.ts +5 -5
  45. package/dist/store.js +25 -13
  46. package/dist/version.d.ts +1 -1
  47. package/dist/version.js +1 -1
  48. package/dist-ui/assets/index-BhT8RvO6.js +61 -0
  49. package/dist-ui/index.html +1 -1
  50. package/extensions/openclaw-plugin/index.ts +1 -0
  51. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  52. package/extensions/openclaw-plugin/package.json +1 -1
  53. package/openclaw.plugin.json +1 -1
  54. package/package.json +1 -1
  55. package/dist-ui/assets/index-BgmA7Hwe.js +0 -61
package/dist/cli.js CHANGED
@@ -37,7 +37,7 @@ import * as path from 'path';
37
37
  import * as fs from 'fs';
38
38
  import * as os from 'os';
39
39
  import { fileURLToPath } from 'node:url';
40
- import { execFileSync, execSync, spawn } from 'child_process';
40
+ import { execFileSync, execSync, spawn, spawnSync } from 'child_process';
41
41
  import { installJsonHooks, uninstallJsonHooks, resolveJsonHookPaths, detectInstalledTools, defaultSleepLogPath, ensureCodexWrapperInstalled, installCodexWrapper, isCodexWrapperInstalled, repairCodexWrapperIfInstalled, uninstallCodexWrapper, resolveCodexSessionTranscript, resolveCodexWrapperPaths, installOpencodePlugin, uninstallOpencodePlugin, resolveOpencodePluginPath, } from './hooks.js';
42
42
  import { createMemory, calculateStrength, calculateRewardFactor, deriveHalfLife, resolveConfidence, confidenceFacets, confidenceLabel, computeSchemaFit, Layer, } from './memory.js';
43
43
  import { detectSecret } from './secret-detect.js';
@@ -69,13 +69,13 @@ import { pushGoal, getActiveGoals, completeGoal, suspendGoal, resumeGoal, applyG
69
69
  import { rowToGoal } from './goals.js';
70
70
  import { captureError, extractLessons, partitionLessons, deduplicateLesson, runWatched, fetchGitLog, isGitRepo, } from './autolearn.js';
71
71
  import { extractInvalidationTarget, invalidateMatching, detectChurnStale } from './invalidation.js';
72
- import { resolveProjectIdentity } from './project-identity.js';
72
+ import { realpathOrResolve, resolveProjectIdentity } from './project-identity.js';
73
73
  import { extractPathTags } from './path-context.js';
74
74
  import { detectScope } from './scope.js';
75
75
  import { getGlobalRoot, initGlobal, shareMemory, listPeers, autoShare, transferScore, searchBothHybrid, syncGlobalToLocal, } from './shared.js';
76
76
  import { DAILY_TASK_NAME, buildDailyRunnerCommand, listRegisteredWorkspaces, registerWorkspace, runDailyMaintenance, } from './scheduler.js';
77
77
  import { importChatGPT, importClaude, importCursor, importGenericFile, importMarkdown, importVault, } from './importers.js';
78
- import { cmdCapture, cmdPreCompact, postCompactMessage, resolveLastSessionTranscript, truncateCodePointSafe, sanitizeLogMessage } from './capture.js';
78
+ import { cmdCapture, cmdPreCompact, postCompactMessage, resolveLastSessionTranscript, truncateCodePointSafe, sanitizeLogMessage, transcriptWorkingState } from './capture.js';
79
79
  import { readStdinBounded } from './stdin.js';
80
80
  import { auditMemories, appendAuditEvent, } from './audit.js';
81
81
  import { listApiKeys, revokeApiKey } from './auth.js';
@@ -203,7 +203,7 @@ function requireInit(hippoRoot) {
203
203
  }
204
204
  /** FE2: run detectChurnStale against every store this repo's memories can live in. */
205
205
  function runChurnStaleForRepo(hippoRoot, dryRun) {
206
- const repoRoot = execFileSync('git', ['rev-parse', '--show-toplevel'], { cwd: process.cwd(), encoding: 'utf8' }).trim();
206
+ const repoRoot = execFileSync('git', ['rev-parse', '--show-toplevel'], { cwd: process.cwd(), encoding: 'utf8', windowsHide: true }).trim();
207
207
  const projectName = resolveProjectIdentity(process.cwd()).name;
208
208
  const globalRoot = getGlobalRoot();
209
209
  const roots = globalRoot !== hippoRoot && isInitialized(globalRoot) ? [hippoRoot, globalRoot] : [hippoRoot];
@@ -484,6 +484,14 @@ function cmdInitScan(scanDir, flags) {
484
484
  (totalLowInfo > 0 ? `, ${totalLowInfo} low-information subject(s) dropped` : '') +
485
485
  '.');
486
486
  console.log(`Global store: ${globalRoot}`);
487
+ if (!flags['no-hooks']) {
488
+ // User-level hooks only: a hippo block in each repo's CLAUDE.md or AGENTS.md would leave a diff in every repo.
489
+ const agents = detectAgentHooks(repos);
490
+ if (agents.length === 0) {
491
+ console.log(' No agent config found in these repositories. Run `hippo setup` to add hooks for the agents on this machine.');
492
+ }
493
+ installUserLevelHooks(agents, true);
494
+ }
487
495
  if (!flags['no-schedule']) {
488
496
  setupDailySchedule(globalRoot);
489
497
  }
@@ -520,7 +528,7 @@ function cmdInit(hippoRoot, flags) {
520
528
  registerWorkspace(globalRoot, path.dirname(hippoRoot));
521
529
  // Auto-detect and install hooks (unless --no-hooks)
522
530
  if (!flags['no-hooks']) {
523
- autoInstallHooks(alreadyExists);
531
+ autoInstallHooks();
524
532
  }
525
533
  // Auto-setup daily schedule (unless --no-schedule)
526
534
  if (!flags['no-schedule'] && !flags['global']) {
@@ -539,64 +547,92 @@ function cmdInit(hippoRoot, flags) {
539
547
  console.log(` No matching commits found in git history.`);
540
548
  }
541
549
  }
542
- // Also import from Claude Code / agent MEMORY.md files
543
550
  const memImported = learnFromMemoryMd(hippoRoot);
544
551
  if (memImported > 0) {
545
- console.log(` Imported ${memImported} memories from agent MEMORY.md files.`);
552
+ console.log(` Imported ${memImported} memories from this project's Claude Code auto memory.`);
546
553
  }
547
554
  }
548
555
  }
549
- /**
550
- * Detect agent config files in cwd and auto-install hippo hooks.
551
- * Skips files that already have a <!-- hippo:start --> block.
552
- */
553
- function autoInstallHooks(quiet) {
556
+ /** Plain init: patch the detected agents' instruction files in cwd, then install their user-level hooks. */
557
+ function autoInstallHooks() {
554
558
  const cwd = process.cwd();
559
+ const agents = detectAgentHooks([cwd]);
560
+ const agentsMd = path.join(cwd, HOOKS.codex.file);
561
+ // Read before patching: a re-run of init finds its own block and skips the Codex hint.
562
+ const codexHint = !(fs.existsSync(agentsMd) && fs.readFileSync(agentsMd, 'utf8').includes(HOOK_MARKERS.start));
563
+ patchInstructionFiles(cwd, agents);
564
+ installUserLevelHooks(agents, codexHint);
565
+ }
566
+ /** HOOKS keys of the agents with a marker file in any of dirs, in detector order. */
567
+ function detectAgentHooks(dirs) {
555
568
  // Map: filename to check -> hook key(s) to install
556
569
  const detectors = [
557
570
  { files: ['CLAUDE.md', '.claude/settings.json'], hook: 'claude-code' },
558
571
  { files: ['AGENTS.md', '.codex'], hook: 'codex' },
559
- { files: ['.cursorrules', '.cursor/rules'], hook: 'cursor' },
572
+ // No Cursor row: Cursor reads the root AGENTS.md, which the rows either side patch.
560
573
  { files: ['.openclaw', 'AGENTS.md'], hook: 'openclaw' },
561
574
  { files: ['.opencode', 'opencode.json'], hook: 'opencode' },
562
575
  { files: ['.pi', '.pi/agent'], hook: 'pi' },
563
576
  ];
564
- // Track which hook files we've already touched to avoid double-patching AGENTS.md
565
- const installed = new Set();
566
- for (const { files, hook } of detectors) {
577
+ return detectors
578
+ .filter(({ files }) => dirs.some((dir) => files.some((f) => fs.existsSync(path.join(dir, f)))))
579
+ .map(({ hook }) => hook);
580
+ }
581
+ function patchInstructionFiles(dir, agents) {
582
+ // One block per file: several agents share AGENTS.md.
583
+ const seen = new Set();
584
+ for (const hook of agents) {
567
585
  const hookDef = HOOKS[hook];
568
586
  if (!hookDef)
569
587
  continue;
570
- // Check if any marker file exists
571
- const detected = files.some((f) => fs.existsSync(path.join(cwd, f)));
572
- if (!detected)
588
+ const targetPath = path.resolve(dir, hookDef.file);
589
+ // Never create the file: a marker such as .codex or .claude/settings.json does not ask for a new AGENTS.md or CLAUDE.md.
590
+ if (!fs.existsSync(targetPath) || seen.has(targetPath))
573
591
  continue;
574
- const targetPath = path.resolve(cwd, hookDef.file);
575
- // Skip if we already installed a hook into this file
576
- if (installed.has(targetPath))
592
+ seen.add(targetPath);
593
+ const existing = fs.readFileSync(targetPath, 'utf8');
594
+ if (existing.includes(HOOK_MARKERS.start)) {
595
+ refreshShippedBlock(targetPath, existing, hook);
577
596
  continue;
578
- // An instruction block already present is not rewritten, but the JSON
579
- // hooks and plugins below still run: they are idempotent, and skipping
580
- // them meant a store set up by an older hippo never got hooks added in
581
- // later releases (PreCompact, PostToolUseFailure) on a re-run of init.
582
- const blockPresent = fs.existsSync(targetPath)
583
- && fs.readFileSync(targetPath, 'utf8').includes(HOOK_MARKERS.start);
584
- // Only patch the agent-instructions file if it already exists.
585
- // Never create a new CLAUDE.md / AGENTS.md / etc. just because a sibling
586
- // marker file (.claude/settings.json, .codex, etc.) was detected — that
587
- // pollutes dirs the user didn't intend to configure.
588
- if (!blockPresent && fs.existsSync(targetPath)) {
589
- const block = `${HOOK_MARKERS.start}\n${hookDef.content}\n${HOOK_MARKERS.end}`;
590
- const existing = fs.readFileSync(targetPath, 'utf8');
591
- const sep = existing.endsWith('\n') ? '\n' : '\n\n';
592
- fs.writeFileSync(targetPath, existing + sep + block + '\n', 'utf8');
593
- installed.add(targetPath);
594
- console.log(` Auto-installed ${hook} hook in ${hookDef.file}`);
595
- }
596
- // The Codex session-capture wrapper swaps the codex launcher binary, so
597
- // init never installs it silently — it points at the explicit opt-in
598
- // command instead (issue #133).
599
- if (hook === 'codex' && !blockPresent && !isCodexWrapperInstalled()) {
597
+ }
598
+ const block = `${HOOK_MARKERS.start}\n${hookDef.content}\n${HOOK_MARKERS.end}`;
599
+ const sep = existing.endsWith('\n') ? '\n' : '\n\n';
600
+ fs.writeFileSync(targetPath, existing + sep + block + '\n', 'utf8');
601
+ console.log(` Auto-installed ${hook} hook in ${hookDef.file}`);
602
+ }
603
+ }
604
+ /** The first hippo block in `text` and the agent whose current or shipped text it is; `owner` is undefined for an edited block. */
605
+ function hippoBlock(text) {
606
+ const at = text.indexOf(HOOK_MARKERS.start);
607
+ const start = at + HOOK_MARKERS.start.length;
608
+ const end = text.indexOf(HOOK_MARKERS.end, start);
609
+ if (at < 0 || end < 0)
610
+ return null;
611
+ // git autocrlf checks these files out with CRLF: match as LF, write back in the file's own ending.
612
+ const raw = text.slice(start, end);
613
+ const inner = raw.replace(/\r\n/g, '\n').trim();
614
+ const owner = Object.keys(HOOKS).find((k) => HOOKS[k].content === inner) ?? SHIPPED_HOOK_HASHES.get(createHash('sha256').update(inner).digest('hex'));
615
+ return { start, end, eol: raw.includes('\r\n') ? '\r\n' : '\n', inner, owner };
616
+ }
617
+ /** Swap an unedited block from an earlier hippo for the current one; an edited block stays, with a hint. */
618
+ function refreshShippedBlock(filePath, text, hook) {
619
+ const block = hippoBlock(text);
620
+ if (!block || (block.owner && HOOKS[block.owner].content === block.inner))
621
+ return;
622
+ const { start, end, eol, owner } = block;
623
+ const name = path.basename(filePath);
624
+ if (!owner) {
625
+ console.log(` Left the edited hippo block in ${name} as is; \`hippo hook install ${hook}\` replaces it.`);
626
+ return;
627
+ }
628
+ fs.writeFileSync(filePath, `${text.slice(0, start)}${eol}${HOOKS[owner].content.replace(/\n/g, eol)}${eol}${text.slice(end)}`, 'utf8');
629
+ console.log(` Refreshed the ${owner} hippo block in ${name}`);
630
+ }
631
+ /** Claude Code settings hooks and the OpenCode plugin, under the home directory; idempotent, so re-running init adds newer hooks. */
632
+ function installUserLevelHooks(agents, codexHint) {
633
+ for (const hook of agents) {
634
+ // The Codex capture wrapper swaps the codex launcher binary, so init only points at the opt-in (issue #133).
635
+ if (hook === 'codex' && codexHint && !isCodexWrapperInstalled()) {
600
636
  console.log(' Codex detected. To capture Codex sessions: hippo hook install codex');
601
637
  }
602
638
  // For Claude Code, also install SessionEnd+SessionStart entries in its
@@ -674,7 +710,7 @@ function setupDailySchedule(globalRoot) {
674
710
  if (isWindows) {
675
711
  // Check if task already exists
676
712
  try {
677
- const existing = execSync(`schtasks /query /tn "${taskName}" 2>nul`, { encoding: 'utf-8' });
713
+ const existing = execSync(`schtasks /query /tn "${taskName}" 2>nul`, { encoding: 'utf-8', windowsHide: true });
678
714
  if (existing.includes(taskName)) {
679
715
  return; // already scheduled
680
716
  }
@@ -683,7 +719,7 @@ function setupDailySchedule(globalRoot) {
683
719
  // Task doesn't exist, create it
684
720
  }
685
721
  try {
686
- execSync(`schtasks /create /tn "${taskName}" /tr "cmd /c ${cmd.replace(/"/g, '""')}" /sc daily /st 06:15 /f`, { stdio: 'pipe' });
722
+ execSync(`schtasks /create /tn "${taskName}" /tr "cmd /c ${cmd.replace(/"/g, '""')}" /sc daily /st 06:15 /f`, { stdio: 'pipe', windowsHide: true });
687
723
  console.log(` Scheduled machine-level daily runner (6:15am) via Task Scheduler: ${taskName}`);
688
724
  }
689
725
  catch {
@@ -696,13 +732,13 @@ function setupDailySchedule(globalRoot) {
696
732
  // Unix: check crontab for existing entry
697
733
  const marker = `# hippo:${taskName}`;
698
734
  try {
699
- const existing = execSync('crontab -l 2>/dev/null', { encoding: 'utf-8' });
735
+ const existing = execSync('crontab -l 2>/dev/null', { encoding: 'utf-8', windowsHide: true });
700
736
  if (existing.includes(marker)) {
701
737
  return; // already scheduled
702
738
  }
703
739
  const cronLine = `15 6 * * * ${cmd} ${marker}`;
704
740
  const newCrontab = existing.trimEnd() + '\n' + cronLine + '\n';
705
- execSync('crontab -', { input: newCrontab, stdio: ['pipe', 'pipe', 'pipe'] });
741
+ execSync('crontab -', { input: newCrontab, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
706
742
  console.log(` Scheduled machine-level daily runner (6:15am) via crontab`);
707
743
  }
708
744
  catch {
@@ -877,6 +913,7 @@ function cmdSupersede(hippoRoot, oldId, newContent, flags) {
877
913
  confidence: 'verified',
878
914
  tenantId: old.tenantId,
879
915
  scope: old.scope,
916
+ baseHalfLifeDays: loadConfig(hippoRoot).defaultHalfLifeDays,
880
917
  });
881
918
  // AT1: write the SUCCESSOR first. The rejection guard fires on the new
882
919
  // content — if it refuses, nothing has been mutated yet (the old ordering
@@ -1081,7 +1118,8 @@ async function cmdRecall(hippoRoot, query, flags) {
1081
1118
  });
1082
1119
  }
1083
1120
  else if (useMultihop) {
1084
- const allEntries = [...localEntries, ...globalEntries];
1121
+ // Unlike searchBothHybrid below, multihop ranks one pooled list, so a shared memory's two copies both compete.
1122
+ const allEntries = api.oneCopyPerMemory(localEntries, globalEntries, evalNow()).flat();
1085
1123
  results = multihopSearch(query, allEntries, {
1086
1124
  budget,
1087
1125
  hippoRoot,
@@ -2463,6 +2501,7 @@ function cmdTraceRecord(hippoRoot, flags) {
2463
2501
  trace_outcome: outcome,
2464
2502
  source_session_id: sessionId,
2465
2503
  tenantId: resolveTenantId({}),
2504
+ baseHalfLifeDays: loadConfig(hippoRoot).defaultHalfLifeDays,
2466
2505
  });
2467
2506
  writeEntry(hippoRoot, entry);
2468
2507
  console.log(`Recorded trace ${entry.id} (outcome=${outcome}, ${steps.length} steps)`);
@@ -2485,7 +2524,7 @@ function cmdTrace(hippoRoot, id, flags) {
2485
2524
  }
2486
2525
  const now = evalNow();
2487
2526
  const strength = calculateStrength(entry, now);
2488
- const halfLife = deriveHalfLife(7, entry);
2527
+ const halfLife = entry.half_life_days;
2489
2528
  const rewardFactor = calculateRewardFactor(entry);
2490
2529
  const effHalfLife = halfLife * rewardFactor;
2491
2530
  const createdMs = new Date(entry.created).getTime();
@@ -2548,7 +2587,7 @@ function cmdTrace(hippoRoot, id, flags) {
2548
2587
  console.log(` now: ${fmt(strength, 3)}`);
2549
2588
  console.log(` in 30 days: ${fmt(projectedAt(30), 3)}`);
2550
2589
  console.log(` in 90 days: ${fmt(projectedAt(90), 3)}`);
2551
- console.log(` half-life: ${fmt(halfLife, 1)}d (base) x ${fmt(rewardFactor, 2)} reward = ${fmt(effHalfLife, 1)}d effective`);
2590
+ console.log(` half-life: ${fmt(halfLife, 1)}d (stored) x ${fmt(rewardFactor, 2)} reward = ${fmt(effHalfLife, 1)}d effective`);
2552
2591
  console.log();
2553
2592
  console.log(`Retrieval:`);
2554
2593
  console.log(` count: ${entry.retrieval_count}`);
@@ -2606,28 +2645,47 @@ async function cmdRefine(hippoRoot, flags) {
2606
2645
  }
2607
2646
  }
2608
2647
  }
2609
- /**
2610
- * Scan for Claude Code MEMORY.md files and import new entries into hippo.
2611
- * Looks in ~/.claude/projects/<project>/memory/ for .md files with YAML frontmatter.
2612
- */
2648
+ /** Claude Code's auto memory folder names for a project: its checkout, which subfolders share, or the folder itself outside a repository. */
2649
+ function claudeMemoryFolderNames(projectRoot) {
2650
+ const roots = [projectRoot, realpathOrResolve(projectRoot)];
2651
+ const git = spawnSync('git', ['rev-parse', '--path-format=absolute', '--show-toplevel', '--absolute-git-dir', '--git-common-dir'], { cwd: projectRoot, encoding: 'utf8', timeout: 10000, windowsHide: true });
2652
+ if (git.status === 0) {
2653
+ const [top, gitDir, common] = git.stdout.trim().split(/\r?\n/);
2654
+ roots.push(claudeCheckoutRoot(top, gitDir, common));
2655
+ }
2656
+ return new Set(roots.map((root) => (process.platform === 'win32' ? claudeFolderName(root).toLowerCase() : claudeFolderName(root))));
2657
+ }
2658
+ /** Claude Code's rule: a linked worktree shares its main checkout's folder, or the git folder's when that sits outside a checkout (a bare repository, or --separate-git-dir); any other checkout, a submodule included, keeps its own. */
2659
+ function claudeCheckoutRoot(top, gitDir, common) {
2660
+ if (gitDir === common)
2661
+ return top;
2662
+ if (path.basename(common) === '.git')
2663
+ return path.dirname(common);
2664
+ return fs.existsSync(path.join(common, '.git')) ? top : common;
2665
+ }
2666
+ /** Claude Code's folder name for a path: non-alphanumerics made '-', and a name over 200 characters cut to 200 plus a base-36 hash of the whole path. */
2667
+ function claudeFolderName(root) {
2668
+ const full = path.resolve(root);
2669
+ const name = full.replace(/[^a-zA-Z0-9]/g, '-');
2670
+ if (name.length <= 200)
2671
+ return name;
2672
+ let hash = 0;
2673
+ for (let i = 0; i < full.length; i++)
2674
+ hash = ((hash << 5) - hash + full.charCodeAt(i)) | 0;
2675
+ return `${name.slice(0, 200)}-${Math.abs(hash).toString(36)}`;
2676
+ }
2677
+ /** Import new entries from the Claude Code auto memory of the store's project, the frontmatter .md files in ~/.claude/projects/<project>/memory/. */
2613
2678
  export function learnFromMemoryMd(hippoRoot, homeDir = os.homedir()) {
2614
- const home = homeDir;
2615
- const memoryDirs = [];
2616
- // Claude Code project memories
2617
- const claudeProjectsDir = path.join(home, '.claude', 'projects');
2618
- if (fs.existsSync(claudeProjectsDir)) {
2619
- try {
2620
- for (const project of fs.readdirSync(claudeProjectsDir)) {
2621
- const memDir = path.join(claudeProjectsDir, project, 'memory');
2622
- if (fs.existsSync(memDir))
2623
- memoryDirs.push(memDir);
2624
- }
2625
- }
2626
- catch { /* permission denied */ }
2627
- }
2679
+ // Only this project's folder, because every entry written here counts as this project's memory.
2680
+ // SHORTCUT: an autoMemoryDirectory or CLAUDE_CODE_PROJECT_DIR_NAME setting imports nothing; read Claude's settings if users ask.
2681
+ const claudeProjectsDir = path.join(homeDir, '.claude', 'projects');
2682
+ const memoryDirs = [...claudeMemoryFolderNames(path.dirname(hippoRoot))]
2683
+ .map((folder) => path.join(claudeProjectsDir, folder, 'memory'))
2684
+ .filter((memDir) => fs.existsSync(memDir));
2628
2685
  if (memoryDirs.length === 0)
2629
2686
  return 0;
2630
2687
  const existing = loadAllEntries(hippoRoot, resolveTenantId({}));
2688
+ const baseHalfLifeDays = loadConfig(hippoRoot).defaultHalfLifeDays;
2631
2689
  let imported = 0;
2632
2690
  let skippedSecret = 0;
2633
2691
  // AT1 (plan §3 containment): a rejection guard refusal is per-VALUE — one
@@ -2671,6 +2729,7 @@ export function learnFromMemoryMd(hippoRoot, homeDir = os.homedir()) {
2671
2729
  source: `claude-memory:${file}`,
2672
2730
  confidence: 'observed',
2673
2731
  tenantId: resolveTenantId({}),
2732
+ baseHalfLifeDays,
2674
2733
  });
2675
2734
  try {
2676
2735
  writeEntry(hippoRoot, entry);
@@ -2854,10 +2913,10 @@ export function renderSleepResult(result) {
2854
2913
  }
2855
2914
  async function cmdSleepCore(hippoRoot, flags) {
2856
2915
  requireInit(hippoRoot);
2857
- // Phase 1: Auto-learn from git + MEMORY.md (CLI-only, uses process.cwd() / os.homedir()).
2916
+ // Phase 1: Auto-learn from git + Claude Code auto memory (CLI-only, uses process.cwd() / os.homedir()).
2858
2917
  // Stays in cli.ts; api.sleep covers Phase 2-6 only.
2859
2918
  if (!flags['no-learn'] && flags['dry-run']) {
2860
- console.log('Dry run: skipped learning from git commits and MEMORY.md files.');
2919
+ console.log("Dry run: skipped learning from git commits and this project's Claude Code auto memory.");
2861
2920
  }
2862
2921
  else if (!flags['no-learn']) {
2863
2922
  const config = loadConfig(hippoRoot);
@@ -2875,10 +2934,9 @@ async function cmdSleepCore(hippoRoot, flags) {
2875
2934
  console.error(`Churn-staleness check failed for ${root}: ${result.error}`);
2876
2935
  }
2877
2936
  }
2878
- // Also learn from Claude Code MEMORY.md files
2879
2937
  const memImported = learnFromMemoryMd(hippoRoot);
2880
2938
  if (memImported > 0)
2881
- console.log(`Imported ${memImported} memories from Claude Code MEMORY.md files.`);
2939
+ console.log(`Imported ${memImported} memories from this project's Claude Code auto memory.`);
2882
2940
  }
2883
2941
  // Phase 2-6: Pure-storage pipeline (consolidate + dedup + audit + share + ambient).
2884
2942
  const ctx = {
@@ -2892,12 +2950,8 @@ async function cmdSleepCore(hippoRoot, flags) {
2892
2950
  });
2893
2951
  renderSleepResult(result);
2894
2952
  }
2895
- /**
2896
- * Print the contents of the SessionEnd sleep log to stdout, then clear it.
2897
- * Called from SessionStart hooks so the user sees the previous session's
2898
- * consolidation output (SessionEnd hook output is invisible because the TUI
2899
- * is tearing down when it runs).
2900
- */
2953
+ /** Prints the SessionEnd sleep log, then clears it. Stderr, because Claude Code adds
2954
+ * SessionStart stdout to the model's context and this log is for the user. */
2901
2955
  function cmdLastSleep(flags) {
2902
2956
  const logPath = typeof flags['path'] === 'string'
2903
2957
  ? flags['path']
@@ -2912,11 +2966,11 @@ function cmdLastSleep(flags) {
2912
2966
  return;
2913
2967
  }
2914
2968
  if (content.trim().length > 0) {
2915
- console.log('=== Previous session hippo consolidation ===');
2916
- process.stdout.write(content);
2969
+ console.error('=== Previous session hippo consolidation ===');
2970
+ process.stderr.write(content);
2917
2971
  if (!content.endsWith('\n'))
2918
- console.log();
2919
- console.log('===========================================');
2972
+ console.error();
2973
+ console.error('===========================================');
2920
2974
  }
2921
2975
  if (!flags['keep']) {
2922
2976
  try {
@@ -3093,7 +3147,7 @@ function collectHandoffEvidence(cwd, testStatus) {
3093
3147
  let gitRef = null;
3094
3148
  try {
3095
3149
  gitRef = execFileSync('git', ['rev-parse', 'HEAD'], {
3096
- cwd, encoding: 'utf8', timeout: 2000, stdio: ['ignore', 'pipe', 'ignore'],
3150
+ cwd, encoding: 'utf8', timeout: 2000, stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true,
3097
3151
  }).trim() || null;
3098
3152
  }
3099
3153
  catch {
@@ -3102,7 +3156,7 @@ function collectHandoffEvidence(cwd, testStatus) {
3102
3156
  let dirtyTree = null;
3103
3157
  try {
3104
3158
  const status = execFileSync('git', ['status', '--porcelain'], {
3105
- cwd, encoding: 'utf8', timeout: 2000, stdio: ['ignore', 'pipe', 'ignore'],
3159
+ cwd, encoding: 'utf8', timeout: 2000, stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true,
3106
3160
  });
3107
3161
  dirtyTree = status.trim().length > 0;
3108
3162
  }
@@ -3119,9 +3173,9 @@ async function cmdSessionEndWorker(hippoRoot, flags) {
3119
3173
  // sleep errors are already tee'd to the log file via cmdSleep's
3120
3174
  // `[hippo] sleep failed: ...` line. Continue to capture regardless.
3121
3175
  }
3176
+ const transcriptPath = typeof flags['transcript'] === 'string' ? flags['transcript'] : undefined;
3122
3177
  try {
3123
3178
  const logFile = typeof flags['log-file'] === 'string' ? flags['log-file'] : undefined;
3124
- const transcriptPath = typeof flags['transcript'] === 'string' ? flags['transcript'] : undefined;
3125
3179
  // With no stdin of its own, capture would read this as a manual run and scan every project.
3126
3180
  if (!transcriptPath) {
3127
3181
  appendSessionEndCloseLog(logFile ?? null, 'skip capture: no transcript for this session');
@@ -3154,14 +3208,19 @@ async function cmdSessionEndWorker(hippoRoot, flags) {
3154
3208
  // snapshot writeSessionEndHandoff reads is still active.
3155
3209
  if (closeSessionId) {
3156
3210
  try {
3157
- const activeForHandoff = loadActiveTaskSnapshot(hippoRoot, resolveTenantId({}));
3158
- if (!activeForHandoff || activeForHandoff.session_id !== closeSessionId) {
3159
- appendSessionEndCloseLog(closeLogFile, 'skip: no active snapshot for session');
3211
+ const tenantId = resolveTenantId({});
3212
+ const ownSnapshot = loadActiveTaskSnapshot(hippoRoot, tenantId)?.session_id === closeSessionId;
3213
+ // A never-compacted session has no snapshot; read even when it has one, as another session's PreCompact can take the slot before the write.
3214
+ const derived = transcriptPath
3215
+ ? transcriptWorkingState(transcriptPath, (message) => appendSessionEndCloseLog(closeLogFile, message))
3216
+ : null;
3217
+ if (!ownSnapshot && !derived) {
3218
+ appendSessionEndCloseLog(closeLogFile, 'skip: no snapshot or transcript for session');
3160
3219
  }
3161
3220
  else {
3162
3221
  const evidence = collectHandoffEvidence(process.cwd(), 'unknown');
3163
- const handoff = writeSessionEndHandoff(hippoRoot, resolveTenantId({}), closeSessionId, evidence);
3164
- appendSessionEndCloseLog(closeLogFile, handoff ? `wrote handoff for session ${closeSessionId}` : 'skip: handoff newer than snapshot');
3222
+ const handoff = writeSessionEndHandoff(hippoRoot, tenantId, closeSessionId, evidence, derived);
3223
+ appendSessionEndCloseLog(closeLogFile, handoff ? `wrote handoff for session ${closeSessionId}` : `skip: kept the existing handoff for session ${closeSessionId}`);
3165
3224
  }
3166
3225
  }
3167
3226
  catch (err) {
@@ -6086,7 +6145,7 @@ function cmdGraph(hippoRoot, args, flags) {
6086
6145
  : process.platform === 'darwin'
6087
6146
  ? ['open', [out]]
6088
6147
  : ['xdg-open', [out]];
6089
- const child = spawn(cmd, cmdArgs, { detached: true, stdio: 'ignore' });
6148
+ const child = spawn(cmd, cmdArgs, { detached: true, stdio: 'ignore', windowsHide: true });
6090
6149
  // A missing launcher (e.g. xdg-open absent) emits 'error' asynchronously;
6091
6150
  // an unhandled 'error' event would throw, so swallow it — the file is
6092
6151
  // already written and its path printed above.
@@ -6664,6 +6723,7 @@ function autoDetectContext() {
6664
6723
  const diff = execSync('git diff --name-only HEAD 2>&1', {
6665
6724
  encoding: 'utf8',
6666
6725
  timeout: 3000,
6726
+ windowsHide: true,
6667
6727
  }).trim();
6668
6728
  if (diff) {
6669
6729
  // Extract meaningful terms from file paths
@@ -6679,6 +6739,7 @@ function autoDetectContext() {
6679
6739
  const branch = execSync('git branch --show-current 2>&1', {
6680
6740
  encoding: 'utf8',
6681
6741
  timeout: 3000,
6742
+ windowsHide: true,
6682
6743
  }).trim();
6683
6744
  if (branch && branch !== 'main' && branch !== 'master') {
6684
6745
  return branch.replace(/[-_\/]/g, ' ');
@@ -6839,7 +6900,8 @@ function learnFromRepo(hippoRoot, repoPath, days, label) {
6839
6900
  }
6840
6901
  // Same patterns as MCP hippo_learn: config.gitLearnPatterns (whose default
6841
6902
  // equals extractLessons' built-in list) so a custom list applies everywhere.
6842
- const parsedLessons = extractLessons(gitLog, loadConfig(hippoRoot).gitLearnPatterns);
6903
+ const config = loadConfig(hippoRoot);
6904
+ const parsedLessons = extractLessons(gitLog, config.gitLearnPatterns);
6843
6905
  if (parsedLessons.length === 0) {
6844
6906
  console.log(`${prefix}No fix/revert/bug commits found in the specified period.`);
6845
6907
  return { added: 0, skipped: 0, lowInfo: 0 };
@@ -6905,6 +6967,7 @@ function learnFromRepo(hippoRoot, repoPath, days, label) {
6905
6967
  confidence: 'observed',
6906
6968
  schema_fit: schemaFitVal,
6907
6969
  tenantId: resolveTenantId({}),
6970
+ baseHalfLifeDays: config.defaultHalfLifeDays,
6908
6971
  });
6909
6972
  // Auto-tag with path context from the repo being learned
6910
6973
  const learnPathTags = extractPathTags(repoPath);
@@ -7180,25 +7243,14 @@ run it if the hook is not installed:
7180
7243
  hippo context --auto --budget 1500
7181
7244
  \`\`\`
7182
7245
 
7183
- When you learn something important:
7184
- \`\`\`bash
7185
- hippo remember "<lesson>"
7186
- \`\`\`
7187
-
7188
- When you hit an error or discover a gotcha:
7246
+ When you find out why something failed, record it right then, while you
7247
+ work, never as a closing step:
7189
7248
  \`\`\`bash
7190
7249
  hippo remember "<what went wrong and why>" --error
7191
7250
  \`\`\`
7192
7251
 
7193
- After completing work successfully:
7194
- \`\`\`bash
7195
- hippo outcome --good
7196
- \`\`\`
7197
-
7198
- When the user ends the session, capture a brief summary:
7199
- \`\`\`bash
7200
- hippo capture --stdin <<< '<decisions, errors, lessons — 2-5 bullets>'
7201
- \`\`\`
7252
+ The installed hooks store failed tool calls and capture the session when it
7253
+ ends, so there is nothing to run before you finish.
7202
7254
  `.trim(),
7203
7255
  },
7204
7256
  'codex': {
@@ -7213,34 +7265,41 @@ hippo context --auto --budget 1500
7213
7265
  \`\`\`
7214
7266
  Read the output before writing any code.
7215
7267
 
7216
- On errors or unexpected behaviour:
7268
+ On errors or unexpected behaviour, record it right then, while you work,
7269
+ never as a closing step:
7217
7270
  \`\`\`bash
7218
7271
  hippo remember "<description of what went wrong>" --error
7219
7272
  \`\`\`
7220
7273
 
7221
- On task completion:
7222
- \`\`\`bash
7223
- hippo outcome --good
7224
- \`\`\`
7225
-
7226
7274
  When Hippo's Codex wrapper is installed, session-end capture runs automatically.
7227
7275
  If the wrapper is not installed, capture a brief summary manually:
7228
7276
  \`\`\`bash
7229
- hippo capture --stdin <<< '<decisions, errors, lessons — 2-5 bullets>'
7277
+ hippo capture --stdin <<< '<decisions, errors, lessons: 2-5 bullets>'
7230
7278
  \`\`\`
7231
7279
  `.trim(),
7232
7280
  },
7233
7281
  'cursor': {
7234
- file: '.cursorrules',
7282
+ file: 'AGENTS.md',
7235
7283
  description: 'Cursor',
7236
7284
  content: `
7237
- # Project Memory (Hippo)
7238
- # Before each task, load context:
7239
- # hippo context --auto --budget 1500
7240
- # After errors:
7241
- # hippo remember "<error description>" --error
7242
- # After completing:
7243
- # hippo outcome --good
7285
+ ## Project Memory (Hippo)
7286
+
7287
+ At the start of every task, run:
7288
+ \`\`\`bash
7289
+ hippo context --auto --budget 1500
7290
+ \`\`\`
7291
+ Read the output before writing any code.
7292
+
7293
+ On errors or unexpected behaviour, record it right then, while you work,
7294
+ never as a closing step:
7295
+ \`\`\`bash
7296
+ hippo remember "<description of what went wrong>" --error
7297
+ \`\`\`
7298
+
7299
+ When ending a session, capture a brief summary:
7300
+ \`\`\`bash
7301
+ hippo capture --stdin <<< '<decisions, errors, lessons: 2-5 bullets>'
7302
+ \`\`\`
7244
7303
  `.trim(),
7245
7304
  },
7246
7305
  'openclaw': {
@@ -7255,19 +7314,15 @@ hippo context --auto --budget 1500
7255
7314
  \`\`\`
7256
7315
  Read the output before writing any code.
7257
7316
 
7258
- On errors or unexpected behaviour:
7317
+ On errors or unexpected behaviour, record it right then, while you work,
7318
+ never as a closing step:
7259
7319
  \`\`\`bash
7260
7320
  hippo remember "<description of what went wrong>" --error
7261
7321
  \`\`\`
7262
7322
 
7263
- On task completion:
7264
- \`\`\`bash
7265
- hippo outcome --good
7266
- \`\`\`
7267
-
7268
7323
  When ending a session, capture a brief summary:
7269
7324
  \`\`\`bash
7270
- hippo capture --stdin <<< '<decisions, errors, lessons — 2-5 bullets>'
7325
+ hippo capture --stdin <<< '<decisions, errors, lessons: 2-5 bullets>'
7271
7326
  \`\`\`
7272
7327
  `.trim(),
7273
7328
  },
@@ -7283,7 +7338,8 @@ hippo context --auto --budget 1500
7283
7338
  \`\`\`
7284
7339
  Read the output before writing any code.
7285
7340
 
7286
- When you learn something important or hit an error:
7341
+ When you learn a non-obvious lesson or hit an error, record it right then,
7342
+ while you work, never as a closing step:
7287
7343
  \`\`\`bash
7288
7344
  hippo remember "<lesson>" --error
7289
7345
  \`\`\`
@@ -7293,14 +7349,9 @@ When stuck or repeating yourself, check if this happened before:
7293
7349
  hippo recall "<what's going wrong>" --budget 2000
7294
7350
  \`\`\`
7295
7351
 
7296
- On task completion:
7297
- \`\`\`bash
7298
- hippo outcome --good
7299
- \`\`\`
7300
-
7301
7352
  When ending a session, capture a brief summary:
7302
7353
  \`\`\`bash
7303
- hippo capture --stdin <<< '<decisions, errors, lessons — 2-5 bullets>'
7354
+ hippo capture --stdin <<< '<decisions, errors, lessons: 2-5 bullets>'
7304
7355
  \`\`\`
7305
7356
  `.trim(),
7306
7357
  },
@@ -7316,28 +7367,44 @@ hippo context --auto --budget 1500
7316
7367
  \`\`\`
7317
7368
  Read the output before writing any code.
7318
7369
 
7319
- On errors or unexpected behaviour:
7370
+ On errors or unexpected behaviour, record it right then, while you work,
7371
+ never as a closing step:
7320
7372
  \`\`\`bash
7321
7373
  hippo remember "<description of what went wrong>" --error
7322
7374
  \`\`\`
7323
7375
 
7324
- On task completion:
7325
- \`\`\`bash
7326
- hippo outcome --good
7327
- \`\`\`
7328
-
7329
7376
  When ending a session, capture a brief summary:
7330
7377
  \`\`\`bash
7331
- hippo capture --stdin <<< '<decisions, errors, lessons — 2-5 bullets>'
7378
+ hippo capture --stdin <<< '<decisions, errors, lessons: 2-5 bullets>'
7332
7379
  \`\`\`
7333
7380
 
7334
7381
  For full integration, copy the hippo-memory Pi extension to \`~/.pi/agent/extensions/hippo-memory/\`.
7335
7382
  `.trim(),
7336
7383
  },
7337
7384
  };
7385
+ // sha256 of each trimmed block an earlier hippo wrote, so init refreshes only blocks nobody edited. Add the old hash when a block changes.
7386
+ const SHIPPED_HOOK_HASHES = new Map([
7387
+ ['c04e48f2896a4fee9ae98f8f832e2d26a3910269df3beb5bcd6baee3cd9db68e', 'claude-code'],
7388
+ ['e6b12bd8983c032e5ca8e95a97aeff4178a5a05026d10acad5b2e1b25d5656dd', 'claude-code'],
7389
+ ['4c64e11d3e5be68fa547c9248d7553feb645a02f7cf13ba02f7275e1854baf44', 'claude-code'],
7390
+ ['293bd319bbc86225a0ee027490a3322a0336257f5832f7fade65e4ffb2530654', 'claude-code'],
7391
+ ['15abcece9712279fb4721f7a8f0ba117457400278977beb5cf5b5d7ba49f7b1a', 'codex'],
7392
+ ['0c81a6b2c21473313001f624b80ea870e661aecbfda9bfe8503febc0d5f34533', 'codex'],
7393
+ ['88e45358aba4f17912f113221c991dc758275991335d1daa4aa1974a69c46769', 'codex'],
7394
+ ['a1415ecda9b2f8f317c233738e4a5ac16e6b2cc385a017c0c8ecfbfacbcab6a3', 'cursor'],
7395
+ ['a38c428bbdfc14ec50f6f7b9183785170a4eae1ce9cde60257cca6efc7206b3a', 'cursor'],
7396
+ ['40524c3bd5a2eb04036567cc761451961d950995768bccd93a9900b0f75eafea', 'openclaw'],
7397
+ ['7b3518e8c0feaa7b8b454cde7743f7598ad14cd9979e1680d0954484e2464aae', 'openclaw'],
7398
+ ['4601c67c31f41cd5b1324cfccdb1afc66872b7fb0bc1e7c5789ecabb1f6bd942', 'opencode'],
7399
+ ['90d9e21d8d1ecbe99a0fc7b7f2d9f8af7b5315a6b4b0203df4f7a9bdc0699b98', 'opencode'],
7400
+ ['8b8f5986d7f7ed15f06e68720d8913c3cab23d94366b411935ca2bbaa334553b', 'pi'],
7401
+ ['37767b355e18beac726b05b9e2b898dab8c6135fd7b98f3aa52edc734d5dd283', 'pi'],
7402
+ ]);
7338
7403
  function cmdHook(args, flags) {
7339
7404
  const subcommand = args[0];
7340
7405
  const target = args[1];
7406
+ // Cursor has no init row, so the AGENTS.md block is usually another agent's, and Cursor reads that one as it is.
7407
+ const othersBlock = (text) => target === 'cursor' && hippoBlock(text)?.owner !== 'cursor';
7341
7408
  if (subcommand === 'list') {
7342
7409
  console.log('Available hooks:\n');
7343
7410
  for (const [name, hook] of Object.entries(HOOKS)) {
@@ -7359,7 +7426,10 @@ function cmdHook(args, flags) {
7359
7426
  let agentFileTouched = false;
7360
7427
  if (fs.existsSync(filepath)) {
7361
7428
  const existing = fs.readFileSync(filepath, 'utf8');
7362
- if (existing.includes(HOOK_MARKERS.start)) {
7429
+ if (existing.includes(HOOK_MARKERS.start) && othersBlock(existing)) {
7430
+ console.log(`${hook.file} already has a hippo block, which Cursor reads; left it as is.`);
7431
+ }
7432
+ else if (existing.includes(HOOK_MARKERS.start)) {
7363
7433
  const re = new RegExp(`${escapeRegex(HOOK_MARKERS.start)}[\\s\\S]*?${escapeRegex(HOOK_MARKERS.end)}`, 'g');
7364
7434
  const updated = existing.replace(re, block);
7365
7435
  fs.writeFileSync(filepath, updated, 'utf8');
@@ -7447,10 +7517,13 @@ function cmdHook(args, flags) {
7447
7517
  const filepath = path.resolve(process.cwd(), hook.file);
7448
7518
  if (fs.existsSync(filepath)) {
7449
7519
  const existing = fs.readFileSync(filepath, 'utf8');
7450
- if (existing.includes(HOOK_MARKERS.start)) {
7451
- const re = new RegExp(`\\n?${escapeRegex(HOOK_MARKERS.start)}[\\s\\S]*?${escapeRegex(HOOK_MARKERS.end)}\\n?`, 'g');
7452
- const cleaned = existing.replace(re, '\n').replace(/\n{3,}/g, '\n\n').trim();
7453
- fs.writeFileSync(filepath, cleaned + '\n', 'utf8');
7520
+ if (existing.includes(HOOK_MARKERS.start) && othersBlock(existing)) {
7521
+ const owner = hippoBlock(existing)?.owner;
7522
+ const whose = owner ? `hippo wrote it for ${owner}` : 'it has been edited, so hippo cannot tell whose it is';
7523
+ console.log(`Left the hippo block in ${hook.file}: ${whose}. Delete it by hand if no agent needs it.`);
7524
+ }
7525
+ else if (existing.includes(HOOK_MARKERS.start)) {
7526
+ fs.writeFileSync(filepath, withoutHookBlock(existing) + '\n', 'utf8');
7454
7527
  console.log(`Removed Hippo hook from ${hook.file}`);
7455
7528
  }
7456
7529
  else {
@@ -7479,11 +7552,28 @@ function cmdHook(args, flags) {
7479
7552
  console.log('Removed Codex wrapper integration');
7480
7553
  }
7481
7554
  }
7555
+ else if (target === 'cursor') {
7556
+ // Older hippo wrote Cursor's block to .cursorrules, creating the file when it was missing.
7557
+ const legacy = path.resolve(process.cwd(), '.cursorrules');
7558
+ const old = fs.existsSync(legacy) ? fs.readFileSync(legacy, 'utf8') : '';
7559
+ if (old.includes(HOOK_MARKERS.start)) {
7560
+ const left = withoutHookBlock(old);
7561
+ if (left)
7562
+ fs.writeFileSync(legacy, left + '\n', 'utf8');
7563
+ else
7564
+ fs.unlinkSync(legacy);
7565
+ console.log(left ? 'Removed the old Hippo hook from .cursorrules' : 'Deleted .cursorrules, which held only the old Hippo hook');
7566
+ }
7567
+ }
7482
7568
  return;
7483
7569
  }
7484
7570
  console.error('Usage: hippo hook <install|uninstall|list> [target]');
7485
7571
  process.exit(1);
7486
7572
  }
7573
+ function withoutHookBlock(text) {
7574
+ const re = new RegExp(`\\n?${escapeRegex(HOOK_MARKERS.start)}[\\s\\S]*?${escapeRegex(HOOK_MARKERS.end)}\\n?`, 'g');
7575
+ return text.replace(re, '\n').replace(/\n{3,}/g, '\n\n').trim();
7576
+ }
7487
7577
  function escapeRegex(s) {
7488
7578
  return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
7489
7579
  }
@@ -8997,7 +9087,7 @@ Commands:
8997
9087
  --all Install for every JSON-hook tool, even if not detected
8998
9088
  --dry-run Show what would be installed without writing
8999
9089
  --no-schedule Skip installing or repairing the daily runner
9000
- last-sleep Print the last 'hippo sleep --log-file' output and clear it
9090
+ last-sleep Print the last 'hippo sleep --log-file' output to stderr and clear it
9001
9091
  --path <p> Log path (default: ~/.hippo/logs/last-sleep.log)
9002
9092
  --keep Print without clearing
9003
9093
  session-end SessionEnd hook: run sleep, then capture this session, in a detached worker