hippo-memory 0.24.1 → 0.26.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/dist/cli.js CHANGED
@@ -19,6 +19,7 @@
19
19
  * hippo embed [--status]
20
20
  * hippo watch "<command>"
21
21
  * hippo learn --git [--days <n>] [--repos <paths>]
22
+ * hippo daily-runner
22
23
  * hippo promote <id>
23
24
  * hippo sync
24
25
  * hippo decide "<decision>" [--context "<why>"] [--supersedes <id>]
@@ -27,7 +28,7 @@
27
28
  import * as path from 'path';
28
29
  import * as fs from 'fs';
29
30
  import * as os from 'os';
30
- import { execSync, spawn } from 'child_process';
31
+ import { execFileSync, execSync, spawn } from 'child_process';
31
32
  import { installJsonHooks, uninstallJsonHooks, resolveJsonHookPaths, detectInstalledTools, defaultSleepLogPath, ensureCodexWrapperInstalled, installCodexWrapper, uninstallCodexWrapper, resolveCodexSessionTranscript, resolveCodexWrapperPaths, } from './hooks.js';
32
33
  import { createMemory, calculateStrength, calculateRewardFactor, deriveHalfLife, resolveConfidence, applyOutcome, computeSchemaFit, Layer, DECISION_HALF_LIFE_DAYS, } from './memory.js';
33
34
  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';
