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.
- package/README.md +57 -34
- package/dist/api.d.ts +1 -0
- package/dist/api.js +36 -11
- package/dist/autolearn.d.ts +1 -4
- package/dist/autolearn.js +6 -7
- package/dist/capture.d.ts +3 -0
- package/dist/capture.js +51 -56
- package/dist/churn-git.js +2 -2
- package/dist/cli.d.ts +1 -4
- package/dist/cli.js +240 -150
- package/dist/config.js +8 -1
- package/dist/consolidate.d.ts +2 -0
- package/dist/consolidate.js +13 -6
- package/dist/customer-notes.js +3 -2
- package/dist/dag.js +5 -0
- package/dist/dashboard.js +4 -0
- package/dist/decisions.d.ts +1 -1
- package/dist/decisions.js +4 -3
- package/dist/extensions/openclaw-plugin/index.js +1 -0
- package/dist/extract.js +3 -0
- package/dist/graph-recall.js +8 -7
- package/dist/half-life-migration.d.ts +14 -5
- package/dist/half-life-migration.js +89 -21
- package/dist/handoff.d.ts +2 -0
- package/dist/hooks.d.ts +4 -4
- package/dist/hooks.js +10 -4
- package/dist/importers.d.ts +2 -1
- package/dist/importers.js +19 -7
- package/dist/incidents.d.ts +1 -1
- package/dist/incidents.js +4 -3
- package/dist/index.d.ts +4 -1
- package/dist/index.js +6 -1
- package/dist/mcp/server.js +3 -3
- package/dist/memory.d.ts +6 -13
- package/dist/memory.js +0 -10
- package/dist/policies.js +3 -2
- package/dist/predictions.js +2 -0
- package/dist/processes.js +3 -2
- package/dist/project-briefs.js +3 -2
- package/dist/secret-detect.js +3 -3
- package/dist/shared.d.ts +1 -1
- package/dist/shared.js +6 -1
- package/dist/skills.js +3 -2
- package/dist/store.d.ts +5 -5
- package/dist/store.js +25 -13
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist-ui/assets/index-BhT8RvO6.js +61 -0
- package/dist-ui/index.html +1 -1
- package/extensions/openclaw-plugin/index.ts +1 -0
- 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-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(
|
|
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
|
|
552
|
+
console.log(` Imported ${memImported} memories from this project's Claude Code auto memory.`);
|
|
546
553
|
}
|
|
547
554
|
}
|
|
548
555
|
}
|
|
549
|
-
/**
|
|
550
|
-
|
|
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
|
-
|
|
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
|
-
|
|
565
|
-
|
|
566
|
-
|
|
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
|
-
|
|
571
|
-
|
|
572
|
-
if (!
|
|
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
|
-
|
|
575
|
-
|
|
576
|
-
if (
|
|
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
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
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
|
-
|
|
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 =
|
|
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 (
|
|
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
|
-
|
|
2611
|
-
|
|
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
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
const
|
|
2618
|
-
|
|
2619
|
-
|
|
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 +
|
|
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(
|
|
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
|
|
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
|
-
*
|
|
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.
|
|
2916
|
-
process.
|
|
2969
|
+
console.error('=== Previous session hippo consolidation ===');
|
|
2970
|
+
process.stderr.write(content);
|
|
2917
2971
|
if (!content.endsWith('\n'))
|
|
2918
|
-
console.
|
|
2919
|
-
console.
|
|
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
|
|
3158
|
-
|
|
3159
|
-
|
|
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,
|
|
3164
|
-
appendSessionEndCloseLog(closeLogFile, handoff ? `wrote handoff for session ${closeSessionId}` :
|
|
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
|
|
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
|
|
7184
|
-
|
|
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
|
-
|
|
7194
|
-
|
|
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
|
|
7277
|
+
hippo capture --stdin <<< '<decisions, errors, lessons: 2-5 bullets>'
|
|
7230
7278
|
\`\`\`
|
|
7231
7279
|
`.trim(),
|
|
7232
7280
|
},
|
|
7233
7281
|
'cursor': {
|
|
7234
|
-
file: '.
|
|
7282
|
+
file: 'AGENTS.md',
|
|
7235
7283
|
description: 'Cursor',
|
|
7236
7284
|
content: `
|
|
7237
|
-
|
|
7238
|
-
|
|
7239
|
-
|
|
7240
|
-
|
|
7241
|
-
|
|
7242
|
-
|
|
7243
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
7452
|
-
const
|
|
7453
|
-
|
|
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
|