hippo-memory 1.52.7 → 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 +1 -1
- package/dist/api.d.ts +1 -0
- package/dist/api.js +34 -10
- package/dist/autolearn.js +3 -2
- package/dist/capture.d.ts +3 -0
- package/dist/capture.js +48 -56
- package/dist/churn-git.js +2 -2
- package/dist/cli.d.ts +1 -4
- package/dist/cli.js +73 -53
- package/dist/dashboard.js +4 -0
- package/dist/extensions/openclaw-plugin/index.js +1 -0
- package/dist/graph-recall.js +8 -7
- package/dist/handoff.d.ts +2 -0
- package/dist/hooks.d.ts +4 -4
- package/dist/hooks.js +9 -3
- package/dist/mcp/server.js +3 -3
- package/dist/secret-detect.js +3 -3
- package/dist/shared.d.ts +1 -1
- package/dist/shared.js +6 -1
- package/dist/store.d.ts +3 -5
- package/dist/store.js +22 -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/README.md
CHANGED
|
@@ -87,7 +87,7 @@ hippo init --scan ~
|
|
|
87
87
|
After setup, `hippo sleep` runs at session end (via auto-installed agent hooks) and does five things:
|
|
88
88
|
|
|
89
89
|
1. **Learns** from today's git commits
|
|
90
|
-
2. **Imports** new entries from Claude Code
|
|
90
|
+
2. **Imports** new entries from the project's Claude Code auto memory
|
|
91
91
|
3. **Consolidates** memories (decay, merge, prune)
|
|
92
92
|
4. **Deduplicates** near-identical memories, keeping the stronger copy
|
|
93
93
|
5. **Shares** high-value lessons to a global store so they surface in every project
|
package/dist/api.d.ts
CHANGED
|
@@ -95,6 +95,7 @@ export { classifyOriginProject } from './project-identity.js';
|
|
|
95
95
|
* flagged rows with no project origin never ambient-inject.
|
|
96
96
|
*/
|
|
97
97
|
export declare function ambientSecretAdmit(e: MemoryEntry, currentProjectName: string): boolean;
|
|
98
|
+
export declare function oneCopyPerMemory(local: readonly MemoryEntry[], global: readonly MemoryEntry[], now: Date): [MemoryEntry[], MemoryEntry[]];
|
|
98
99
|
export interface RememberOpts {
|
|
99
100
|
content: string;
|
|
100
101
|
kind?: MemoryKind;
|
package/dist/api.js
CHANGED
|
@@ -142,6 +142,26 @@ function loadAmbientEntries(hippoRoot, tenantId, pinnedOnly, includeRecent, admi
|
|
|
142
142
|
const admitAmbient = (e) => admit(e) && (e.pinned || isContentWorthStoring(e.content));
|
|
143
143
|
return loadAmbientCandidates(hippoRoot, tenantId, includeRecent, admitAmbient, recall);
|
|
144
144
|
}
|
|
145
|
+
// Share and promote copy a memory to the global store under a new id, so equal content is the only link.
|
|
146
|
+
// A pinned copy wins, then the stronger one after the ranking's own global discount; a tie keeps the local copy.
|
|
147
|
+
export function oneCopyPerMemory(local, global, now) {
|
|
148
|
+
const score = (e, isGlobal) => calculateStrength(e, now) * (isGlobal ? 1 / 1.2 : 1);
|
|
149
|
+
const best = new Map();
|
|
150
|
+
const offer = (entry, isGlobal) => {
|
|
151
|
+
const held = best.get(entry.content);
|
|
152
|
+
const wins = !held || (held.entry.pinned !== entry.pinned
|
|
153
|
+
? entry.pinned
|
|
154
|
+
: score(entry, isGlobal) > score(held.entry, held.isGlobal));
|
|
155
|
+
if (wins)
|
|
156
|
+
best.set(entry.content, { entry, isGlobal });
|
|
157
|
+
};
|
|
158
|
+
for (const e of local)
|
|
159
|
+
offer(e, false);
|
|
160
|
+
for (const e of global)
|
|
161
|
+
offer(e, true);
|
|
162
|
+
const kept = new Set([...best.values()].map((b) => b.entry));
|
|
163
|
+
return [local.filter((e) => kept.has(e)), global.filter((e) => kept.has(e))];
|
|
164
|
+
}
|
|
145
165
|
export function remember(ctx, opts) {
|
|
146
166
|
const detection = opts.untrusted ? detectInstruction(opts.content) : { flagged: false, reason: null };
|
|
147
167
|
const requestedScope = opts.scope ?? null;
|
|
@@ -1620,13 +1640,14 @@ export async function getContext(ctx, opts = {}) {
|
|
|
1620
1640
|
// Effective budget: explicit opts.budget wins over config.
|
|
1621
1641
|
const effBudget = opts.budget !== undefined ? budget : pinnedCfg.pinnedInject.budget;
|
|
1622
1642
|
const nowP = evalNow(); // honors HIPPO_FAKE_NOW (eval-only; see ablation.ts)
|
|
1643
|
+
const [localPool, globalPool] = oneCopyPerMemory(localEntries, globalEntries, nowP);
|
|
1623
1644
|
const selectedIds = new Set();
|
|
1624
1645
|
let usedP = 0;
|
|
1625
1646
|
// Pinned entries are explicit user intent, the recent-N list an automatic
|
|
1626
1647
|
// backfill. Both loops share ONE budget and the recent loop runs first, so
|
|
1627
1648
|
// pins are ranked here and reserve their share before it can spend.
|
|
1628
|
-
const pinnedLocal =
|
|
1629
|
-
const pinnedGlobal =
|
|
1649
|
+
const pinnedLocal = localPool.filter((e) => e.pinned);
|
|
1650
|
+
const pinnedGlobal = globalPool.filter((e) => e.pinned);
|
|
1630
1651
|
const rankedPinned = [
|
|
1631
1652
|
...pinnedLocal.map((e) => ({ entry: e, isGlobal: false })),
|
|
1632
1653
|
...pinnedGlobal.map((e) => ({ entry: e, isGlobal: true })),
|
|
@@ -1683,19 +1704,21 @@ export async function getContext(ctx, opts = {}) {
|
|
|
1683
1704
|
const p = promptTokens(opts.prompt ?? '');
|
|
1684
1705
|
if (p.size > 0) {
|
|
1685
1706
|
// Candidates came off the ambient load's own connection (recallRequest above), not a fresh open.
|
|
1686
|
-
|
|
1687
|
-
const
|
|
1707
|
+
// A candidate carrying a pin's text would inject that memory a second time.
|
|
1708
|
+
const pinnedText = new Set(rankedPinned.map((r) => r.entry.content));
|
|
1709
|
+
const eligible = (e) => admit(e) && !e.pinned && isContentWorthStoring(e.content) && !pinnedText.has(e.content);
|
|
1710
|
+
const [localCandidates, globalCandidates] = oneCopyPerMemory((localLoad.recall ?? []).filter(eligible), (globalLoad.recall ?? []).filter(eligible), nowP);
|
|
1688
1711
|
const seenCandidateIds = new Set();
|
|
1689
1712
|
const candidateItems = [];
|
|
1690
1713
|
// Local wins the id collision (a global row synced into the local store).
|
|
1691
1714
|
for (const e of localCandidates) {
|
|
1692
|
-
if (
|
|
1715
|
+
if (seenCandidateIds.has(e.id))
|
|
1693
1716
|
continue;
|
|
1694
1717
|
seenCandidateIds.add(e.id);
|
|
1695
1718
|
candidateItems.push({ id: e.id, tokens: contentTokens(e.content), entry: e, isGlobal: false });
|
|
1696
1719
|
}
|
|
1697
1720
|
for (const e of globalCandidates) {
|
|
1698
|
-
if (
|
|
1721
|
+
if (seenCandidateIds.has(e.id))
|
|
1699
1722
|
continue;
|
|
1700
1723
|
seenCandidateIds.add(e.id);
|
|
1701
1724
|
candidateItems.push({ id: e.id, tokens: contentTokens(e.content), entry: e, isGlobal: true });
|
|
@@ -1715,8 +1738,8 @@ export async function getContext(ctx, opts = {}) {
|
|
|
1715
1738
|
}
|
|
1716
1739
|
else if (includeRecent > 0) {
|
|
1717
1740
|
const recent = [
|
|
1718
|
-
...
|
|
1719
|
-
...
|
|
1741
|
+
...localPool.map((entry) => ({ entry, isGlobal: false })),
|
|
1742
|
+
...globalPool.map((entry) => ({ entry, isGlobal: true })),
|
|
1720
1743
|
]
|
|
1721
1744
|
// T2 (src/compare.ts) note: this already carries an explicit
|
|
1722
1745
|
// per-instance tiebreak (created desc -> id localeCompare) and is
|
|
@@ -1784,7 +1807,8 @@ export async function getContext(ctx, opts = {}) {
|
|
|
1784
1807
|
else if (query === '*') {
|
|
1785
1808
|
// No query: return strongest memories by strength, up to budget.
|
|
1786
1809
|
const now = evalNow(); // honors HIPPO_FAKE_NOW (eval-only; see ablation.ts)
|
|
1787
|
-
const
|
|
1810
|
+
const [localPool, globalPool] = oneCopyPerMemory(localEntries, globalEntries, now);
|
|
1811
|
+
const localRanked = localPool
|
|
1788
1812
|
.map((e) => ({
|
|
1789
1813
|
entry: e,
|
|
1790
1814
|
score: calculateStrength(e, now),
|
|
@@ -1792,7 +1816,7 @@ export async function getContext(ctx, opts = {}) {
|
|
|
1792
1816
|
isGlobal: false,
|
|
1793
1817
|
}))
|
|
1794
1818
|
.sort(compareScoredResults);
|
|
1795
|
-
const globalRanked =
|
|
1819
|
+
const globalRanked = globalPool
|
|
1796
1820
|
.map((e) => ({
|
|
1797
1821
|
entry: e,
|
|
1798
1822
|
score: calculateStrength(e, now) * (1 / 1.2),
|
package/dist/autolearn.js
CHANGED
|
@@ -120,7 +120,7 @@ export function deduplicateLesson(hippoRootOrEntries, lesson, threshold = 0.7, t
|
|
|
120
120
|
export function runWatched(command) {
|
|
121
121
|
return new Promise((resolve) => {
|
|
122
122
|
// Use shell: true so the command string is handled by the shell as-is
|
|
123
|
-
const child = spawn(command, { shell: true, stdio: ['inherit', 'inherit', 'pipe'] });
|
|
123
|
+
const child = spawn(command, { shell: true, stdio: ['inherit', 'inherit', 'pipe'], windowsHide: true });
|
|
124
124
|
const stderrChunks = [];
|
|
125
125
|
child.stderr.on('data', (chunk) => {
|
|
126
126
|
stderrChunks.push(chunk);
|
|
@@ -148,6 +148,7 @@ export function isGitRepo(cwd) {
|
|
|
148
148
|
cwd,
|
|
149
149
|
timeout: 10000,
|
|
150
150
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
151
|
+
windowsHide: true,
|
|
151
152
|
});
|
|
152
153
|
return raw.trim() === 'true';
|
|
153
154
|
}
|
|
@@ -163,7 +164,7 @@ export function fetchGitLog(cwd, days) {
|
|
|
163
164
|
try {
|
|
164
165
|
const raw = execFileSync('git', [
|
|
165
166
|
'log', `--since=${days} days ago`, '--pretty=format:%s',
|
|
166
|
-
], { encoding: 'utf8', cwd, timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
167
|
+
], { encoding: 'utf8', cwd, timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
|
|
167
168
|
return raw;
|
|
168
169
|
}
|
|
169
170
|
catch {
|
package/dist/capture.d.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* - Errors / gotchas ("error:", "bug:", "gotcha:", "watch out")
|
|
9
9
|
* - Preferences ("prefer", "use X instead of Y", "don't use")
|
|
10
10
|
*/
|
|
11
|
+
import { type TaskSnapshot } from './store.js';
|
|
11
12
|
export interface ExtractedItem {
|
|
12
13
|
content: string;
|
|
13
14
|
category: string;
|
|
@@ -116,6 +117,8 @@ export declare function preCompactMessage(report: Pick<PreCompactReport, 'snapsh
|
|
|
116
117
|
* pre-compact log, so both hooks find it from the same `--log-file`.
|
|
117
118
|
*/
|
|
118
119
|
export declare function preCompactReportPath(logFile: string): string;
|
|
120
|
+
/** A session's task, summary and next step from its transcript tail, secrets scrubbed and capped, '' where none; null with a logged reason when nothing is derivable. */
|
|
121
|
+
export declare function transcriptWorkingState(transcriptPath: string, log: (message: string) => void): Pick<TaskSnapshot, 'task' | 'summary' | 'next_step'> | null;
|
|
119
122
|
export interface PreCompactOptions {
|
|
120
123
|
stdinText?: string;
|
|
121
124
|
stdinTimedOut?: boolean;
|
package/dist/capture.js
CHANGED
|
@@ -18,7 +18,7 @@ import { embedMemory } from './embeddings.js';
|
|
|
18
18
|
import { isEmbeddingConfigured } from './embedding-provider.js';
|
|
19
19
|
import { resolveTenantId } from './tenant.js';
|
|
20
20
|
import { defaultPreCompactLogPath } from './hooks.js';
|
|
21
|
-
import {
|
|
21
|
+
import { redactSecretsStrict } from './secret-detect.js';
|
|
22
22
|
import { RejectedValueError, checkRejectionGuard } from './rejection.js';
|
|
23
23
|
import { openHippoDb, closeHippoDb } from './db.js';
|
|
24
24
|
import { loadConfig } from './config.js';
|
|
@@ -1080,6 +1080,46 @@ export function preCompactMessage(report) {
|
|
|
1080
1080
|
export function preCompactReportPath(logFile) {
|
|
1081
1081
|
return path.join(path.dirname(logFile), 'pre-compact-last.json');
|
|
1082
1082
|
}
|
|
1083
|
+
/** A session's task, summary and next step from its transcript tail, secrets scrubbed and capped, '' where none; null with a logged reason when nothing is derivable. */
|
|
1084
|
+
export function transcriptWorkingState(transcriptPath, log) {
|
|
1085
|
+
let tail;
|
|
1086
|
+
try {
|
|
1087
|
+
// CX7 (codex round 2): a final record bigger than the window leaves an empty tail, as when a huge tool_result
|
|
1088
|
+
// triggered compaction, so grow the window a bounded number of times until one complete line survives.
|
|
1089
|
+
tail = readTranscriptTail(transcriptPath, PRE_COMPACT_TAIL_BYTES);
|
|
1090
|
+
let prevCap = PRE_COMPACT_TAIL_BYTES;
|
|
1091
|
+
for (const grownCap of [PRE_COMPACT_TAIL_BYTES * 4, PRE_COMPACT_TAIL_BYTES * 16]) {
|
|
1092
|
+
if (tail.trim() !== '')
|
|
1093
|
+
break;
|
|
1094
|
+
if (fs.statSync(transcriptPath).size <= prevCap)
|
|
1095
|
+
break; // already read the whole file
|
|
1096
|
+
log(`tail window grown to ${grownCap} bytes (oversized final record)`);
|
|
1097
|
+
tail = readTranscriptTail(transcriptPath, grownCap);
|
|
1098
|
+
prevCap = grownCap;
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
catch (err) {
|
|
1102
|
+
log(`skip: could not read transcript tail: ${errorMessage(err)}`);
|
|
1103
|
+
return null;
|
|
1104
|
+
}
|
|
1105
|
+
const rawSummary = summariseTranscript(tail);
|
|
1106
|
+
const rawTask = lastPlainUserMessage(tail);
|
|
1107
|
+
const rawNextStep = lastAssistantTextBlock(tail);
|
|
1108
|
+
if (!rawTask.trim() && !rawSummary.trim() && !rawNextStep.trim()) {
|
|
1109
|
+
log('skip: empty summary');
|
|
1110
|
+
return null;
|
|
1111
|
+
}
|
|
1112
|
+
// X9: these fields skip the capture content gate and reach a prompt, so the strict scrub runs. The caps protect the
|
|
1113
|
+
// re-injection token budget and never split a surrogate pair (X2); `hippo snapshot save` stays uncapped.
|
|
1114
|
+
const task = redactSecretsStrict(rawTask);
|
|
1115
|
+
const summary = redactSecretsStrict(rawSummary);
|
|
1116
|
+
const nextStep = redactSecretsStrict(rawNextStep);
|
|
1117
|
+
return {
|
|
1118
|
+
task: task.trim() ? truncateCodePointSafe(task, PRE_COMPACT_TASK_CAP) : '',
|
|
1119
|
+
summary: summary.trim() ? truncateKeepNewest(summary, PRE_COMPACT_SUMMARY_CAP) : '',
|
|
1120
|
+
next_step: nextStep.trim() ? truncateCodePointSafe(nextStep, PRE_COMPACT_NEXT_STEP_CAP) : '',
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1083
1123
|
/** Runs the PreCompact producer: saves a working-state snapshot into `report`. Never extracts memories; SessionEnd capture owns that. */
|
|
1084
1124
|
function runPreCompact(hippoRoot, stdinText, stdinTimedOut, logFile, report) {
|
|
1085
1125
|
// X3: the PreCompact hook fires in every Claude Code project, including
|
|
@@ -1160,40 +1200,10 @@ function runPreCompact(hippoRoot, stdinText, stdinTimedOut, logFile, report) {
|
|
|
1160
1200
|
appendPreCompactLog(logFile, 'skip: no transcript resolved');
|
|
1161
1201
|
return;
|
|
1162
1202
|
}
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
// swallows the whole tail — the seek lands inside it and alignment
|
|
1167
|
-
// drops everything up to its terminator, which is exactly the shape of
|
|
1168
|
-
// a huge tool_result that itself triggered compaction. Grow the window
|
|
1169
|
-
// a bounded number of times until at least one complete line survives.
|
|
1170
|
-
tail = readTranscriptTail(transcriptPath, PRE_COMPACT_TAIL_BYTES);
|
|
1171
|
-
let prevCap = PRE_COMPACT_TAIL_BYTES;
|
|
1172
|
-
for (const grownCap of [PRE_COMPACT_TAIL_BYTES * 4, PRE_COMPACT_TAIL_BYTES * 16]) {
|
|
1173
|
-
if (tail.trim() !== '')
|
|
1174
|
-
break;
|
|
1175
|
-
if (fs.statSync(transcriptPath).size <= prevCap)
|
|
1176
|
-
break; // already read the whole file
|
|
1177
|
-
appendPreCompactLog(logFile, `tail window grown to ${grownCap} bytes (oversized final record)`);
|
|
1178
|
-
tail = readTranscriptTail(transcriptPath, grownCap);
|
|
1179
|
-
prevCap = grownCap;
|
|
1180
|
-
}
|
|
1181
|
-
}
|
|
1182
|
-
catch (err) {
|
|
1183
|
-
appendPreCompactLog(logFile, `skip: could not read transcript tail: ${errorMessage(err)}`);
|
|
1184
|
-
return;
|
|
1185
|
-
}
|
|
1186
|
-
const summaryFull = summariseTranscript(tail);
|
|
1187
|
-
// Snapshot fields must never carry raw secrets, same rule X9 applies below to task/next_step.
|
|
1188
|
-
const scrubbedSummary = redactSecrets(summaryFull);
|
|
1189
|
-
const rawTask = lastPlainUserMessage(tail);
|
|
1190
|
-
const rawNextStep = lastAssistantTextBlock(tail);
|
|
1191
|
-
// Full skip only when every derived field is empty: never clobber a
|
|
1192
|
-
// user-authored active snapshot with junk.
|
|
1193
|
-
if (!rawTask.trim() && !summaryFull.trim() && !rawNextStep.trim()) {
|
|
1194
|
-
appendPreCompactLog(logFile, 'skip: empty summary');
|
|
1203
|
+
// Nothing derivable skips the write, so a user-authored active snapshot is never clobbered with junk.
|
|
1204
|
+
const derived = transcriptWorkingState(transcriptPath, (message) => appendPreCompactLog(logFile, message));
|
|
1205
|
+
if (!derived)
|
|
1195
1206
|
return;
|
|
1196
|
-
}
|
|
1197
1207
|
const tenantId = resolveTenantId({});
|
|
1198
1208
|
// Per-field merge (X1): a tool-heavy tail whose only user turns are
|
|
1199
1209
|
// tool_result arrays derives an empty task even though the summary is
|
|
@@ -1207,15 +1217,6 @@ function runPreCompact(hippoRoot, stdinText, stdinTimedOut, logFile, report) {
|
|
|
1207
1217
|
catch {
|
|
1208
1218
|
// No existing snapshot to merge against — proceed with derived-only.
|
|
1209
1219
|
}
|
|
1210
|
-
// X9: scrub secret-shaped substrings out of freshly-derived text before it
|
|
1211
|
-
// is capped/stored. These fields bypass the normal capture content gate
|
|
1212
|
-
// (they're not extracted items), so this producer is the only place that
|
|
1213
|
-
// ever sees them before they land in task_snapshots. Carried-over
|
|
1214
|
-
// existing field values are NOT re-scrubbed here — they already passed
|
|
1215
|
-
// through this same gate (or were set via `hippo snapshot save`, which is
|
|
1216
|
-
// deliberately untouched, same as the caps below).
|
|
1217
|
-
const scrubbedTask = redactSecrets(rawTask);
|
|
1218
|
-
const scrubbedNextStep = redactSecrets(rawNextStep);
|
|
1219
1220
|
// CX6 (codex round 2): field fallback must never move content across
|
|
1220
1221
|
// sessions — session A's task carried into a snapshot saved under session
|
|
1221
1222
|
// B's id would pass compact-resume's session gate wearing the wrong
|
|
@@ -1225,19 +1226,10 @@ function runPreCompact(hippoRoot, stdinText, stdinTimedOut, logFile, report) {
|
|
|
1225
1226
|
(existing.session_id === null || sessionId === null || existing.session_id === sessionId)
|
|
1226
1227
|
? existing
|
|
1227
1228
|
: null;
|
|
1228
|
-
//
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
const task = scrubbedTask.trim()
|
|
1233
|
-
? truncateCodePointSafe(scrubbedTask, PRE_COMPACT_TASK_CAP)
|
|
1234
|
-
: (fallback?.task ?? '');
|
|
1235
|
-
const summary = scrubbedSummary.trim()
|
|
1236
|
-
? truncateKeepNewest(scrubbedSummary, PRE_COMPACT_SUMMARY_CAP)
|
|
1237
|
-
: (fallback?.summary ?? '');
|
|
1238
|
-
const nextStep = scrubbedNextStep.trim()
|
|
1239
|
-
? truncateCodePointSafe(scrubbedNextStep, PRE_COMPACT_NEXT_STEP_CAP)
|
|
1240
|
-
: (fallback?.next_step ?? '');
|
|
1229
|
+
// Carried-over fields are not re-capped, as `hippo snapshot save` stays uncapped; saveActiveTaskSnapshot scrubs every field.
|
|
1230
|
+
const task = derived.task || (fallback?.task ?? '');
|
|
1231
|
+
const summary = derived.summary || (fallback?.summary ?? '');
|
|
1232
|
+
const nextStep = derived.next_step || (fallback?.next_step ?? '');
|
|
1241
1233
|
// All-empty fields (cross-session tail with nothing derivable) skip the
|
|
1242
1234
|
// write so a foreign session's junk never displaces the owning snapshot.
|
|
1243
1235
|
if (!task && !summary && !nextStep) {
|
package/dist/churn-git.js
CHANGED
|
@@ -10,7 +10,7 @@ export class GitReadError extends Error {
|
|
|
10
10
|
}
|
|
11
11
|
function runGit(args, repoRoot) {
|
|
12
12
|
try {
|
|
13
|
-
return execFileSync('git', args, { cwd: repoRoot, encoding: 'utf8', maxBuffer: GIT_MAX_BUFFER });
|
|
13
|
+
return execFileSync('git', args, { cwd: repoRoot, encoding: 'utf8', maxBuffer: GIT_MAX_BUFFER, windowsHide: true });
|
|
14
14
|
}
|
|
15
15
|
catch (err) {
|
|
16
16
|
throw new GitReadError(`git ${args.join(' ')} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -19,7 +19,7 @@ function runGit(args, repoRoot) {
|
|
|
19
19
|
// git grep exits 1 for "no match" -- a normal empty result, not a failure.
|
|
20
20
|
function runGitGrepOrEmpty(args, repoRoot) {
|
|
21
21
|
try {
|
|
22
|
-
return execFileSync('git', args, { cwd: repoRoot, encoding: 'utf8', maxBuffer: GIT_MAX_BUFFER });
|
|
22
|
+
return execFileSync('git', args, { cwd: repoRoot, encoding: 'utf8', maxBuffer: GIT_MAX_BUFFER, windowsHide: true });
|
|
23
23
|
}
|
|
24
24
|
catch (err) {
|
|
25
25
|
// SAFETY: execFileSync attaches `status` to the thrown Error on a non-zero child exit.
|
package/dist/cli.d.ts
CHANGED
|
@@ -43,10 +43,7 @@ export declare function parseArgs(argv: string[]): {
|
|
|
43
43
|
args: string[];
|
|
44
44
|
flags: Record<string, string | boolean | string[]>;
|
|
45
45
|
};
|
|
46
|
-
/**
|
|
47
|
-
* Scan for Claude Code MEMORY.md files and import new entries into hippo.
|
|
48
|
-
* Looks in ~/.claude/projects/<project>/memory/ for .md files with YAML frontmatter.
|
|
49
|
-
*/
|
|
46
|
+
/** Import new entries from the Claude Code auto memory of the store's project, the frontmatter .md files in ~/.claude/projects/<project>/memory/. */
|
|
50
47
|
export declare function learnFromMemoryMd(hippoRoot: string, homeDir?: string): number;
|
|
51
48
|
/**
|
|
52
49
|
* Render an api.sleep result as console output, byte-identical to the
|
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];
|
|
@@ -547,10 +547,9 @@ function cmdInit(hippoRoot, flags) {
|
|
|
547
547
|
console.log(` No matching commits found in git history.`);
|
|
548
548
|
}
|
|
549
549
|
}
|
|
550
|
-
// Also import from Claude Code / agent MEMORY.md files
|
|
551
550
|
const memImported = learnFromMemoryMd(hippoRoot);
|
|
552
551
|
if (memImported > 0) {
|
|
553
|
-
console.log(` Imported ${memImported} memories from
|
|
552
|
+
console.log(` Imported ${memImported} memories from this project's Claude Code auto memory.`);
|
|
554
553
|
}
|
|
555
554
|
}
|
|
556
555
|
}
|
|
@@ -711,7 +710,7 @@ function setupDailySchedule(globalRoot) {
|
|
|
711
710
|
if (isWindows) {
|
|
712
711
|
// Check if task already exists
|
|
713
712
|
try {
|
|
714
|
-
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 });
|
|
715
714
|
if (existing.includes(taskName)) {
|
|
716
715
|
return; // already scheduled
|
|
717
716
|
}
|
|
@@ -720,7 +719,7 @@ function setupDailySchedule(globalRoot) {
|
|
|
720
719
|
// Task doesn't exist, create it
|
|
721
720
|
}
|
|
722
721
|
try {
|
|
723
|
-
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 });
|
|
724
723
|
console.log(` Scheduled machine-level daily runner (6:15am) via Task Scheduler: ${taskName}`);
|
|
725
724
|
}
|
|
726
725
|
catch {
|
|
@@ -733,13 +732,13 @@ function setupDailySchedule(globalRoot) {
|
|
|
733
732
|
// Unix: check crontab for existing entry
|
|
734
733
|
const marker = `# hippo:${taskName}`;
|
|
735
734
|
try {
|
|
736
|
-
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 });
|
|
737
736
|
if (existing.includes(marker)) {
|
|
738
737
|
return; // already scheduled
|
|
739
738
|
}
|
|
740
739
|
const cronLine = `15 6 * * * ${cmd} ${marker}`;
|
|
741
740
|
const newCrontab = existing.trimEnd() + '\n' + cronLine + '\n';
|
|
742
|
-
execSync('crontab -', { input: newCrontab, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
741
|
+
execSync('crontab -', { input: newCrontab, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
|
|
743
742
|
console.log(` Scheduled machine-level daily runner (6:15am) via crontab`);
|
|
744
743
|
}
|
|
745
744
|
catch {
|
|
@@ -1119,7 +1118,8 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
1119
1118
|
});
|
|
1120
1119
|
}
|
|
1121
1120
|
else if (useMultihop) {
|
|
1122
|
-
|
|
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();
|
|
1123
1123
|
results = multihopSearch(query, allEntries, {
|
|
1124
1124
|
budget,
|
|
1125
1125
|
hippoRoot,
|
|
@@ -2645,25 +2645,43 @@ async function cmdRefine(hippoRoot, flags) {
|
|
|
2645
2645
|
}
|
|
2646
2646
|
}
|
|
2647
2647
|
}
|
|
2648
|
-
/**
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
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/. */
|
|
2652
2678
|
export function learnFromMemoryMd(hippoRoot, homeDir = os.homedir()) {
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
const
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
for (const project of fs.readdirSync(claudeProjectsDir)) {
|
|
2660
|
-
const memDir = path.join(claudeProjectsDir, project, 'memory');
|
|
2661
|
-
if (fs.existsSync(memDir))
|
|
2662
|
-
memoryDirs.push(memDir);
|
|
2663
|
-
}
|
|
2664
|
-
}
|
|
2665
|
-
catch { /* permission denied */ }
|
|
2666
|
-
}
|
|
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));
|
|
2667
2685
|
if (memoryDirs.length === 0)
|
|
2668
2686
|
return 0;
|
|
2669
2687
|
const existing = loadAllEntries(hippoRoot, resolveTenantId({}));
|
|
@@ -2895,10 +2913,10 @@ export function renderSleepResult(result) {
|
|
|
2895
2913
|
}
|
|
2896
2914
|
async function cmdSleepCore(hippoRoot, flags) {
|
|
2897
2915
|
requireInit(hippoRoot);
|
|
2898
|
-
// Phase 1: Auto-learn from git +
|
|
2916
|
+
// Phase 1: Auto-learn from git + Claude Code auto memory (CLI-only, uses process.cwd() / os.homedir()).
|
|
2899
2917
|
// Stays in cli.ts; api.sleep covers Phase 2-6 only.
|
|
2900
2918
|
if (!flags['no-learn'] && flags['dry-run']) {
|
|
2901
|
-
console.log(
|
|
2919
|
+
console.log("Dry run: skipped learning from git commits and this project's Claude Code auto memory.");
|
|
2902
2920
|
}
|
|
2903
2921
|
else if (!flags['no-learn']) {
|
|
2904
2922
|
const config = loadConfig(hippoRoot);
|
|
@@ -2916,10 +2934,9 @@ async function cmdSleepCore(hippoRoot, flags) {
|
|
|
2916
2934
|
console.error(`Churn-staleness check failed for ${root}: ${result.error}`);
|
|
2917
2935
|
}
|
|
2918
2936
|
}
|
|
2919
|
-
// Also learn from Claude Code MEMORY.md files
|
|
2920
2937
|
const memImported = learnFromMemoryMd(hippoRoot);
|
|
2921
2938
|
if (memImported > 0)
|
|
2922
|
-
console.log(`Imported ${memImported} memories from Claude Code
|
|
2939
|
+
console.log(`Imported ${memImported} memories from this project's Claude Code auto memory.`);
|
|
2923
2940
|
}
|
|
2924
2941
|
// Phase 2-6: Pure-storage pipeline (consolidate + dedup + audit + share + ambient).
|
|
2925
2942
|
const ctx = {
|
|
@@ -2933,12 +2950,8 @@ async function cmdSleepCore(hippoRoot, flags) {
|
|
|
2933
2950
|
});
|
|
2934
2951
|
renderSleepResult(result);
|
|
2935
2952
|
}
|
|
2936
|
-
/**
|
|
2937
|
-
*
|
|
2938
|
-
* Called from SessionStart hooks so the user sees the previous session's
|
|
2939
|
-
* consolidation output (SessionEnd hook output is invisible because the TUI
|
|
2940
|
-
* is tearing down when it runs).
|
|
2941
|
-
*/
|
|
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. */
|
|
2942
2955
|
function cmdLastSleep(flags) {
|
|
2943
2956
|
const logPath = typeof flags['path'] === 'string'
|
|
2944
2957
|
? flags['path']
|
|
@@ -2953,11 +2966,11 @@ function cmdLastSleep(flags) {
|
|
|
2953
2966
|
return;
|
|
2954
2967
|
}
|
|
2955
2968
|
if (content.trim().length > 0) {
|
|
2956
|
-
console.
|
|
2957
|
-
process.
|
|
2969
|
+
console.error('=== Previous session hippo consolidation ===');
|
|
2970
|
+
process.stderr.write(content);
|
|
2958
2971
|
if (!content.endsWith('\n'))
|
|
2959
|
-
console.
|
|
2960
|
-
console.
|
|
2972
|
+
console.error();
|
|
2973
|
+
console.error('===========================================');
|
|
2961
2974
|
}
|
|
2962
2975
|
if (!flags['keep']) {
|
|
2963
2976
|
try {
|
|
@@ -3134,7 +3147,7 @@ function collectHandoffEvidence(cwd, testStatus) {
|
|
|
3134
3147
|
let gitRef = null;
|
|
3135
3148
|
try {
|
|
3136
3149
|
gitRef = execFileSync('git', ['rev-parse', 'HEAD'], {
|
|
3137
|
-
cwd, encoding: 'utf8', timeout: 2000, stdio: ['ignore', 'pipe', 'ignore'],
|
|
3150
|
+
cwd, encoding: 'utf8', timeout: 2000, stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true,
|
|
3138
3151
|
}).trim() || null;
|
|
3139
3152
|
}
|
|
3140
3153
|
catch {
|
|
@@ -3143,7 +3156,7 @@ function collectHandoffEvidence(cwd, testStatus) {
|
|
|
3143
3156
|
let dirtyTree = null;
|
|
3144
3157
|
try {
|
|
3145
3158
|
const status = execFileSync('git', ['status', '--porcelain'], {
|
|
3146
|
-
cwd, encoding: 'utf8', timeout: 2000, stdio: ['ignore', 'pipe', 'ignore'],
|
|
3159
|
+
cwd, encoding: 'utf8', timeout: 2000, stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true,
|
|
3147
3160
|
});
|
|
3148
3161
|
dirtyTree = status.trim().length > 0;
|
|
3149
3162
|
}
|
|
@@ -3160,9 +3173,9 @@ async function cmdSessionEndWorker(hippoRoot, flags) {
|
|
|
3160
3173
|
// sleep errors are already tee'd to the log file via cmdSleep's
|
|
3161
3174
|
// `[hippo] sleep failed: ...` line. Continue to capture regardless.
|
|
3162
3175
|
}
|
|
3176
|
+
const transcriptPath = typeof flags['transcript'] === 'string' ? flags['transcript'] : undefined;
|
|
3163
3177
|
try {
|
|
3164
3178
|
const logFile = typeof flags['log-file'] === 'string' ? flags['log-file'] : undefined;
|
|
3165
|
-
const transcriptPath = typeof flags['transcript'] === 'string' ? flags['transcript'] : undefined;
|
|
3166
3179
|
// With no stdin of its own, capture would read this as a manual run and scan every project.
|
|
3167
3180
|
if (!transcriptPath) {
|
|
3168
3181
|
appendSessionEndCloseLog(logFile ?? null, 'skip capture: no transcript for this session');
|
|
@@ -3195,14 +3208,19 @@ async function cmdSessionEndWorker(hippoRoot, flags) {
|
|
|
3195
3208
|
// snapshot writeSessionEndHandoff reads is still active.
|
|
3196
3209
|
if (closeSessionId) {
|
|
3197
3210
|
try {
|
|
3198
|
-
const
|
|
3199
|
-
|
|
3200
|
-
|
|
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');
|
|
3201
3219
|
}
|
|
3202
3220
|
else {
|
|
3203
3221
|
const evidence = collectHandoffEvidence(process.cwd(), 'unknown');
|
|
3204
|
-
const handoff = writeSessionEndHandoff(hippoRoot,
|
|
3205
|
-
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}`);
|
|
3206
3224
|
}
|
|
3207
3225
|
}
|
|
3208
3226
|
catch (err) {
|
|
@@ -6127,7 +6145,7 @@ function cmdGraph(hippoRoot, args, flags) {
|
|
|
6127
6145
|
: process.platform === 'darwin'
|
|
6128
6146
|
? ['open', [out]]
|
|
6129
6147
|
: ['xdg-open', [out]];
|
|
6130
|
-
const child = spawn(cmd, cmdArgs, { detached: true, stdio: 'ignore' });
|
|
6148
|
+
const child = spawn(cmd, cmdArgs, { detached: true, stdio: 'ignore', windowsHide: true });
|
|
6131
6149
|
// A missing launcher (e.g. xdg-open absent) emits 'error' asynchronously;
|
|
6132
6150
|
// an unhandled 'error' event would throw, so swallow it — the file is
|
|
6133
6151
|
// already written and its path printed above.
|
|
@@ -6705,6 +6723,7 @@ function autoDetectContext() {
|
|
|
6705
6723
|
const diff = execSync('git diff --name-only HEAD 2>&1', {
|
|
6706
6724
|
encoding: 'utf8',
|
|
6707
6725
|
timeout: 3000,
|
|
6726
|
+
windowsHide: true,
|
|
6708
6727
|
}).trim();
|
|
6709
6728
|
if (diff) {
|
|
6710
6729
|
// Extract meaningful terms from file paths
|
|
@@ -6720,6 +6739,7 @@ function autoDetectContext() {
|
|
|
6720
6739
|
const branch = execSync('git branch --show-current 2>&1', {
|
|
6721
6740
|
encoding: 'utf8',
|
|
6722
6741
|
timeout: 3000,
|
|
6742
|
+
windowsHide: true,
|
|
6723
6743
|
}).trim();
|
|
6724
6744
|
if (branch && branch !== 'main' && branch !== 'master') {
|
|
6725
6745
|
return branch.replace(/[-_\/]/g, ' ');
|
|
@@ -9067,7 +9087,7 @@ Commands:
|
|
|
9067
9087
|
--all Install for every JSON-hook tool, even if not detected
|
|
9068
9088
|
--dry-run Show what would be installed without writing
|
|
9069
9089
|
--no-schedule Skip installing or repairing the daily runner
|
|
9070
|
-
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
|
|
9071
9091
|
--path <p> Log path (default: ~/.hippo/logs/last-sleep.log)
|
|
9072
9092
|
--keep Print without clearing
|
|
9073
9093
|
session-end SessionEnd hook: run sleep, then capture this session, in a detached worker
|