@@ -42,8 +43,10 @@ import { captureError, extractLessons, deduplicateLesson, runWatched, fetchGitLo
42
43
  import { extractInvalidationTarget, invalidateMatching } from './invalidation.js';
43
44
  import { extractPathTags } from './path-context.js';
44
45
  import { getGlobalRoot, initGlobal, promoteToGlobal, shareMemory, listPeers, autoShare, transferScore, searchBothHybrid, syncGlobalToLocal, } from './shared.js';
46
+ import { DAILY_TASK_NAME, buildDailyRunnerCommand, listRegisteredWorkspaces, registerWorkspace, runDailyMaintenance, } from './scheduler.js';
45
47
  import { importChatGPT, importClaude, importCursor, importGenericFile, importMarkdown, } from './importers.js';
46
48
  import { cmdCapture } from './capture.js';
49
+ import { auditMemories } from './audit.js';
47
50
  import { wmPush, wmRead, wmClear, wmFlush } from './working-memory.js';
48
51
  // ---------------------------------------------------------------------------
49
52
  // Helpers
@@ -160,6 +163,7 @@ function cmdInitScan(scanDir, flags) {
160
163
  if (!alreadyExists) {
161
164
  initStore(repoHippo);
162
165
  }
166
+ registerWorkspace(globalRoot, repo);
163
167
  // Learn from git history
164
168
  let added = 0;
165
169
  if (!flags['no-learn'] && isGitRepo(repo)) {
@@ -173,6 +177,9 @@ function cmdInitScan(scanDir, flags) {
173
177
  }
174
178
  console.log(`\n${repos.length} repositories, ${totalLessons} new lessons learned.`);
175
179
  console.log(`Global store: ${globalRoot}`);
180
+ if (!flags['no-schedule']) {
181
+ setupDailySchedule(globalRoot);
182
+ }
176
183
  console.log(`\nRun \`hippo sleep\` in any project to consolidate and auto-share to global.`);
177
184
  }
178
185
  function cmdInit(hippoRoot, flags) {
@@ -202,13 +209,15 @@ function cmdInit(hippoRoot, flags) {
202
209
  console.log(' Directories: buffer/ episodic/ semantic/ conflicts/');
203
210
  console.log(' Files: hippo.db index.json stats.json');
204
211
  }
212
+ const globalRoot = getGlobalRoot();
213
+ registerWorkspace(globalRoot, path.dirname(hippoRoot));
205
214
  // Auto-detect and install hooks (unless --no-hooks)
206
215
  if (!flags['no-hooks']) {
207
216
  autoInstallHooks(alreadyExists);
208
217
  }
209
218
  // Auto-setup daily schedule (unless --no-schedule)
210
219
  if (!flags['no-schedule'] && !flags['global']) {
211
- setupDailySchedule(hippoRoot);
220
+ setupDailySchedule(globalRoot);
212
221
  }
213
222
  // Seed with git history on first init (unless --no-learn)
214
223
  if (!alreadyExists && !flags['no-learn'] && !flags['global']) {
@@ -301,22 +310,24 @@ function autoInstallHooks(quiet) {
301
310
  }
302
311
  }
303
312
  /**
304
- * Set up a daily cron job for hippo learn + sleep.
313
+ * Set up a machine-level daily runner that sweeps all registered Hippo
314
+ * workspaces.
305
315
  * Linux/macOS: writes to user crontab.
306
316
  * Windows: creates a scheduled task.
307
317
  * Skips if already installed.
308
318
  */
309
- function setupDailySchedule(hippoRoot) {
310
- const projectDir = path.resolve(path.dirname(hippoRoot));
319
+ function setupDailySchedule(globalRoot) {
320
+ const runnerDir = path.resolve(globalRoot);
311
321
  // Reject paths with characters that could break shell/crontab quoting
312
322
  // (backslash is normal on Windows, only dangerous in Unix shell/crontab)
313
323
  const unsafeChars = process.platform === 'win32' ? /["`$%\n\r]/ : /["`$\n\r\\]/;
314
- if (unsafeChars.test(projectDir)) {
315
- console.log(` Skipping schedule: project path contains unsafe characters.`);
324
+ if (unsafeChars.test(runnerDir)) {
325
+ console.log(` Skipping schedule: runner path contains unsafe characters.`);
316
326
  return;
317
327
  }
318
328
  const isWindows = process.platform === 'win32';
319
- const taskName = `hippo-daily-${path.basename(projectDir)}`.replace(/[^a-zA-Z0-9_-]/g, '-');
329
+ const taskName = DAILY_TASK_NAME;
330
+ const cmd = buildDailyRunnerCommand(runnerDir);
320
331
  if (isWindows) {
321
332
  // Check if task already exists
322
333
  try {
@@ -328,14 +339,13 @@ function setupDailySchedule(hippoRoot) {
328
339
  catch {
329
340
  // Task doesn't exist, create it
330
341
  }
331
- const cmd = `cd /d "${projectDir}" && hippo learn --git --days 1 && hippo sleep`;
332
342
  try {
333
343
  execSync(`schtasks /create /tn "${taskName}" /tr "cmd /c ${cmd.replace(/"/g, '""')}" /sc daily /st 06:15 /f`, { stdio: 'pipe' });
334
- console.log(` Scheduled daily learn+sleep (6:15am) via Task Scheduler: ${taskName}`);
344
+ console.log(` Scheduled machine-level daily runner (6:15am) via Task Scheduler: ${taskName}`);
335
345
  }
336
346
  catch {
337
347
  // No admin rights or schtasks unavailable, fall back to printing instructions
338
- console.log(` To schedule daily learn+sleep, run:`);
348
+ console.log(` To schedule the machine-level daily runner, run:`);
339
349
  console.log(` schtasks /create /tn "${taskName}" /tr "cmd /c ${cmd}" /sc daily /st 06:15`);
340
350
  }
341
351
  }
@@ -347,14 +357,14 @@ function setupDailySchedule(hippoRoot) {
347
357
  if (existing.includes(marker)) {
348
358
  return; // already scheduled
349
359
  }
350
- const cronLine = `15 6 * * * cd "${projectDir}" && hippo learn --git --days 1 && hippo sleep ${marker}`;
360
+ const cronLine = `15 6 * * * ${cmd} ${marker}`;
351
361
  const newCrontab = existing.trimEnd() + '\n' + cronLine + '\n';
352
362
  execSync('crontab -', { input: newCrontab, stdio: ['pipe', 'pipe', 'pipe'] });
353
- console.log(` Scheduled daily learn+sleep (6:15am) via crontab`);
363
+ console.log(` Scheduled machine-level daily runner (6:15am) via crontab`);
354
364
  }
355
365
  catch {
356
- const cronLine = `15 6 * * * cd "${projectDir}" && hippo learn --git --days 1 && hippo sleep`;
357
- console.log(` To schedule daily learn+sleep, add to crontab (crontab -e):`);
366
+ const cronLine = `15 6 * * * ${cmd}`;
367
+ console.log(` To schedule the machine-level daily runner, add to crontab (crontab -e):`);
358
368
  console.log(` ${cronLine}`);
359
369
  }
360
370
  }
@@ -758,6 +768,24 @@ function cmdSleepCore(hippoRoot, flags) {
758
768
  console.log(`\nDeduped ${dedupResult.removed} duplicates (${parts.join(', ')}). Kept stronger copies.`);
759
769
  }
760
770
  }
