hippo-memory 1.56.0 → 1.58.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -0
- package/dist/agent-memories/claude-code.js +1 -1
- package/dist/agent-memories/gemini.js +1 -1
- package/dist/api-errors.d.ts +27 -0
- package/dist/api-errors.js +37 -0
- package/dist/api.d.ts +21 -14
- package/dist/api.js +97 -71
- package/dist/audit.d.ts +4 -0
- package/dist/audit.js +11 -0
- package/dist/autolearn.d.ts +1 -1
- package/dist/autolearn.js +7 -5
- package/dist/capture-contract.d.ts +47 -0
- package/dist/capture-contract.js +49 -0
- package/dist/capture-error.js +2 -1
- package/dist/capture.d.ts +0 -13
- package/dist/capture.js +5 -66
- package/dist/card-detail.d.ts +1 -1
- package/dist/card-detail.js +1 -1
- package/dist/cli/shared.d.ts +137 -0
- package/dist/cli/shared.js +834 -0
- package/dist/cli/sleep.d.ts +10 -0
- package/dist/cli/sleep.js +171 -0
- package/dist/cli.d.ts +0 -7
- package/dist/cli.js +322 -1827
- package/dist/client.js +9 -0
- package/dist/codex-patch.js +1 -1
- package/dist/compaction-record.d.ts +1 -1
- package/dist/compaction-record.js +3 -2
- package/dist/config.d.ts +5 -0
- package/dist/config.js +17 -0
- package/dist/connectors/github/dlq.js +5 -2
- package/dist/connectors/github/octokit-client.js +4 -2
- package/dist/connectors/github/webhook.d.ts +19 -0
- package/dist/connectors/github/webhook.js +313 -0
- package/dist/connectors/slack/dlq.js +6 -2
- package/dist/connectors/slack/web-client.js +7 -5
- package/dist/connectors/slack/webhook.d.ts +22 -0
- package/dist/connectors/slack/webhook.js +203 -0
- package/dist/consolidate.d.ts +10 -0
- package/dist/consolidate.js +38 -35
- package/dist/context-auto.d.ts +3 -0
- package/dist/context-auto.js +34 -0
- package/dist/customer-notes.js +16 -14
- package/dist/dag.js +3 -2
- package/dist/dashboard.js +3 -2
- package/dist/db.d.ts +12 -0
- package/dist/db.js +62 -1
- package/dist/decisions.js +11 -9
- package/dist/doctor.js +5 -0
- package/dist/embedding-provider.js +3 -3
- package/dist/embeddings.d.ts +4 -4
- package/dist/embeddings.js +72 -16
- package/dist/eval-stats.d.ts +58 -0
- package/dist/eval-stats.js +111 -0
- package/dist/extract.js +3 -2
- package/dist/goals.d.ts +49 -25
- package/dist/goals.js +39 -22
- package/dist/graph-extract.js +1 -1
- package/dist/graph-recall.d.ts +1 -1
- package/dist/graph-recall.js +1 -1
- package/dist/graph.js +1 -1
- package/dist/hooks.d.ts +1 -3
- package/dist/hooks.js +2 -4
- package/dist/http-retry.d.ts +21 -0
- package/dist/http-retry.js +50 -0
- package/dist/http-util.d.ts +39 -0
- package/dist/http-util.js +56 -0
- package/dist/importers.d.ts +2 -0
- package/dist/importers.js +16 -5
- package/dist/incidents.js +13 -11
- package/dist/index.d.ts +5 -2
- package/dist/index.js +5 -2
- package/dist/judgment.js +10 -17
- package/dist/log.d.ts +25 -0
- package/dist/log.js +48 -0
- package/dist/mcp/server.js +224 -308
- package/dist/mcp/tool-args.d.ts +21 -0
- package/dist/mcp/tool-args.js +80 -0
- package/dist/memory.d.ts +19 -0
- package/dist/memory.js +41 -2
- package/dist/overlap-index.d.ts +7 -0
- package/dist/overlap-index.js +38 -0
- package/dist/pilot-arm.d.ts +9 -0
- package/dist/pilot-arm.js +47 -0
- package/dist/policies.js +14 -12
- package/dist/predictions.js +11 -9
- package/dist/processes.js +16 -14
- package/dist/project-briefs.js +19 -16
- package/dist/project-identity.d.ts +1 -1
- package/dist/project-identity.js +25 -1
- package/dist/prompt-recall.js +1 -1
- package/dist/raw-archive.js +7 -6
- package/dist/recall-history.d.ts +5 -0
- package/dist/recall-history.js +9 -0
- package/dist/recall-pipeline.d.ts +101 -0
- package/dist/recall-pipeline.js +313 -0
- package/dist/recall-scope.d.ts +24 -1
- package/dist/recall-scope.js +29 -2
- package/dist/refine-llm.js +3 -2
- package/dist/reject-flow.js +6 -9
- package/dist/rejection.d.ts +2 -1
- package/dist/rejection.js +2 -1
- package/dist/search.d.ts +0 -20
- package/dist/search.js +16 -51
- package/dist/secret-detect.d.ts +13 -1
- package/dist/secret-detect.js +33 -1
- package/dist/server.d.ts +3 -1
- package/dist/server.js +1854 -2566
- package/dist/session-digest.js +2 -1
- package/dist/shared.js +7 -6
- package/dist/skills.js +17 -15
- package/dist/store-cards.d.ts +53 -0
- package/dist/store-cards.js +512 -0
- package/dist/store.d.ts +2 -89
- package/dist/store.js +10 -566
- package/dist/tenant.d.ts +22 -0
- package/dist/tenant.js +26 -0
- package/dist/token-ledger.d.ts +4 -2
- package/dist/token-ledger.js +2 -2
- package/dist/tokenize.d.ts +2 -0
- package/dist/tokenize.js +8 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- 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/connectors/slack/ratelimit.d.ts +0 -9
- package/dist/connectors/slack/ratelimit.js +0 -18
|
@@ -0,0 +1,834 @@
|
|
|
1
|
+
// Helpers two or more CLI verbs use, split from cli.ts so a verb can move to its own file without importing cli.ts.
|
|
2
|
+
// This module must never import cli.ts.
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import * as fs from 'fs';
|
|
5
|
+
import { execFileSync, execSync } from 'child_process';
|
|
6
|
+
import { installJsonHooks, CODEX_TRUST_LINE } from '../hooks.js';
|
|
7
|
+
import { confidenceLabel, computeSchemaFit, createMemory, Layer } from '../memory.js';
|
|
8
|
+
import { isInitialized, loadAllEntries, writeEntry, updateStats } from '../store.js';
|
|
9
|
+
import { RejectedValueError } from '../rejection.js';
|
|
10
|
+
import { explainMatch } from '../search.js';
|
|
11
|
+
import { embedMemory } from '../embeddings.js';
|
|
12
|
+
import { loadConfig } from '../config.js';
|
|
13
|
+
import { openHippoDb, closeHippoDb, isSqliteBusy, noteStoreBusy } from '../db.js';
|
|
14
|
+
import { hookPayloadSessionId, isSubagentPayload, recordTokenUse } from '../token-ledger.js';
|
|
15
|
+
import { isGitRepo, fetchGitLog, extractLessons, partitionLessons } from '../autolearn.js';
|
|
16
|
+
import { storedTextKeys, duplicateKey } from '../same-text.js';
|
|
17
|
+
import { importAtSessionEnd, currentMachine } from '../agent-memories/sync.js';
|
|
18
|
+
import { summaryLine } from '../agent-memories/report.js';
|
|
19
|
+
import { detectChurnStale, extractInvalidationTarget, invalidateMatching } from '../invalidation.js';
|
|
20
|
+
import { resolveProjectIdentity } from '../project-identity.js';
|
|
21
|
+
import { extractPathTags } from '../path-context.js';
|
|
22
|
+
import { getGlobalRoot, initGlobal } from '../shared.js';
|
|
23
|
+
import { DAILY_TASK_NAME, buildDailyRunnerCommand, buildSchtasksCreateArgs, buildWindowsTaskRun } from '../scheduler.js';
|
|
24
|
+
import { sanitizeLogMessage } from '../capture.js';
|
|
25
|
+
import { appendAuditEvent, reportAuditWriteFailure } from '../audit.js';
|
|
26
|
+
import { createHash } from 'node:crypto';
|
|
27
|
+
import * as client from '../client.js';
|
|
28
|
+
import { detectServer, removePidfileIfOwned } from '../server-detect.js';
|
|
29
|
+
import { resolveTenantId } from '../tenant.js';
|
|
30
|
+
import { snapshotText, sessionTrailText, handoffText } from '../context-render.js';
|
|
31
|
+
export function parseLimitFlag(value) {
|
|
32
|
+
if (!value)
|
|
33
|
+
return Infinity;
|
|
34
|
+
const parsed = parseInt(String(value), 10);
|
|
35
|
+
return Number.isFinite(parsed) && parsed >= 1 ? parsed : Infinity;
|
|
36
|
+
}
|
|
37
|
+
export function parseCountFlag(value) {
|
|
38
|
+
if (!value || value === true || Array.isArray(value))
|
|
39
|
+
return 0;
|
|
40
|
+
const parsed = parseInt(String(value), 10);
|
|
41
|
+
return Number.isFinite(parsed) && parsed >= 1 ? parsed : 0;
|
|
42
|
+
}
|
|
43
|
+
export function parseBudgetFlag(value, fallback) {
|
|
44
|
+
if (value === undefined)
|
|
45
|
+
return fallback;
|
|
46
|
+
// A value-less flag and a junk value are different typos; the --hops guard already splits them.
|
|
47
|
+
if (typeof value !== 'string') {
|
|
48
|
+
console.error('--budget requires an integer value (e.g. --budget 1500).');
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
// Number(), like the --hops guard: parseInt('12abc') is 12, silently accepting what this message rejects.
|
|
52
|
+
const parsed = Number(value);
|
|
53
|
+
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
54
|
+
console.error(`Invalid --budget: "${value}". Must be a non-negative integer.`);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
return parsed;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Emit an audit event against `hippoRoot`'s db. Opens its own short-lived
|
|
61
|
+
* connection so callers don't have to thread a db handle. Swallows all errors
|
|
62
|
+
* — audit must never crash a CLI command.
|
|
63
|
+
*/
|
|
64
|
+
export function emitCliAudit(hippoRoot, op, targetId, metadata) {
|
|
65
|
+
try {
|
|
66
|
+
const db = openHippoDb(hippoRoot);
|
|
67
|
+
try {
|
|
68
|
+
appendAuditEvent(db, {
|
|
69
|
+
tenantId: resolveTenantId({}),
|
|
70
|
+
actor: 'cli',
|
|
71
|
+
op,
|
|
72
|
+
targetId,
|
|
73
|
+
metadata,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
closeHippoDb(db);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
// Best effort: the command already did its work.
|
|
82
|
+
reportAuditWriteFailure(op, String(error), targetId);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export function requireInit(hippoRoot) {
|
|
86
|
+
if (!isInitialized(hippoRoot)) {
|
|
87
|
+
console.error(`No hippo store at ${hippoRoot} (searched ${process.cwd()} and its parents up to your home directory). Run \`hippo init\` first.`);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/** Runs detectChurnStale against every store this repo's memories can live in. */
|
|
92
|
+
export function runChurnStaleForRepo(hippoRoot, dryRun) {
|
|
93
|
+
const repoRoot = execFileSync('git', ['rev-parse', '--show-toplevel'], { cwd: process.cwd(), encoding: 'utf8', windowsHide: true }).trim();
|
|
94
|
+
const projectName = resolveProjectIdentity(process.cwd()).name;
|
|
95
|
+
const globalRoot = getGlobalRoot();
|
|
96
|
+
const roots = globalRoot !== hippoRoot && isInitialized(globalRoot) ? [hippoRoot, globalRoot] : [hippoRoot];
|
|
97
|
+
const tenantId = resolveTenantId({});
|
|
98
|
+
return roots.map((root) => {
|
|
99
|
+
// One store failing must not abort sleep's later phases or skip the other store.
|
|
100
|
+
try {
|
|
101
|
+
return { root, result: detectChurnStale(root, repoRoot, { tenantId, projectName, dryRun }) };
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
105
|
+
return { root, result: { checked: 0, marked: 0, alreadyMarked: 0, skippedPinned: [], dryRun, preview: [], error: message } };
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* When HIPPO_REQUIRE_SERVER is set, the CLI must not silently fall back to
|
|
111
|
+
* direct DB mode — a missing server then masks a real misconfiguration (the
|
|
112
|
+
* configured HIPPO_API_KEY is also silently discarded on fallback). Throws a
|
|
113
|
+
* clear error then. It guards only the routed writes (remember, forget, archive,
|
|
114
|
+
* promote); every other command opens the store directly, knob or not.
|
|
115
|
+
*/
|
|
116
|
+
function failIfServerRequired(reason) {
|
|
117
|
+
if (process.env['HIPPO_REQUIRE_SERVER']) {
|
|
118
|
+
throw new Error(`hippo: HIPPO_REQUIRE_SERVER is set but ${reason}. ` +
|
|
119
|
+
`Start \`hippo serve\`, or unset HIPPO_REQUIRE_SERVER to allow direct-mode fallback.`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Run an HTTP-routed command if a `hippo serve` instance is detected for
|
|
124
|
+
* `hippoRoot`. Returns:
|
|
125
|
+
* - true if the HTTP path ran (success OR a structured server error that
|
|
126
|
+
* was already surfaced to stdout/stderr by `httpFn`),
|
|
127
|
+
* - false if no server was detected, or if the detected pidfile turned out
|
|
128
|
+
* to be stale (connection refused). On stale, the pidfile is removed
|
|
129
|
+
* if it still names that dead server (a newer one may have replaced
|
|
130
|
+
* it) and the caller should fall back to the direct path.
|
|
131
|
+
*
|
|
132
|
+
* Stale pidfiles must self-heal, not crash.
|
|
133
|
+
* When HIPPO_REQUIRE_SERVER is set, both fallback paths throw instead of
|
|
134
|
+
* returning false, so a missing server fails loudly rather than silently
|
|
135
|
+
* degrading to direct mode.
|
|
136
|
+
*/
|
|
137
|
+
export async function runViaServerIfAvailable(hippoRoot, httpFn) {
|
|
138
|
+
const info = await detectServer(hippoRoot);
|
|
139
|
+
if (!info) {
|
|
140
|
+
failIfServerRequired('no running server was detected for this hippoRoot');
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
const apiKey = process.env['HIPPO_API_KEY'];
|
|
144
|
+
try {
|
|
145
|
+
await httpFn(info, apiKey);
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
const failure = client.classifyTransportFailure(err);
|
|
150
|
+
if (failure === 'never-sent') {
|
|
151
|
+
failIfServerRequired('the server pidfile was stale (connection refused)');
|
|
152
|
+
console.error('hippo: stale server pidfile detected, falling back to direct mode');
|
|
153
|
+
// Clear the pidfile only if it still names the dead server we just
|
|
154
|
+
// probed — a newer server may have rewritten it (removePidfileIfOwned).
|
|
155
|
+
removePidfileIfOwned(hippoRoot, { pid: info.pid, startedAt: info.started_at });
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
if (failure === 'delivery-unknown') {
|
|
159
|
+
// Every caller of this helper is a non-idempotent write, so replaying on
|
|
160
|
+
// the direct path would store a row the server may already have committed.
|
|
161
|
+
// Leave the pidfile alone: the next command's connect-phase failure heals it.
|
|
162
|
+
console.error(`hippo: the connection to ${info.url} dropped or timed out mid-request, so the write may already have been applied. Not retrying locally. Check with \`hippo recall\` before running this again.`);
|
|
163
|
+
process.exit(1);
|
|
164
|
+
}
|
|
165
|
+
throw err;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
export function fmt(n, digits = 2) {
|
|
169
|
+
return n.toFixed(digits);
|
|
170
|
+
}
|
|
171
|
+
// What `hippo recall` prints for one result; the budget prices this same text.
|
|
172
|
+
export function recallEntryText(r, query, showWhy, isGlobal) {
|
|
173
|
+
const e = r.entry;
|
|
174
|
+
const label = confidenceLabel(e);
|
|
175
|
+
const confLabel = label.warn ? `[${label.text}] ⚠️` : `[${label.text}]`;
|
|
176
|
+
const bars = Math.round(e.strength * 10);
|
|
177
|
+
const graphMark = r.graphVia ? ` [graph: ${r.graphVia.hops}hop ${r.graphVia.relType}]` : '';
|
|
178
|
+
const lines = [
|
|
179
|
+
`--- ${e.id} [${e.layer}] ${confLabel}${isGlobal ? ' [global]' : ''}${e.superseded_by ? ' [superseded]' : ''}${graphMark} score=${fmt(r.score, 3)} strength=${fmt(e.strength)}`,
|
|
180
|
+
` [${'█'.repeat(bars)}${'░'.repeat(10 - bars)}] tags: ${e.tags.join(', ') || 'none'} | retrieved: ${e.retrieval_count}x`,
|
|
181
|
+
];
|
|
182
|
+
if (showWhy) {
|
|
183
|
+
const explanation = explainMatch(query, r);
|
|
184
|
+
lines.push(` source:${isGlobal ? ' [global]' : ' [local]'} | layer: [${e.layer}] | confidence: [${label.text}]`, ` reason: ${explanation.reason}`);
|
|
185
|
+
const env = explanation.envelope;
|
|
186
|
+
if (env) {
|
|
187
|
+
lines.push(` kind: ${env.kind}`);
|
|
188
|
+
if (env.scope)
|
|
189
|
+
lines.push(` scope: ${env.scope}`);
|
|
190
|
+
if (env.owner)
|
|
191
|
+
lines.push(` owner: ${env.owner}`);
|
|
192
|
+
if (env.artifact_ref)
|
|
193
|
+
lines.push(` artifact_ref: ${env.artifact_ref}`);
|
|
194
|
+
if (env.session_id)
|
|
195
|
+
lines.push(` session_id: ${env.session_id}`);
|
|
196
|
+
lines.push(` confidence: ${env.confidence}`);
|
|
197
|
+
}
|
|
198
|
+
// The recall trace, e.g. "ranking: base 0.420 -> interference x0.30 -> 0.126 -> goal-boost x1.50 -> 0.189".
|
|
199
|
+
if (r.rerankTrace && r.rerankTrace.length > 0) {
|
|
200
|
+
const parts = [`base ${fmt(r.rerankTrace[0].scoreBefore, 3)}`];
|
|
201
|
+
for (const step of r.rerankTrace) {
|
|
202
|
+
parts.push(`${step.stage}${step.multiplier !== undefined ? ` x${fmt(step.multiplier, 2)}` : ''}`, fmt(step.scoreAfter, 3));
|
|
203
|
+
}
|
|
204
|
+
lines.push(` ranking: ${parts.join(' -> ')}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
lines.push('', e.content, '');
|
|
208
|
+
return lines.join('\n');
|
|
209
|
+
}
|
|
210
|
+
export function recallHeading(entries, tokens, query) {
|
|
211
|
+
return `Found ${entries} memories (${tokens} tokens) for: "${query}"\n`;
|
|
212
|
+
}
|
|
213
|
+
/** One line when an agent memory import moved anything; its warnings go to stderr. */
|
|
214
|
+
export function printAgentImport(report, indent = ' ') {
|
|
215
|
+
const line = summaryLine(report);
|
|
216
|
+
if (line !== null)
|
|
217
|
+
console.log(`${indent}${line}`);
|
|
218
|
+
for (const warning of report.warnings)
|
|
219
|
+
console.error(`hippo: agent memories: ${warning}`);
|
|
220
|
+
}
|
|
221
|
+
/** The first hippo block in `text` and the agent whose current or shipped text it is; `owner` is undefined for an edited block. */
|
|
222
|
+
export function hippoBlock(text) {
|
|
223
|
+
const at = text.indexOf(HOOK_MARKERS.start);
|
|
224
|
+
const start = at + HOOK_MARKERS.start.length;
|
|
225
|
+
const end = text.indexOf(HOOK_MARKERS.end, start);
|
|
226
|
+
if (at < 0 || end < 0)
|
|
227
|
+
return null;
|
|
228
|
+
// git autocrlf checks these files out with CRLF: match as LF, write back in the file's own ending.
|
|
229
|
+
const raw = text.slice(start, end);
|
|
230
|
+
const inner = raw.replace(/\r\n/g, '\n').trim();
|
|
231
|
+
const owner = Object.keys(HOOKS).find((k) => HOOKS[k].content === inner) ?? SHIPPED_HOOK_HASHES.get(createHash('sha256').update(inner).digest('hex'));
|
|
232
|
+
return { start, end, eol: raw.includes('\r\n') ? '\r\n' : '\n', inner, owner };
|
|
233
|
+
}
|
|
234
|
+
/** Adds hippo's two Codex hooks and says what changed; each install ends on the trust reminder, since Codex skips an untrusted hook. */
|
|
235
|
+
export function installCodexMemoryHooks(indent) {
|
|
236
|
+
const result = installJsonHooks('codex');
|
|
237
|
+
if (result.invalidJson) {
|
|
238
|
+
console.log(`${indent}WARNING: ${result.settingsPath} is not a hooks file hippo can merge into; fix it, then run \`hippo hook install codex\`.`);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const added = [
|
|
242
|
+
result.installedUserPromptSubmit ? 'UserPromptSubmit' : '',
|
|
243
|
+
result.installedCompactResume ? 'SessionStart(compact)' : '',
|
|
244
|
+
].filter(Boolean);
|
|
245
|
+
console.log(added.length > 0
|
|
246
|
+
? `${indent}Installed hippo's Codex memory hooks (${added.join(', ')}) in ${result.settingsPath}`
|
|
247
|
+
: `${indent}hippo's Codex memory hooks already in ${result.settingsPath}`);
|
|
248
|
+
console.log(`${indent}${CODEX_TRUST_LINE}`);
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Set up a machine-level daily runner that sweeps all registered Hippo
|
|
252
|
+
* workspaces.
|
|
253
|
+
* Linux/macOS: writes to user crontab.
|
|
254
|
+
* Windows: creates a scheduled task.
|
|
255
|
+
* Skips if already installed.
|
|
256
|
+
*/
|
|
257
|
+
export function setupDailySchedule(globalRoot) {
|
|
258
|
+
const runnerDir = path.resolve(globalRoot);
|
|
259
|
+
// Reject paths with characters that could break shell/crontab quoting
|
|
260
|
+
// (backslash is normal on Windows, only dangerous in Unix shell/crontab)
|
|
261
|
+
const unsafeChars = process.platform === 'win32' ? /["`$%\n\r]/ : /["`$\n\r\\]/;
|
|
262
|
+
if (unsafeChars.test(runnerDir)) {
|
|
263
|
+
console.log(` Skipping schedule: runner path contains unsafe characters.`);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const isWindows = process.platform === 'win32';
|
|
267
|
+
const taskName = DAILY_TASK_NAME;
|
|
268
|
+
const cmd = buildDailyRunnerCommand(runnerDir);
|
|
269
|
+
if (isWindows) {
|
|
270
|
+
// Check if task already exists
|
|
271
|
+
try {
|
|
272
|
+
const existing = execSync(`schtasks /query /tn "${taskName}" 2>nul`, { encoding: 'utf-8', windowsHide: true });
|
|
273
|
+
if (existing.includes(taskName)) {
|
|
274
|
+
return; // already scheduled
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
// Task doesn't exist, create it
|
|
279
|
+
}
|
|
280
|
+
try {
|
|
281
|
+
execFileSync('schtasks', buildSchtasksCreateArgs(taskName, cmd), { stdio: 'pipe', windowsHide: true });
|
|
282
|
+
console.log(` Scheduled machine-level daily runner (6:15am) via Task Scheduler: ${taskName}`);
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
// No admin rights or schtasks unavailable, fall back to printing instructions
|
|
286
|
+
console.log(` To schedule the machine-level daily runner, run:`);
|
|
287
|
+
console.log(` schtasks /create /tn "${taskName}" /tr "${buildWindowsTaskRun(cmd).replace(/"/g, '\\"')}" /sc daily /st 06:15`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
// Unix: check crontab for existing entry
|
|
292
|
+
const marker = `# hippo:${taskName}`;
|
|
293
|
+
try {
|
|
294
|
+
const existing = execSync('crontab -l 2>/dev/null', { encoding: 'utf-8', windowsHide: true });
|
|
295
|
+
if (existing.includes(marker)) {
|
|
296
|
+
return; // already scheduled
|
|
297
|
+
}
|
|
298
|
+
const cronLine = `15 6 * * * ${cmd} ${marker}`;
|
|
299
|
+
const newCrontab = existing.trimEnd() + '\n' + cronLine + '\n';
|
|
300
|
+
execSync('crontab -', { input: newCrontab, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
|
|
301
|
+
console.log(` Scheduled machine-level daily runner (6:15am) via crontab`);
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
const cronLine = `15 6 * * * ${cmd}`;
|
|
305
|
+
console.log(` To schedule the machine-level daily runner, add to crontab (crontab -e):`);
|
|
306
|
+
console.log(` ${cronLine}`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
export function parseAsOfFlag(flags) {
|
|
311
|
+
const asOf = typeof flags['as-of'] === 'string' ? flags['as-of'] : undefined;
|
|
312
|
+
if (asOf !== undefined && Number.isNaN(new Date(asOf).getTime())) {
|
|
313
|
+
console.error(`Error: --as-of value "${asOf}" is not a valid ISO date (e.g. 2026-04-22 or 2026-04-22T12:00:00Z).`);
|
|
314
|
+
process.exit(1);
|
|
315
|
+
}
|
|
316
|
+
return asOf;
|
|
317
|
+
}
|
|
318
|
+
/** --physics forces physics, --classic forces BM25+cosine, else physics unless the config turns it off. */
|
|
319
|
+
export function engineFlags(flags, config) {
|
|
320
|
+
return {
|
|
321
|
+
usePhysics: Boolean(flags['physics']) || (!flags['classic'] && config.physics.enabled !== false),
|
|
322
|
+
physicsConfig: config.physics,
|
|
323
|
+
mmr: !flags['no-mmr'] && config.mmr.enabled,
|
|
324
|
+
mmrLambda: flags['mmr-lambda'] !== undefined ? parseFloat(String(flags['mmr-lambda'])) : config.mmr.lambda,
|
|
325
|
+
localBump: flags['equal-sources']
|
|
326
|
+
? 1.0
|
|
327
|
+
: flags['local-bump'] !== undefined ? parseFloat(String(flags['local-bump'])) : config.search.localBump,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Detached worker that counts re-reads, runs sleep, then capture. Invoked via the internal
|
|
332
|
+
* `__session-end-worker` subcommand (not user-facing). Failures in one stage
|
|
333
|
+
* do not block the other.
|
|
334
|
+
*/
|
|
335
|
+
// Best-effort git state; a missing git, non-repo cwd, or the timeout all
|
|
336
|
+
// yield null fields rather than throw (autolearn.ts execFileSync shape).
|
|
337
|
+
export function collectHandoffEvidence(cwd, testStatus) {
|
|
338
|
+
let gitRef = null;
|
|
339
|
+
try {
|
|
340
|
+
gitRef = execFileSync('git', ['rev-parse', 'HEAD'], {
|
|
341
|
+
cwd, encoding: 'utf8', timeout: 2000, stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true,
|
|
342
|
+
}).trim() || null;
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
gitRef = null;
|
|
346
|
+
}
|
|
347
|
+
let dirtyTree = null;
|
|
348
|
+
try {
|
|
349
|
+
const status = execFileSync('git', ['status', '--porcelain'], {
|
|
350
|
+
cwd, encoding: 'utf8', timeout: 2000, stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true,
|
|
351
|
+
});
|
|
352
|
+
dirtyTree = status.trim().length > 0;
|
|
353
|
+
}
|
|
354
|
+
catch {
|
|
355
|
+
dirtyTree = null;
|
|
356
|
+
}
|
|
357
|
+
return { gitRef, dirtyTree, testStatus };
|
|
358
|
+
}
|
|
359
|
+
/** A folder without its own store never sleeps at session end, so its project's agent notes go to the global store here. */
|
|
360
|
+
export function logSessionEndImport(logFile, transcriptPath) {
|
|
361
|
+
try {
|
|
362
|
+
const report = importAtSessionEnd(process.cwd(), transcriptPath, { machine: currentMachine() });
|
|
363
|
+
const line = summaryLine(report);
|
|
364
|
+
if (line !== null)
|
|
365
|
+
appendSessionEndCloseLog(logFile, line);
|
|
366
|
+
for (const warning of report.warnings)
|
|
367
|
+
appendSessionEndCloseLog(logFile, `agent memories: ${warning}`);
|
|
368
|
+
}
|
|
369
|
+
catch (err) {
|
|
370
|
+
appendSessionEndCloseLog(logFile, `agent memory import failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Best-effort log line for the snapshot-close step in
|
|
375
|
+
* `cmdSessionEndWorker`. `cmdSleep`/`cmdCapture` each tee console output to
|
|
376
|
+
* `logFile` only for their own duration (the tee is restored before this
|
|
377
|
+
* runs), so a plain `console.log` here would be silently discarded under
|
|
378
|
+
* the detached worker's `stdio: 'ignore'` — write straight to the file
|
|
379
|
+
* instead, matching capture.ts's `appendPreCompactLog` convention.
|
|
380
|
+
*/
|
|
381
|
+
export function appendSessionEndCloseLog(logFile, message, opts = {}) {
|
|
382
|
+
if (!logFile)
|
|
383
|
+
return;
|
|
384
|
+
try {
|
|
385
|
+
fs.mkdirSync(path.dirname(logFile), { recursive: true });
|
|
386
|
+
// sanitizeLogMessage: `message` interpolates the payload-controlled
|
|
387
|
+
// session_id — same log-forgery guard appendPreCompactLog applies.
|
|
388
|
+
const write = opts.startFresh ? fs.writeFileSync : fs.appendFileSync;
|
|
389
|
+
write(logFile, `[hippo] ${new Date().toISOString()} ${sanitizeLogMessage(message)}\n`, 'utf8');
|
|
390
|
+
}
|
|
391
|
+
catch {
|
|
392
|
+
// Best-effort only — never let a log-write failure surface as an error.
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
export function printActiveTaskSnapshot(snapshot) {
|
|
396
|
+
console.log(snapshotText(snapshot));
|
|
397
|
+
}
|
|
398
|
+
export function printSessionEvents(events) {
|
|
399
|
+
console.log(events.length === 0 ? 'No session events found.' : sessionTrailText(events));
|
|
400
|
+
}
|
|
401
|
+
export function printHandoff(handoff) {
|
|
402
|
+
console.log(handoffText(handoff));
|
|
403
|
+
}
|
|
404
|
+
// parseArgs turns a value-less flag into `true`; refuse rather than silently
|
|
405
|
+
// stringifying it (String(true) === 'true'), mirroring cmdHandoff's guard.
|
|
406
|
+
export function cardStringFlag(flags, key) {
|
|
407
|
+
const v = flags[key];
|
|
408
|
+
if (v === undefined)
|
|
409
|
+
return undefined;
|
|
410
|
+
if (v === true || v === false || Array.isArray(v)) {
|
|
411
|
+
console.error(`--${key} requires a value`);
|
|
412
|
+
process.exit(1);
|
|
413
|
+
}
|
|
414
|
+
return v.trim();
|
|
415
|
+
}
|
|
416
|
+
// Claude Code exports its own session var, not ours; without the fallback agent-run recalls trace with no session.
|
|
417
|
+
export function hostSessionId() {
|
|
418
|
+
return process.env.HIPPO_SESSION_ID?.trim() || process.env.CLAUDE_CODE_SESSION_ID?.trim() || undefined;
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Compaction drops the pinned blocks the per-prompt hook injected
|
|
422
|
+
* earlier, so record a `reset` for the payload's session and the next prompt
|
|
423
|
+
* injects again even if nothing changed. `requiredSource` limits it to hook
|
|
424
|
+
* payloads with that `source` (SessionStart fires for other reasons too).
|
|
425
|
+
* Best-effort and silent: a malformed payload records nothing.
|
|
426
|
+
*/
|
|
427
|
+
export function resetHookInjection(hippoRoot, stdinText, requiredSource) {
|
|
428
|
+
const sessionId = hookPayloadSessionId(stdinText, requiredSource);
|
|
429
|
+
// A sub-agent's compaction leaves its parent's context, and the blocks in it, as they were.
|
|
430
|
+
if (sessionId === null || isSubagentPayload(stdinText))
|
|
431
|
+
return;
|
|
432
|
+
withLedgerDb(hippoRoot, (db) => recordTokenUse(db, {
|
|
433
|
+
tenantId: resolveTenantId({}), sessionId, surface: 'hook', event: 'reset', items: 0, tokens: 0,
|
|
434
|
+
}));
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Run `fn` with console.log captured; returns the captured lines joined by
|
|
438
|
+
* newlines (what the same calls would have printed, minus the final newline).
|
|
439
|
+
*/
|
|
440
|
+
export function captureConsole(fn) {
|
|
441
|
+
const lines = [];
|
|
442
|
+
const realLog = console.log;
|
|
443
|
+
console.log = (...parts) => { lines.push(parts.map(String).join(' ')); };
|
|
444
|
+
try {
|
|
445
|
+
fn();
|
|
446
|
+
}
|
|
447
|
+
finally {
|
|
448
|
+
console.log = realLog;
|
|
449
|
+
}
|
|
450
|
+
return lines.join('\n');
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* The store a Claude Code hook writes to: the project store when there is
|
|
454
|
+
* one, else an existing global store, else the project path (which the hook
|
|
455
|
+
* then skips, since hooks fire in every directory and must not create one).
|
|
456
|
+
* Pre-compact and compact-resume must agree, or a snapshot saved to one store
|
|
457
|
+
* is looked for in the other.
|
|
458
|
+
*/
|
|
459
|
+
export function hookStoreRoot(hippoRoot) {
|
|
460
|
+
if (isInitialized(hippoRoot))
|
|
461
|
+
return hippoRoot;
|
|
462
|
+
const globalRoot = getGlobalRoot();
|
|
463
|
+
return isInitialized(globalRoot) ? globalRoot : hippoRoot;
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Run `fn` against the token ledger's store: the local store when it is
|
|
467
|
+
* initialized, else the global one (the per-prompt hook runs in directories
|
|
468
|
+
* without a local store). Best-effort: returns undefined and never throws,
|
|
469
|
+
* because a ledger failure must not break context or recall.
|
|
470
|
+
*/
|
|
471
|
+
export function withLedgerDb(hippoRoot, fn) {
|
|
472
|
+
let root = null;
|
|
473
|
+
try {
|
|
474
|
+
if (isInitialized(hippoRoot))
|
|
475
|
+
root = hippoRoot;
|
|
476
|
+
else if (isInitialized(getGlobalRoot()))
|
|
477
|
+
root = getGlobalRoot();
|
|
478
|
+
}
|
|
479
|
+
catch {
|
|
480
|
+
return undefined;
|
|
481
|
+
}
|
|
482
|
+
if (root === null)
|
|
483
|
+
return undefined;
|
|
484
|
+
let db;
|
|
485
|
+
try {
|
|
486
|
+
db = openHippoDb(root);
|
|
487
|
+
return fn(db);
|
|
488
|
+
}
|
|
489
|
+
catch (error) {
|
|
490
|
+
// Best effort, but a busy store is the one failure an operator can act on, so it warns once.
|
|
491
|
+
if (isSqliteBusy(error))
|
|
492
|
+
noteStoreBusy('token ledger row skipped');
|
|
493
|
+
return undefined;
|
|
494
|
+
}
|
|
495
|
+
finally {
|
|
496
|
+
if (db)
|
|
497
|
+
closeHippoDb(db);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
export function learnFromRepo(hippoRoot, repoPath, days, label) {
|
|
501
|
+
const prefix = label ? `[${label}] ` : '';
|
|
502
|
+
if (!isGitRepo(repoPath)) {
|
|
503
|
+
console.log(`${prefix}No git history found (or not a git repository).`);
|
|
504
|
+
return { added: 0, skipped: 0, lowInfo: 0 };
|
|
505
|
+
}
|
|
506
|
+
const gitLog = fetchGitLog(repoPath, days);
|
|
507
|
+
if (!gitLog.trim()) {
|
|
508
|
+
console.log(`${prefix}No fix/revert/bug commits found in the specified period.`);
|
|
509
|
+
return { added: 0, skipped: 0, lowInfo: 0 };
|
|
510
|
+
}
|
|
511
|
+
// Same patterns as MCP hippo_learn: config.gitLearnPatterns (whose default
|
|
512
|
+
// equals extractLessons' built-in list) so a custom list applies everywhere.
|
|
513
|
+
const config = loadConfig(hippoRoot);
|
|
514
|
+
const parsedLessons = extractLessons(gitLog, config.gitLearnPatterns);
|
|
515
|
+
if (parsedLessons.length === 0) {
|
|
516
|
+
console.log(`${prefix}No fix/revert/bug commits found in the specified period.`);
|
|
517
|
+
return { added: 0, skipped: 0, lowInfo: 0 };
|
|
518
|
+
}
|
|
519
|
+
// The admission gate lives at the write path, not in extractLessons
|
|
520
|
+
// (a published API surface that only parses). Bare subjects like "fixed
|
|
521
|
+
// signals" are dropped here, before they ever become a memory.
|
|
522
|
+
// The gate filters the loop INPUT, so a dropped lesson neither stores nor
|
|
523
|
+
// invalidates. That is deliberate, and it was argued both ways.
|
|
524
|
+
//
|
|
525
|
+
// One review called the lost invalidation serious: a migration subject
|
|
526
|
+
// too thin to store ("replace webpack with vite") would stop weakening
|
|
527
|
+
// stale webpack memories. True. So the loop was widened to walk every
|
|
528
|
+
// parsed lesson with the gate on the write alone.
|
|
529
|
+
//
|
|
530
|
+
// A second review found the cure was worse. STORAGE is what makes invalidation
|
|
531
|
+
// idempotent here: a stored lesson is recognised by its same-text key on
|
|
532
|
+
// the next scan and short-circuits before invalidating again. A lesson that
|
|
533
|
+
// invalidates but is never stored has no such record, so every rescan
|
|
534
|
+
// re-invalidates, and invalidateMatching halves half_life_days each time.
|
|
535
|
+
// Measured: 7 -> 3 -> 1 over two runs. That is compounding data damage.
|
|
536
|
+
//
|
|
537
|
+
// Measured frequency decided it. Across 413 real auto-learn rows in 4
|
|
538
|
+
// stores, 24 are gated and ZERO of those carry an invalidation target; the
|
|
539
|
+
// 45 lessons that do carry targets all pass the gate and are unaffected
|
|
540
|
+
// either way. Both failure modes are empty on real data, so the tie breaks
|
|
541
|
+
// on which one is benign if it ever fires: not invalidating is a missed
|
|
542
|
+
// improvement, re-invalidating forever is damage.
|
|
543
|
+
//
|
|
544
|
+
// Documented limitation, pinned by test: a migration subject too thin to
|
|
545
|
+
// store also does not invalidate. Making invalidateMatching idempotent
|
|
546
|
+
// would allow both, and is backlogged - it is a latent issue for the manual
|
|
547
|
+
// `hippo invalidate` path too, not just this one.
|
|
548
|
+
const { kept: lessons, dropped } = partitionLessons(parsedLessons);
|
|
549
|
+
const lowInfo = dropped.length;
|
|
550
|
+
let added = 0;
|
|
551
|
+
let skipped = 0;
|
|
552
|
+
// Containment: per-lesson refusal must not abort the rest
|
|
553
|
+
// of the git-log scan. No signature change (added/skipped return shape
|
|
554
|
+
// used by cmdLearn + cmdSleepCore callers) — counted locally, folded into
|
|
555
|
+
// the existing summary line.
|
|
556
|
+
let rejected = 0;
|
|
557
|
+
const gitLearnTags = ['error', 'git-learned'];
|
|
558
|
+
const existingForSchema = loadAllEntries(hippoRoot, resolveTenantId({}));
|
|
559
|
+
const keys = storedTextKeys(existingForSchema);
|
|
560
|
+
for (const lesson of lessons) {
|
|
561
|
+
if (keys.has(duplicateKey(lesson))) {
|
|
562
|
+
skipped++;
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
const target = extractInvalidationTarget(lesson);
|
|
566
|
+
if (target) {
|
|
567
|
+
const invResult = invalidateMatching(hippoRoot, target, resolveTenantId({}));
|
|
568
|
+
if (invResult.invalidated > 0) {
|
|
569
|
+
console.log(`${prefix} Invalidated ${invResult.invalidated} memories referencing "${target.from}"`);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
const schemaFitVal = computeSchemaFit(lesson, gitLearnTags, existingForSchema);
|
|
573
|
+
const entry = createMemory(lesson, {
|
|
574
|
+
layer: Layer.Episodic,
|
|
575
|
+
tags: [...gitLearnTags],
|
|
576
|
+
source: 'git-learn',
|
|
577
|
+
confidence: 'observed',
|
|
578
|
+
schema_fit: schemaFitVal,
|
|
579
|
+
tenantId: resolveTenantId({}),
|
|
580
|
+
baseHalfLifeDays: config.defaultHalfLifeDays,
|
|
581
|
+
});
|
|
582
|
+
// Auto-tag with path context from the repo being learned
|
|
583
|
+
const learnPathTags = extractPathTags(repoPath);
|
|
584
|
+
for (const pt of learnPathTags) {
|
|
585
|
+
if (!entry.tags.includes(pt))
|
|
586
|
+
entry.tags.push(pt);
|
|
587
|
+
}
|
|
588
|
+
try {
|
|
589
|
+
writeEntry(hippoRoot, entry);
|
|
590
|
+
}
|
|
591
|
+
catch (err) {
|
|
592
|
+
if (err instanceof RejectedValueError) {
|
|
593
|
+
rejected++;
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
throw err;
|
|
597
|
+
}
|
|
598
|
+
updateStats(hippoRoot, { remembered: 1 });
|
|
599
|
+
keys.add(duplicateKey(lesson));
|
|
600
|
+
void embedMemory(hippoRoot, entry);
|
|
601
|
+
added++;
|
|
602
|
+
}
|
|
603
|
+
console.log(`${prefix}${added} new lessons added, ${skipped} duplicates skipped` +
|
|
604
|
+
(rejected > 0 ? `, ${rejected} rejected value(s) skipped` : '') +
|
|
605
|
+
(lowInfo > 0 ? `, ${lowInfo} low-information subject(s) dropped` : '') +
|
|
606
|
+
'.');
|
|
607
|
+
return { added, skipped, lowInfo };
|
|
608
|
+
}
|
|
609
|
+
export const HOOK_MARKERS = {
|
|
610
|
+
start: '<!-- hippo:start -->',
|
|
611
|
+
end: '<!-- hippo:end -->',
|
|
612
|
+
};
|
|
613
|
+
export const HOOKS = {
|
|
614
|
+
'claude-code': {
|
|
615
|
+
file: 'CLAUDE.md',
|
|
616
|
+
description: 'Claude Code',
|
|
617
|
+
content: `
|
|
618
|
+
## Project Memory (Hippo)
|
|
619
|
+
|
|
620
|
+
Pinned rules and recent writes auto-inject at every prompt via the installed
|
|
621
|
+
UserPromptSubmit hook; never re-run that part manually. At the START of a
|
|
622
|
+
task (not per prompt), additionally load task-specific context: git-aware
|
|
623
|
+
recall over the full store that per-prompt injection does not cover. Also
|
|
624
|
+
run it if the hook is not installed:
|
|
625
|
+
\`\`\`bash
|
|
626
|
+
hippo context --auto --budget 1500
|
|
627
|
+
\`\`\`
|
|
628
|
+
|
|
629
|
+
When you find out why something failed, record it right then, while you
|
|
630
|
+
work, never as a closing step:
|
|
631
|
+
\`\`\`bash
|
|
632
|
+
hippo remember "<what went wrong and why>" --error
|
|
633
|
+
\`\`\`
|
|
634
|
+
|
|
635
|
+
The installed hooks store failed tool calls and capture the session when it
|
|
636
|
+
ends, so there is nothing to run before you finish.
|
|
637
|
+
`.trim(),
|
|
638
|
+
},
|
|
639
|
+
'codex': {
|
|
640
|
+
file: 'AGENTS.md',
|
|
641
|
+
description: 'OpenAI Codex',
|
|
642
|
+
content: `
|
|
643
|
+
## Project Memory (Hippo)
|
|
644
|
+
|
|
645
|
+
At the start of every task, run:
|
|
646
|
+
\`\`\`bash
|
|
647
|
+
hippo context --auto --budget 1500
|
|
648
|
+
\`\`\`
|
|
649
|
+
Read the output before writing any code.
|
|
650
|
+
|
|
651
|
+
On errors or unexpected behaviour, record it right then, while you work,
|
|
652
|
+
never as a closing step:
|
|
653
|
+
\`\`\`bash
|
|
654
|
+
hippo remember "<description of what went wrong>" --error
|
|
655
|
+
\`\`\`
|
|
656
|
+
|
|
657
|
+
When you learn something that should outlive this session (a decision and
|
|
658
|
+
its reason, a user preference, a lesson), record it right then, while you
|
|
659
|
+
work, never as a closing step. Leave out secrets and personal details:
|
|
660
|
+
\`\`\`bash
|
|
661
|
+
hippo remember "<what you learned and why>"
|
|
662
|
+
\`\`\`
|
|
663
|
+
|
|
664
|
+
When Hippo's Codex wrapper is installed, session-end capture runs automatically.
|
|
665
|
+
If the wrapper is not installed, capture a brief summary manually:
|
|
666
|
+
\`\`\`bash
|
|
667
|
+
hippo capture --stdin <<< '<decisions, errors, lessons: 2-5 bullets>'
|
|
668
|
+
\`\`\`
|
|
669
|
+
`.trim(),
|
|
670
|
+
},
|
|
671
|
+
'cursor': {
|
|
672
|
+
file: 'AGENTS.md',
|
|
673
|
+
description: 'Cursor',
|
|
674
|
+
content: `
|
|
675
|
+
## Project Memory (Hippo)
|
|
676
|
+
|
|
677
|
+
At the start of every task, run:
|
|
678
|
+
\`\`\`bash
|
|
679
|
+
hippo context --auto --budget 1500
|
|
680
|
+
\`\`\`
|
|
681
|
+
Read the output before writing any code.
|
|
682
|
+
|
|
683
|
+
On errors or unexpected behaviour, record it right then, while you work,
|
|
684
|
+
never as a closing step:
|
|
685
|
+
\`\`\`bash
|
|
686
|
+
hippo remember "<description of what went wrong>" --error
|
|
687
|
+
\`\`\`
|
|
688
|
+
|
|
689
|
+
When you learn something that should outlive this session (a decision and
|
|
690
|
+
its reason, a user preference, a lesson), record it right then, while you
|
|
691
|
+
work, never as a closing step. Leave out secrets and personal details:
|
|
692
|
+
\`\`\`bash
|
|
693
|
+
hippo remember "<what you learned and why>"
|
|
694
|
+
\`\`\`
|
|
695
|
+
|
|
696
|
+
When ending a session, capture a brief summary:
|
|
697
|
+
\`\`\`bash
|
|
698
|
+
hippo capture --stdin <<< '<decisions, errors, lessons: 2-5 bullets>'
|
|
699
|
+
\`\`\`
|
|
700
|
+
`.trim(),
|
|
701
|
+
},
|
|
702
|
+
'openclaw': {
|
|
703
|
+
file: 'AGENTS.md',
|
|
704
|
+
description: 'OpenClaw',
|
|
705
|
+
content: `
|
|
706
|
+
## Project Memory (Hippo)
|
|
707
|
+
|
|
708
|
+
At the start of every session, run:
|
|
709
|
+
\`\`\`bash
|
|
710
|
+
hippo context --auto --budget 1500
|
|
711
|
+
\`\`\`
|
|
712
|
+
Read the output before writing any code.
|
|
713
|
+
|
|
714
|
+
On errors or unexpected behaviour, record it right then, while you work,
|
|
715
|
+
never as a closing step:
|
|
716
|
+
\`\`\`bash
|
|
717
|
+
hippo remember "<description of what went wrong>" --error
|
|
718
|
+
\`\`\`
|
|
719
|
+
|
|
720
|
+
When you learn something that should outlive this session (a decision and
|
|
721
|
+
its reason, a user preference, a lesson), record it right then, while you
|
|
722
|
+
work, never as a closing step. Leave out secrets and personal details:
|
|
723
|
+
\`\`\`bash
|
|
724
|
+
hippo remember "<what you learned and why>"
|
|
725
|
+
\`\`\`
|
|
726
|
+
|
|
727
|
+
When ending a session, capture a brief summary:
|
|
728
|
+
\`\`\`bash
|
|
729
|
+
hippo capture --stdin <<< '<decisions, errors, lessons: 2-5 bullets>'
|
|
730
|
+
\`\`\`
|
|
731
|
+
`.trim(),
|
|
732
|
+
},
|
|
733
|
+
'opencode': {
|
|
734
|
+
file: 'AGENTS.md',
|
|
735
|
+
description: 'OpenCode',
|
|
736
|
+
content: `
|
|
737
|
+
## Project Memory (Hippo)
|
|
738
|
+
|
|
739
|
+
At the start of every task, run:
|
|
740
|
+
\`\`\`bash
|
|
741
|
+
hippo context --auto --budget 1500
|
|
742
|
+
\`\`\`
|
|
743
|
+
Read the output before writing any code.
|
|
744
|
+
|
|
745
|
+
On errors or unexpected behaviour, record it right then, while you work,
|
|
746
|
+
never as a closing step:
|
|
747
|
+
\`\`\`bash
|
|
748
|
+
hippo remember "<description of what went wrong>" --error
|
|
749
|
+
\`\`\`
|
|
750
|
+
|
|
751
|
+
When you learn something that should outlive this session (a decision and
|
|
752
|
+
its reason, a user preference, a lesson), record it right then, while you
|
|
753
|
+
work, never as a closing step. Leave out secrets and personal details:
|
|
754
|
+
\`\`\`bash
|
|
755
|
+
hippo remember "<what you learned and why>"
|
|
756
|
+
\`\`\`
|
|
757
|
+
|
|
758
|
+
When stuck or repeating yourself, check if this happened before:
|
|
759
|
+
\`\`\`bash
|
|
760
|
+
hippo recall "<what's going wrong>" --budget 2000
|
|
761
|
+
\`\`\`
|
|
762
|
+
|
|
763
|
+
When ending a session, capture a brief summary:
|
|
764
|
+
\`\`\`bash
|
|
765
|
+
hippo capture --stdin <<< '<decisions, errors, lessons: 2-5 bullets>'
|
|
766
|
+
\`\`\`
|
|
767
|
+
`.trim(),
|
|
768
|
+
},
|
|
769
|
+
'pi': {
|
|
770
|
+
file: 'AGENTS.md',
|
|
771
|
+
description: 'Pi',
|
|
772
|
+
content: `
|
|
773
|
+
## Project Memory (Hippo)
|
|
774
|
+
|
|
775
|
+
At the start of every session, run:
|
|
776
|
+
\`\`\`bash
|
|
777
|
+
hippo context --auto --budget 1500
|
|
778
|
+
\`\`\`
|
|
779
|
+
Read the output before writing any code.
|
|
780
|
+
|
|
781
|
+
On errors or unexpected behaviour, record it right then, while you work,
|
|
782
|
+
never as a closing step:
|
|
783
|
+
\`\`\`bash
|
|
784
|
+
hippo remember "<description of what went wrong>" --error
|
|
785
|
+
\`\`\`
|
|
786
|
+
|
|
787
|
+
When you learn something that should outlive this session (a decision and
|
|
788
|
+
its reason, a user preference, a lesson), record it right then, while you
|
|
789
|
+
work, never as a closing step. Leave out secrets and personal details:
|
|
790
|
+
\`\`\`bash
|
|
791
|
+
hippo remember "<what you learned and why>"
|
|
792
|
+
\`\`\`
|
|
793
|
+
|
|
794
|
+
When ending a session, capture a brief summary:
|
|
795
|
+
\`\`\`bash
|
|
796
|
+
hippo capture --stdin <<< '<decisions, errors, lessons: 2-5 bullets>'
|
|
797
|
+
\`\`\`
|
|
798
|
+
|
|
799
|
+
For full integration, copy the hippo-memory Pi extension to \`~/.pi/agent/extensions/hippo-memory/\`.
|
|
800
|
+
`.trim(),
|
|
801
|
+
},
|
|
802
|
+
};
|
|
803
|
+
// sha256 of each trimmed block an earlier hippo wrote, so init refreshes only blocks nobody edited. Add the old hash when a block changes.
|
|
804
|
+
const SHIPPED_HOOK_HASHES = new Map([
|
|
805
|
+
['c04e48f2896a4fee9ae98f8f832e2d26a3910269df3beb5bcd6baee3cd9db68e', 'claude-code'],
|
|
806
|
+
['e6b12bd8983c032e5ca8e95a97aeff4178a5a05026d10acad5b2e1b25d5656dd', 'claude-code'],
|
|
807
|
+
['4c64e11d3e5be68fa547c9248d7553feb645a02f7cf13ba02f7275e1854baf44', 'claude-code'],
|
|
808
|
+
['293bd319bbc86225a0ee027490a3322a0336257f5832f7fade65e4ffb2530654', 'claude-code'],
|
|
809
|
+
['15abcece9712279fb4721f7a8f0ba117457400278977beb5cf5b5d7ba49f7b1a', 'codex'],
|
|
810
|
+
['0c81a6b2c21473313001f624b80ea870e661aecbfda9bfe8503febc0d5f34533', 'codex'],
|
|
811
|
+
['88e45358aba4f17912f113221c991dc758275991335d1daa4aa1974a69c46769', 'codex'],
|
|
812
|
+
['e61632fe177450a06541c148a9a4f9182530d8df667806927a99792825903298', 'codex'],
|
|
813
|
+
['a1415ecda9b2f8f317c233738e4a5ac16e6b2cc385a017c0c8ecfbfacbcab6a3', 'cursor'],
|
|
814
|
+
['a38c428bbdfc14ec50f6f7b9183785170a4eae1ce9cde60257cca6efc7206b3a', 'cursor'],
|
|
815
|
+
['0ec9f556abfd55e94f9e6fb47ece0fc5acb841977d144b35a2371e03645d8636', 'cursor'],
|
|
816
|
+
['40524c3bd5a2eb04036567cc761451961d950995768bccd93a9900b0f75eafea', 'openclaw'],
|
|
817
|
+
['7b3518e8c0feaa7b8b454cde7743f7598ad14cd9979e1680d0954484e2464aae', 'openclaw'],
|
|
818
|
+
['1137dcf04568caf011e41db77bc55324faee88bc29c3a5fcc98ab687cd952a16', 'openclaw'],
|
|
819
|
+
['4601c67c31f41cd5b1324cfccdb1afc66872b7fb0bc1e7c5789ecabb1f6bd942', 'opencode'],
|
|
820
|
+
['90d9e21d8d1ecbe99a0fc7b7f2d9f8af7b5315a6b4b0203df4f7a9bdc0699b98', 'opencode'],
|
|
821
|
+
['ca4e00284f1397ed2f2fcc53210c27f63b90edf6b37fd66dad5ee58b94ea3eee', 'opencode'],
|
|
822
|
+
['8b8f5986d7f7ed15f06e68720d8913c3cab23d94366b411935ca2bbaa334553b', 'pi'],
|
|
823
|
+
['37767b355e18beac726b05b9e2b898dab8c6135fd7b98f3aa52edc734d5dd283', 'pi'],
|
|
824
|
+
['6e85a5cccb3cfeaa9a080713754936db730f96376f94cc9a9888a746149c7268', 'pi'],
|
|
825
|
+
]);
|
|
826
|
+
export function resolveAuthRoot(hippoRoot, flags) {
|
|
827
|
+
if (flags['global']) {
|
|
828
|
+
initGlobal();
|
|
829
|
+
return getGlobalRoot();
|
|
830
|
+
}
|
|
831
|
+
requireInit(hippoRoot);
|
|
832
|
+
return hippoRoot;
|
|
833
|
+
}
|
|
834
|
+
//# sourceMappingURL=shared.js.map
|