771
+ // Quality audit — remove junk, report warnings
772
+ if (!dryRun) {
773
+ const allEntries = loadAllEntries(hippoRoot);
774
+ const audit = auditMemories(allEntries);
775
+ if (audit.issues.length > 0) {
776
+ const errors = audit.issues.filter(i => i.severity === 'error');
777
+ const warnings = audit.issues.filter(i => i.severity === 'warning');
778
+ if (errors.length > 0) {
779
+ for (const issue of errors) {
780
+ deleteEntry(hippoRoot, issue.memoryId);
781
+ }
782
+ console.log(`\nAudit: removed ${errors.length} junk memories (too short/empty).`);
783
+ }
784
+ if (warnings.length > 0) {
785
+ console.log(`Audit: ${warnings.length} low-quality memories detected (run \`hippo audit\` for details).`);
786
+ }
787
+ }
788
+ }
761
789
  // Auto-share high-transfer-score memories to global (unless --no-share or dry-run)
762
790
  if (!dryRun && !flags['no-share']) {
763
791
  const sleepConfig = loadConfig(hippoRoot);
@@ -2141,7 +2169,7 @@ hippo capture --stdin <<< '<decisions, errors, lessons — 2-5 bullets>'
2141
2169
  'codex': {
2142
2170
  file: 'AGENTS.md',
2143
2171
  description: 'OpenAI Codex',
2144
- content: `
2172
+ content: `
2145
2173
  ## Project Memory (Hippo)
2146
2174
 
2147
2175
  At the start of every task, run:
@@ -2160,8 +2188,8 @@ On task completion:
2160
2188
  hippo outcome --good
2161
2189
  \`\`\`
2162
2190
 
2163
- When Hippo's Codex wrapper is installed, session-end capture runs automatically.
2164
- If the wrapper is not installed, capture a brief summary manually:
2191
+ When Hippo's Codex wrapper is installed, session-end capture runs automatically.
2192
+ If the wrapper is not installed, capture a brief summary manually:
2165
2193
  \`\`\`bash
2166
2194
  hippo capture --stdin <<< '<decisions, errors, lessons — 2-5 bullets>'
2167
2195
  \`\`\`
@@ -2391,6 +2419,7 @@ function cmdSetup(flags) {
2391
2419
  const dryRun = Boolean(flags['dry-run']);
2392
2420
  const forceAll = Boolean(flags['all']);
2393
2421
  const tools = detectInstalledTools();
2422
+ const globalRoot = getGlobalRoot();
2394
2423
  console.log('Hippo setup -- configuring SessionEnd + SessionStart hooks');
2395
2424
  console.log('');
2396
2425
  const jsonTools = tools.filter((t) => t.kind === 'json-hook' && (t.detected || forceAll));
@@ -2464,9 +2493,46 @@ function cmdSetup(flags) {
2464
2493
  console.log(` ${tool.name.padEnd(14)} ${tool.notes}`);
2465
2494
  }
2466
2495
  }
2496
+ if (!flags['no-schedule']) {
2497
+ console.log('');
2498
+ if (dryRun) {
2499
+ console.log(`[dry-run] would install the machine-level daily runner around ${globalRoot}`);
2500
+ }
2501
+ else {
2502
+ setupDailySchedule(globalRoot);
2503
+ }
2504
+ }
2467
2505
  console.log('');
2468
2506
  console.log('Done. Restart your AI tool to activate the hooks.');
2469
2507
  }
2508
+ function cmdDailyRunner() {
2509
+ const globalRoot = getGlobalRoot();
2510
+ const workspaces = listRegisteredWorkspaces(globalRoot);
2511
+ if (workspaces.length === 0) {
2512
+ console.log('No registered Hippo workspaces found. Run `hippo init` inside a project first.');
2513
+ return;
2514
+ }
2515
+ console.log(`Running daily maintenance across ${workspaces.length} registered workspace${workspaces.length === 1 ? '' : 's'}...`);
2516
+ let processed = 0;
2517
+ let failed = 0;
2518
+ runDailyMaintenance(workspaces, (cwd, args) => {
2519
+ try {
2520
+ execFileSync(process.execPath, [process.argv[1], ...args], {
2521
+ cwd,
2522
+ stdio: 'inherit',
2523
+ windowsHide: true,
2524
+ });
2525
+ if (args[0] === 'sleep')
2526
+ processed++;
2527
+ }
2528
+ catch (err) {
2529
+ failed++;
2530
+ const action = args.join(' ');
2531
+ console.error(`[hippo] daily-runner failed in ${cwd} during \`${action}\`: ${err.message}`);
2532
+ }
2533
+ });
2534
+ console.log(`Daily maintenance complete: ${processed} workspace${processed === 1 ? '' : 's'} processed, ${failed} command failure${failed === 1 ? '' : 's'}.`);
2535
+ }
2470
2536
  // JSON-hook install/uninstall lives in ./hooks.ts so tests can import it
2471
2537
  // without running the CLI main(). Backwards-compatible wrappers below keep
2472
2538
  // older call sites working.
@@ -2560,7 +2626,7 @@ Commands:
2560
2626
  --days <n> Days of git history to seed (default: 365 for --scan, 30 for single)
2561
2627
  --global Init the global store ($HIPPO_HOME or ~/.hippo/)
2562
2628
  --no-hooks Skip auto-detecting and installing agent hooks
2563
- --no-schedule Skip auto-creating daily learn+sleep cron job
2629
+ --no-schedule Skip auto-creating the machine-level daily runner
2564
2630
  --no-learn Skip seeding memories from git history
2565
2631
  remember <text> Store a memory
2566
2632
  --tag <tag> Add a tag (repeatable)
@@ -2583,10 +2649,12 @@ Commands:
2583
2649
  --dry-run Preview without writing
2584
2650
  --no-learn Skip auto git-learn before consolidation
2585
2651
  --no-share Skip auto-sharing to global store
2652
+ daily-runner Sweep registered workspaces and run daily learn+sleep
2586
2653
  dedup Remove duplicate memories (keeps stronger copy)
2587
2654
  --dry-run Preview without removing
2588
2655
  --threshold <n> Overlap threshold 0-1 (default: 0.7)
2589
2656
  status Show memory health stats
2657
+ audit [--fix] Check memory quality (--fix removes junk)
2590
2658
  outcome Apply feedback to last recall
2591
2659
  --good Memories were helpful
2592
2660
  --bad Memories were irrelevant
@@ -2679,15 +2747,16 @@ Commands:
2679
2747
  available SessionEnd+SessionStart hooks
2680
2748
  --all Install for every JSON-hook tool, even if not detected
2681
2749
  --dry-run Show what would be installed without writing
2682
- last-sleep Print the last 'hippo sleep --log-file' output and clear it
2683
- --path <p> Log path (default: ~/.hippo/logs/last-sleep.log)
2684
- --keep Print without clearing
2685
- codex-run [-- ...args] Launch real Codex behind Hippo's session-end wrapper
2686
- hook <sub> [target] Manage framework integrations
2687
- hook list Show available hooks
2688
- hook install <target> Install hook (claude-code|codex|cursor|openclaw|opencode|pi)
2689
- claude-code/opencode install SessionEnd+SessionStart;
2690
- codex wraps the detected launcher in place
2750
+ --no-schedule Skip installing or repairing the daily runner
2751
+ last-sleep Print the last 'hippo sleep --log-file' output and clear it
2752
+ --path <p> Log path (default: ~/.hippo/logs/last-sleep.log)
2753
+ --keep Print without clearing
2754
+ codex-run [-- ...args] Launch real Codex behind Hippo's session-end wrapper
2755
+ hook <sub> [target] Manage framework integrations
2756
+ hook list Show available hooks
2757
+ hook install <target> Install hook (claude-code|codex|cursor|openclaw|opencode|pi)
2758
+ claude-code/opencode install SessionEnd+SessionStart;
2759
+ codex wraps the detected launcher in place
2691
2760
  hook uninstall <target> Remove hook
2692
2761
  decide "<decision>" Record an architectural decision (90-day half-life)
2693
2762
  --context "<why>" Why this decision was made
@@ -2756,8 +2825,8 @@ async function main() {
2756
2825
  break;
2757
2826
  case 'remember': {
2758
2827
  const text = args.join(' ').trim();
2759
- if (!text) {
2760
- console.error('Please provide text to remember.');
2828
+ if (!text || text.length < 3) {
2829
+ console.error('Memory content too short (minimum 3 characters).');
2761
2830
  process.exit(1);
2762
2831
  }
2763
2832
  cmdRemember(hippoRoot, text, flags);
@@ -2793,6 +2862,40 @@ async function main() {
2793
2862
  case 'dedup':
2794
2863
  cmdDedup(hippoRoot, flags);
2795
2864
  break;
2865
+ case 'audit': {
2866
+ requireInit(hippoRoot);
2867
+ const entries = loadAllEntries(hippoRoot);
2868
+ const result = auditMemories(entries);
2869
+ const shouldFix = Boolean(flags['fix']);
2870
+ if (result.issues.length === 0) {
2871
+ console.log(`All ${result.total} memories passed quality checks.`);
2872
+ }
2873
+ else {
2874
+ console.log(`Audited ${result.total} memories: ${result.clean} clean, ${result.issues.length} issues\n`);
2875
+ for (const issue of result.issues) {
2876
+ const icon = issue.severity === 'error' ? 'ERR' : 'WARN';
2877
+ console.log(` [${icon}] ${issue.memoryId}: ${issue.reason}`);
2878
+ console.log(` "${issue.content.slice(0, 80)}${issue.content.length > 80 ? '...' : ''}"`);
2879
+ }
2880
+ if (shouldFix) {
2881
+ const errorIds = result.issues.filter(i => i.severity === 'error').map(i => i.memoryId);
2882
+ if (errorIds.length > 0) {
2883
+ for (const id of errorIds) {
2884
+ deleteEntry(hippoRoot, id);
2885
+ }
2886
+ console.log(`\nRemoved ${errorIds.length} error-severity memories.`);
2887
+ console.log(`${result.issues.length - errorIds.length} warnings remain (review manually).`);
2888
+ }
2889
+ else {
2890
+ console.log(`\nNo error-severity issues. Warnings require manual review.`);
2891
+ }
2892
+ }
2893
+ else {
2894
+ console.log(`\nRun with --fix to auto-remove error-severity issues.`);
2895
+ }
2896
+ }
2897
+ break;
2898
+ }
2796
2899
  case 'status':
2797
2900
  cmdStatus(hippoRoot);
2798
2901
  break;
@@ -2844,6 +2947,9 @@ async function main() {
2844
2947
  case 'setup':
2845
2948
  cmdSetup(flags);
2846
2949
  break;
2950
+ case 'daily-runner':
2951
+ cmdDailyRunner();
2952
+ break;
2847
2953
  case 'embed':
2848
2954
  await cmdEmbed(hippoRoot, flags);
2849
2955
  break;