hippo-memory 1.59.0 → 1.60.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.d.ts +2 -0
- package/dist/api.js +25 -9
- package/dist/audit.js +1 -0
- package/dist/autolearn.js +5 -2
- package/dist/capture.js +8 -6
- package/dist/cli.d.ts +355 -0
- package/dist/cli.js +1236 -1081
- package/dist/compaction-record.js +7 -6
- package/dist/config.js +12 -11
- package/dist/connectors/github/cli-impl.js +1 -0
- package/dist/consolidate.js +4 -3
- package/dist/dag.js +7 -7
- package/dist/dashboard.js +4 -2
- package/dist/db.js +16 -3
- package/dist/delivery-recorder.js +1 -0
- package/dist/doctor.js +1 -0
- package/dist/dormant.js +1 -0
- package/dist/embedding-provider.js +3 -2
- package/dist/embeddings.js +10 -7
- package/dist/extract.js +4 -3
- package/dist/graph.js +3 -8
- package/dist/handoff.js +3 -0
- package/dist/hooks.js +3 -0
- package/dist/importers.js +6 -3
- package/dist/incidents.js +1 -0
- package/dist/judgment.js +5 -2
- package/dist/mcp/server.d.ts +5 -0
- package/dist/mcp/server.js +32 -13
- package/dist/memory.d.ts +5 -3
- package/dist/processes.js +1 -0
- package/dist/project-identity.d.ts +1 -1
- package/dist/project-identity.js +9 -4
- package/dist/raw-archive-mirror-cleanup.js +2 -1
- package/dist/recall-trace.d.ts +2 -2
- package/dist/recall-trace.js +10 -14
- package/dist/refine-llm.js +18 -10
- package/dist/rerankers/clef.d.ts +2 -2
- package/dist/rerankers/clef.js +68 -27
- package/dist/rerankers/cross-encoder.js +6 -4
- package/dist/rerankers/jev.d.ts +3 -1
- package/dist/rerankers/jev.js +20 -15
- package/dist/rerankers/llm.js +3 -3
- package/dist/same-text.d.ts +2 -0
- package/dist/same-text.js +4 -0
- package/dist/scheduler.js +1 -0
- package/dist/search.js +4 -2
- package/dist/secret-detect.js +1 -0
- package/dist/server.js +29 -12
- package/dist/shared.js +21 -19
- package/dist/stdin.js +1 -0
- package/dist/store.d.ts +33 -1
- package/dist/store.js +117 -16
- package/dist/token-ledger.js +1 -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 +2 -1
package/dist/cli.js
CHANGED
|
@@ -841,9 +841,12 @@ function parseRerankerFlag(flags) {
|
|
|
841
841
|
}
|
|
842
842
|
if (!fn)
|
|
843
843
|
return {};
|
|
844
|
-
const
|
|
845
|
-
|
|
846
|
-
|
|
844
|
+
const raw = flags['reranker-top-k'];
|
|
845
|
+
const topK = raw !== undefined ? Number(raw) : name === 'jev' || isClefModel(name) ? JEV_DEFAULT_TOP_K : 50;
|
|
846
|
+
// slice(0, -1) would quietly drop the last candidate rather than fail.
|
|
847
|
+
if (!Number.isInteger(topK) || topK < 1) {
|
|
848
|
+
return { fail: failWith(`Invalid --reranker-top-k: "${String(raw)}". Must be a positive integer.`) };
|
|
849
|
+
}
|
|
847
850
|
return { value: { fn, topK } };
|
|
848
851
|
}
|
|
849
852
|
function parseSalienceFlag(flags) {
|
|
@@ -7202,13 +7205,660 @@ function cmdSlack(hippoRoot, args, flags) {
|
|
|
7202
7205
|
printError('Usage: hippo slack <backfill|dlq list|dlq replay <id> [--force]|workspaces add|workspaces list|workspaces remove> [...]');
|
|
7203
7206
|
process.exit(1);
|
|
7204
7207
|
}
|
|
7205
|
-
|
|
7206
|
-
|
|
7207
|
-
|
|
7208
|
-
|
|
7209
|
-
|
|
7210
|
-
|
|
7211
|
-
|
|
7208
|
+
async function handleRemember({ hippoRoot, args, flags }) {
|
|
7209
|
+
let text;
|
|
7210
|
+
if (args.length === 1 && args[0] === '-') {
|
|
7211
|
+
text = fs.readFileSync(0, 'utf-8').trim();
|
|
7212
|
+
}
|
|
7213
|
+
else {
|
|
7214
|
+
text = args.join(' ').trim();
|
|
7215
|
+
}
|
|
7216
|
+
if (!text || text.length < 3) {
|
|
7217
|
+
printError('Memory content too short (minimum 3 characters).');
|
|
7218
|
+
process.exit(1);
|
|
7219
|
+
}
|
|
7220
|
+
// Thin-client routing. When a server is up, simple `remember` calls go
|
|
7221
|
+
// over HTTP so the daemon stays single-writer (footgun #2). Rich CLI
|
|
7222
|
+
// flags (--pin, --layer, --extract, --global) still need the direct
|
|
7223
|
+
// path; we only intercept the minimal envelope. The salience gate is
|
|
7224
|
+
// NOT in richFlag and the route does not apply it, so a routed remember
|
|
7225
|
+
// stores what a direct one would skip. Measured 2026-09-07, tracked in
|
|
7226
|
+
// TODOS.md; do not read this list as covering salience.
|
|
7227
|
+
const richFlag = flags['pin'] || flags['global'] || flags['extract'] || flags['force'] ||
|
|
7228
|
+
flags['observed'] || flags['inferred'] || flags['verified'] ||
|
|
7229
|
+
flags['layer'] !== undefined;
|
|
7230
|
+
if (!richFlag) {
|
|
7231
|
+
const rememberKindRaw = typeof flags['kind'] === 'string' ? flags['kind'].toLowerCase() : undefined;
|
|
7232
|
+
const rememberKindAllowed = ['distilled', 'superseded'];
|
|
7233
|
+
if (rememberKindRaw === undefined || rememberKindAllowed.includes(rememberKindRaw)) {
|
|
7234
|
+
const tags = rememberTags(flags, process.cwd()).all;
|
|
7235
|
+
// B2 v1.12.6 — validate --owner on the thin-client path too.
|
|
7236
|
+
// Failure on this path exits early so the user gets the same
|
|
7237
|
+
// validation experience whether or not a server is up.
|
|
7238
|
+
const thinOwnerRaw = typeof flags['owner'] === 'string' ? flags['owner'] : undefined;
|
|
7239
|
+
const thinOwnerCheck = validateOwner(thinOwnerRaw, { strict: isStrictOwnerEnv() });
|
|
7240
|
+
if (!thinOwnerCheck.ok) {
|
|
7241
|
+
printError(thinOwnerCheck.message);
|
|
7242
|
+
process.exit(1);
|
|
7243
|
+
}
|
|
7244
|
+
if (thinOwnerCheck.message)
|
|
7245
|
+
printError(thinOwnerCheck.message);
|
|
7246
|
+
const remembered = await runViaServerIfAvailable(hippoRoot, async (info, apiKey) => {
|
|
7247
|
+
const result = await client.remember(info.url, apiKey, {
|
|
7248
|
+
content: text,
|
|
7249
|
+
kind: rememberKindRaw,
|
|
7250
|
+
scope: typeof flags['scope'] === 'string' ? flags['scope'] : undefined,
|
|
7251
|
+
owner: thinOwnerCheck.value,
|
|
7252
|
+
artifactRef: typeof flags['artifact-ref'] === 'string' ? flags['artifact-ref'] : undefined,
|
|
7253
|
+
tags,
|
|
7254
|
+
});
|
|
7255
|
+
console.log(`Remembered [${result.id}] (via ${info.url})`);
|
|
7256
|
+
console.log(` Kind: ${result.kind} | Tenant: ${result.tenantId}`);
|
|
7257
|
+
for (const w of result.warnings ?? [])
|
|
7258
|
+
printError(`Warning: ${w}`);
|
|
7259
|
+
});
|
|
7260
|
+
if (remembered)
|
|
7261
|
+
return;
|
|
7262
|
+
}
|
|
7263
|
+
}
|
|
7264
|
+
await cmdRemember(hippoRoot, text, flags);
|
|
7265
|
+
}
|
|
7266
|
+
async function handleRecall({ hippoRoot, args, flags }) {
|
|
7267
|
+
const query = args.join(' ').trim();
|
|
7268
|
+
if (!query) {
|
|
7269
|
+
printError('Please provide a search query.');
|
|
7270
|
+
process.exit(1);
|
|
7271
|
+
}
|
|
7272
|
+
await cmdRecall(hookStoreRoot(hippoRoot), query, flags);
|
|
7273
|
+
}
|
|
7274
|
+
function handleDrill({ hippoRoot, args, flags }) {
|
|
7275
|
+
const summaryId = args[0];
|
|
7276
|
+
if (!summaryId) {
|
|
7277
|
+
printError('Usage: hippo drill <summary-id> [--limit N] [--budget N]');
|
|
7278
|
+
process.exit(1);
|
|
7279
|
+
}
|
|
7280
|
+
cmdDrillDown(hippoRoot, summaryId, flags);
|
|
7281
|
+
}
|
|
7282
|
+
function handleAssemble({ hippoRoot, args, flags }) {
|
|
7283
|
+
const sessionId = typeof flags['session'] === 'string' ? flags['session'] : args[0];
|
|
7284
|
+
if (!sessionId) {
|
|
7285
|
+
printError('Usage: hippo assemble --session <id> [--budget N] [--fresh-tail N] [--no-summarize-older] [--json]');
|
|
7286
|
+
process.exit(1);
|
|
7287
|
+
}
|
|
7288
|
+
cmdAssemble(hippoRoot, sessionId, flags);
|
|
7289
|
+
}
|
|
7290
|
+
function handleSupersede({ hippoRoot, args, flags }) {
|
|
7291
|
+
const oldId = args[0];
|
|
7292
|
+
const newContent = args.slice(1).join(' ').trim();
|
|
7293
|
+
if (!oldId || !newContent) {
|
|
7294
|
+
printError('Usage: hippo supersede <old-id> "<new content>" [--layer L] [--tag T] [--pin]');
|
|
7295
|
+
process.exit(1);
|
|
7296
|
+
}
|
|
7297
|
+
cmdSupersede(hippoRoot, oldId, newContent, flags);
|
|
7298
|
+
}
|
|
7299
|
+
async function handleExplain({ hippoRoot, args, flags }) {
|
|
7300
|
+
const query = args.join(' ').trim();
|
|
7301
|
+
if (!query) {
|
|
7302
|
+
printError('Please provide a search query.');
|
|
7303
|
+
process.exit(1);
|
|
7304
|
+
}
|
|
7305
|
+
await cmdExplain(hippoRoot, query, flags);
|
|
7306
|
+
}
|
|
7307
|
+
async function handleEval({ hippoRoot, args, flags }) {
|
|
7308
|
+
const corpusPath = args[0] ? String(args[0]) : null;
|
|
7309
|
+
await cmdEval(hippoRoot, corpusPath, flags);
|
|
7310
|
+
}
|
|
7311
|
+
function handleTrace({ hippoRoot, args, flags }) {
|
|
7312
|
+
const sub = args[0] ? String(args[0]) : '';
|
|
7313
|
+
if (sub === 'record') {
|
|
7314
|
+
cmdTraceRecord(hippoRoot, flags);
|
|
7315
|
+
return;
|
|
7316
|
+
}
|
|
7317
|
+
if (!sub) {
|
|
7318
|
+
printError('Usage: hippo trace <memory-id> | hippo trace record --task <t> --steps <json> --outcome <o>');
|
|
7319
|
+
process.exit(1);
|
|
7320
|
+
}
|
|
7321
|
+
cmdTrace(hippoRoot, sub, flags);
|
|
7322
|
+
}
|
|
7323
|
+
async function handlePreCompact({ hippoRoot, flags }) {
|
|
7324
|
+
// Bounded wait, not a TTY guard: an idle non-TTY pipe must not hang.
|
|
7325
|
+
const { text: stdinText, timedOut: stdinTimedOut } = await readStdinBounded();
|
|
7326
|
+
await runHookWithStores(async () => {
|
|
7327
|
+
resetHookInjection(hippoRoot, stdinText, null);
|
|
7328
|
+
await cmdPreCompact(hookStoreRoot(hippoRoot), {
|
|
7329
|
+
stdinText,
|
|
7330
|
+
stdinTimedOut,
|
|
7331
|
+
logFile: typeof flags['log-file'] === 'string' ? flags['log-file'] : undefined,
|
|
7332
|
+
});
|
|
7333
|
+
});
|
|
7334
|
+
}
|
|
7335
|
+
async function handlePostCompact({ hippoRoot, flags }) {
|
|
7336
|
+
// PostCompact hook: saves the compaction summary and its memories, then prints one plain line, because Claude Code shows this hook's stdout as-is. Always exits 0.
|
|
7337
|
+
const { text } = await readStdinBounded();
|
|
7338
|
+
const logFlag = flags['log-file'];
|
|
7339
|
+
const store = hookStoreRoot(hippoRoot);
|
|
7340
|
+
const line = await runHookWithStores(() => cmdPostCompact(store, {
|
|
7341
|
+
stdinText: text,
|
|
7342
|
+
logFile: logFlag === true || logFlag === false || Array.isArray(logFlag) ? undefined : logFlag,
|
|
7343
|
+
// Passed in, since capture.ts importing the sync would close an import cycle.
|
|
7344
|
+
afterSave: (transcriptPath, originProject, log) => {
|
|
7345
|
+
const report = importAtCompaction(store, transcriptPath, originProject, { machine: currentMachine(), busyWaitMs: COMPACTION_DB_WAIT_MS });
|
|
7346
|
+
const summary = summaryLine(report);
|
|
7347
|
+
if (summary !== null)
|
|
7348
|
+
log(summary);
|
|
7349
|
+
for (const warning of report.warnings)
|
|
7350
|
+
log(`agent memories: ${warning}`);
|
|
7351
|
+
},
|
|
7352
|
+
}));
|
|
7353
|
+
if (line !== null && line !== undefined)
|
|
7354
|
+
console.log(line);
|
|
7355
|
+
}
|
|
7356
|
+
async function handleCaptureError({ hippoRoot }) {
|
|
7357
|
+
// PostToolUseFailure hook: every path exits 0, and nothing is created
|
|
7358
|
+
// when no store exists (the hook fires in every directory).
|
|
7359
|
+
const { text } = await readStdinBounded();
|
|
7360
|
+
try {
|
|
7361
|
+
const root = hookStoreRoot(hippoRoot);
|
|
7362
|
+
const payload = (text ?? '').trim();
|
|
7363
|
+
if (isInitialized(root) && payload) {
|
|
7364
|
+
// SAFETY: JSON.parse returns a JSON value by definition.
|
|
7365
|
+
const failure = JSON.parse(payload);
|
|
7366
|
+
await runHookWithStores(() => captureToolFailure(root, resolveTenantId({}), failure));
|
|
7367
|
+
}
|
|
7368
|
+
}
|
|
7369
|
+
catch {
|
|
7370
|
+
// A malformed payload or store error must never fail the agent's tool call.
|
|
7371
|
+
}
|
|
7372
|
+
}
|
|
7373
|
+
async function handleCompactResume({ hippoRoot }) {
|
|
7374
|
+
const { text: stdinText, timedOut: stdinTimedOut } = await readStdinBounded();
|
|
7375
|
+
await runHookWithStores(() => {
|
|
7376
|
+
resetHookInjection(hippoRoot, stdinText, 'compact');
|
|
7377
|
+
cmdCompactResume(hookStoreRoot(hippoRoot), stdinText, stdinTimedOut);
|
|
7378
|
+
});
|
|
7379
|
+
}
|
|
7380
|
+
function handleAudit({ hippoRoot, args, flags }) {
|
|
7381
|
+
// `audit list` and `audit prune` -> A5 audit-log subcommands.
|
|
7382
|
+
// Other forms (no sub, --fix) keep the existing memory-quality auditor
|
|
7383
|
+
// for backwards compatibility.
|
|
7384
|
+
if (args[0] === 'list' || args[0] === 'prune') {
|
|
7385
|
+
cmdAuditLog(hippoRoot, args, flags);
|
|
7386
|
+
return;
|
|
7387
|
+
}
|
|
7388
|
+
requireInit(hippoRoot);
|
|
7389
|
+
const entries = loadAllEntries(hippoRoot, resolveTenantId({}));
|
|
7390
|
+
const result = auditMemories(entries, memoriesBackingObjects(hippoRoot));
|
|
7391
|
+
const shouldFix = Boolean(flags['fix']);
|
|
7392
|
+
if (result.issues.length === 0) {
|
|
7393
|
+
console.log(`All ${result.total} memories passed quality checks.`);
|
|
7394
|
+
}
|
|
7395
|
+
else {
|
|
7396
|
+
console.log(`Audited ${result.total} memories: ${result.clean} clean, ${result.issues.length} issues\n`);
|
|
7397
|
+
for (const issue of result.issues) {
|
|
7398
|
+
const icon = issue.severity === 'error' ? 'ERR' : 'WARN';
|
|
7399
|
+
console.log(` [${icon}] ${issue.memoryId}: ${issue.reason}`);
|
|
7400
|
+
console.log(` "${issue.content.slice(0, 80)}${issue.content.length > 80 ? '...' : ''}"`);
|
|
7401
|
+
}
|
|
7402
|
+
if (shouldFix) {
|
|
7403
|
+
const errors = result.issues.filter(i => i.severity === 'error');
|
|
7404
|
+
if (errors.length > 0 && flags['dry-run'] === true) {
|
|
7405
|
+
console.log(`\nWould remove ${errors.length} error-severity memories (dry run, nothing deleted).`);
|
|
7406
|
+
console.log(`${result.issues.length - errors.length} warnings would remain (review manually).`);
|
|
7407
|
+
}
|
|
7408
|
+
else if (errors.length > 0) {
|
|
7409
|
+
const removedCount = errors.filter((issue) => deleteEntry(hippoRoot, issue.memoryId, { reason: `audit --fix: ${issue.reason}`, automatic: true })).length;
|
|
7410
|
+
console.log(`\nRemoved ${removedCount} error-severity memories.`);
|
|
7411
|
+
console.log(`${result.issues.length - errors.length} warnings remain (review manually).`);
|
|
7412
|
+
}
|
|
7413
|
+
else {
|
|
7414
|
+
console.log(`\nNo error-severity issues. Warnings require manual review.`);
|
|
7415
|
+
}
|
|
7416
|
+
}
|
|
7417
|
+
else {
|
|
7418
|
+
console.log(`\nRun with --fix to auto-remove error-severity issues.`);
|
|
7419
|
+
}
|
|
7420
|
+
}
|
|
7421
|
+
}
|
|
7422
|
+
function handleCorrectionLatency({ hippoRoot, flags }) {
|
|
7423
|
+
requireInit(hippoRoot);
|
|
7424
|
+
const entries = loadAllEntries(hippoRoot);
|
|
7425
|
+
const report = buildCorrectionLatency(entries);
|
|
7426
|
+
if (flags['json']) {
|
|
7427
|
+
console.log(JSON.stringify(report, null, 2));
|
|
7428
|
+
}
|
|
7429
|
+
else if (report.count === 0) {
|
|
7430
|
+
console.log('No supersessions found. Correction latency is undefined.');
|
|
7431
|
+
}
|
|
7432
|
+
else {
|
|
7433
|
+
const fmt = (ms) => {
|
|
7434
|
+
if (ms === null)
|
|
7435
|
+
return 'n/a';
|
|
7436
|
+
if (ms < 1000)
|
|
7437
|
+
return `${ms}ms`;
|
|
7438
|
+
if (ms < 60_000)
|
|
7439
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
7440
|
+
if (ms < 3_600_000)
|
|
7441
|
+
return `${(ms / 60_000).toFixed(1)}m`;
|
|
7442
|
+
return `${(ms / 3_600_000).toFixed(1)}h`;
|
|
7443
|
+
};
|
|
7444
|
+
console.log(`Corrections: ${report.count} total (${report.extractionCount} extraction-driven, ${report.manualCount} manual)`);
|
|
7445
|
+
console.log(`Latency p50: ${fmt(report.p50Ms)}, p95: ${fmt(report.p95Ms)}, max: ${fmt(report.maxMs)}`);
|
|
7446
|
+
if (report.extractionCount === 0 && report.manualCount > 0) {
|
|
7447
|
+
console.log(`\nAll ${report.manualCount} corrections were manual supersedes: no measurable observation lag.`);
|
|
7448
|
+
console.log(`To measure latency, route corrections through extraction (set new.extracted_from to the raw receipt).`);
|
|
7449
|
+
}
|
|
7450
|
+
}
|
|
7451
|
+
}
|
|
7452
|
+
function handleProvenance({ hippoRoot, flags }) {
|
|
7453
|
+
requireInit(hippoRoot);
|
|
7454
|
+
const entries = loadAllEntries(hippoRoot);
|
|
7455
|
+
const coverage = buildProvenanceCoverage(entries);
|
|
7456
|
+
if (flags['json']) {
|
|
7457
|
+
console.log(JSON.stringify(coverage, null, 2));
|
|
7458
|
+
}
|
|
7459
|
+
else if (coverage.rawTotal === 0) {
|
|
7460
|
+
console.log('No kind=raw memories present. Coverage gate trivially satisfied.');
|
|
7461
|
+
}
|
|
7462
|
+
else {
|
|
7463
|
+
const pct = (coverage.coverage * 100).toFixed(1);
|
|
7464
|
+
console.log(`Provenance coverage: ${coverage.rawWithEnvelope}/${coverage.rawTotal} raw rows envelope-complete (${pct}%)`);
|
|
7465
|
+
if (coverage.gaps.length > 0) {
|
|
7466
|
+
console.log(`\nGaps:`);
|
|
7467
|
+
for (const g of coverage.gaps) {
|
|
7468
|
+
console.log(` ${g.id}: missing ${g.missing.join(', ')}`);
|
|
7469
|
+
}
|
|
7470
|
+
}
|
|
7471
|
+
}
|
|
7472
|
+
if (flags['strict'] && coverage.coverage < 1) {
|
|
7473
|
+
process.exit(1);
|
|
7474
|
+
}
|
|
7475
|
+
}
|
|
7476
|
+
function handleDoctor({ flags }) {
|
|
7477
|
+
// SAFETY: package.json always carries a string "version" (checked at release by check-manifest-versions).
|
|
7478
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'package.json'), 'utf-8'));
|
|
7479
|
+
const report = runDoctor({ version: pkg.version });
|
|
7480
|
+
console.log(flags['json'] ? JSON.stringify(report, null, 2) : formatDoctor(report));
|
|
7481
|
+
if (!report.ok)
|
|
7482
|
+
process.exit(1);
|
|
7483
|
+
}
|
|
7484
|
+
function handleSupportBundle({ flags }) {
|
|
7485
|
+
const outFlag = cardStringFlag(flags, 'out');
|
|
7486
|
+
if (outFlag === '') {
|
|
7487
|
+
printError('--out requires a file path.');
|
|
7488
|
+
process.exit(1);
|
|
7489
|
+
}
|
|
7490
|
+
const includeLogs = flags['include-logs'] === true;
|
|
7491
|
+
const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
7492
|
+
const now = new Date();
|
|
7493
|
+
const bundle = buildSupportBundle({ cwd: process.cwd(), home, version: PACKAGE_VERSION, includeLogs, now });
|
|
7494
|
+
const stamp = now.toISOString().replace(/[:.]/g, '-');
|
|
7495
|
+
const file = outFlag ?? path.join(process.cwd(), `hippo-support-${stamp}.json`);
|
|
7496
|
+
const json = JSON.stringify(bundle, null, 2);
|
|
7497
|
+
try {
|
|
7498
|
+
fs.writeFileSync(file, `${json}\n`, { flag: 'wx', mode: 0o600 });
|
|
7499
|
+
}
|
|
7500
|
+
catch (err) {
|
|
7501
|
+
if (err instanceof Error && 'code' in err && err.code === 'EEXIST') {
|
|
7502
|
+
printError(`${file} already exists; pass --out to choose another file. Nothing was written.`);
|
|
7503
|
+
}
|
|
7504
|
+
else {
|
|
7505
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
7506
|
+
}
|
|
7507
|
+
process.exit(1);
|
|
7508
|
+
}
|
|
7509
|
+
const kb = Math.round(Buffer.byteLength(json) / 1024);
|
|
7510
|
+
console.log(`Wrote ${file} (${kb} KB).`);
|
|
7511
|
+
console.log(includeLogs
|
|
7512
|
+
? `It holds versions, doctor checks, config with secrets removed, store counts, and the last ${TAIL_MAX_LINES} lines of each hippo log with known secret shapes removed. Those log lines can quote memory text. Read it before you attach it to a ticket.`
|
|
7513
|
+
: 'It holds versions, doctor checks, config with secrets removed, store counts and log file names. It never holds memory text. Read it before you attach it to a ticket.');
|
|
7514
|
+
}
|
|
7515
|
+
async function handleForget({ hippoRoot, args, flags }) {
|
|
7516
|
+
const id = args[0];
|
|
7517
|
+
if (!id) {
|
|
7518
|
+
printError('Please provide a memory ID.');
|
|
7519
|
+
process.exit(1);
|
|
7520
|
+
}
|
|
7521
|
+
// Archive has its own HTTP route (POST /v1/memories/:id/archive); route
|
|
7522
|
+
// both branches the same way the direct path does.
|
|
7523
|
+
const archive = flags['archive'] === true;
|
|
7524
|
+
const reason = typeof flags['reason'] === 'string' ? flags['reason'] : null;
|
|
7525
|
+
if (archive && !reason) {
|
|
7526
|
+
printError(ARCHIVE_REASON_REQUIRED);
|
|
7527
|
+
process.exit(1);
|
|
7528
|
+
}
|
|
7529
|
+
if (flags['dry-run'] === true) {
|
|
7530
|
+
previewForget(hippoRoot, id, archive);
|
|
7531
|
+
return;
|
|
7532
|
+
}
|
|
7533
|
+
const routed = await runViaServerIfAvailable(hippoRoot, async (info, apiKey) => {
|
|
7534
|
+
try {
|
|
7535
|
+
if (archive) {
|
|
7536
|
+
await client.archiveRaw(info.url, apiKey, id, reason);
|
|
7537
|
+
console.log(`Archived ${id}`);
|
|
7538
|
+
}
|
|
7539
|
+
else {
|
|
7540
|
+
await client.forget(info.url, apiKey, id);
|
|
7541
|
+
console.log(`Forgot ${id}`);
|
|
7542
|
+
}
|
|
7543
|
+
}
|
|
7544
|
+
catch (err) {
|
|
7545
|
+
// A server that died after the health probe is the caller's transport
|
|
7546
|
+
// fallback to handle, not an error to report to the user.
|
|
7547
|
+
if (client.classifyTransportFailure(err) !== 'none')
|
|
7548
|
+
throw err;
|
|
7549
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
7550
|
+
printError(archive ? `Could not archive ${id}: ${msg}` : msg);
|
|
7551
|
+
process.exit(1);
|
|
7552
|
+
}
|
|
7553
|
+
});
|
|
7554
|
+
if (routed)
|
|
7555
|
+
return;
|
|
7556
|
+
cmdForget(hippoRoot, id, flags);
|
|
7557
|
+
}
|
|
7558
|
+
function handleInspect({ hippoRoot, args }) {
|
|
7559
|
+
const id = args[0];
|
|
7560
|
+
if (!id) {
|
|
7561
|
+
printError('Please provide a memory ID.');
|
|
7562
|
+
process.exit(1);
|
|
7563
|
+
}
|
|
7564
|
+
cmdInspect(hippoRoot, id);
|
|
7565
|
+
}
|
|
7566
|
+
async function handleContext({ hippoRoot, args, flags }) {
|
|
7567
|
+
// Bounded, not a TTY guard (DF1 T2, docs/plans/2026-08-23-df1-snapshot-lifecycle.md):
|
|
7568
|
+
// the hot stdin path and a manual run share this one command.
|
|
7569
|
+
const { text: stdinText } = await readStdinBounded();
|
|
7570
|
+
await runHookWithStores(() => cmdContext(hookStoreRoot(hippoRoot), args, flags, stdinText));
|
|
7571
|
+
}
|
|
7572
|
+
async function handleWatch({ hippoRoot, args }) {
|
|
7573
|
+
const watchCmd = args.join(' ').trim();
|
|
7574
|
+
await cmdWatch(watchCmd, hippoRoot);
|
|
7575
|
+
}
|
|
7576
|
+
async function handlePromote({ hippoRoot, args }) {
|
|
7577
|
+
const id = args[0];
|
|
7578
|
+
if (!id) {
|
|
7579
|
+
printError('Please provide a memory ID.');
|
|
7580
|
+
process.exit(1);
|
|
7581
|
+
}
|
|
7582
|
+
const promoted = await runViaServerIfAvailable(hippoRoot, async (info, apiKey) => {
|
|
7583
|
+
try {
|
|
7584
|
+
const result = await client.promote(info.url, apiKey, id);
|
|
7585
|
+
console.log(`Promoted ${id} to global store as ${result.globalId}`);
|
|
7586
|
+
}
|
|
7587
|
+
catch (err) {
|
|
7588
|
+
printError(`Failed to promote: ${err.message}`);
|
|
7589
|
+
process.exit(1);
|
|
7590
|
+
}
|
|
7591
|
+
});
|
|
7592
|
+
if (promoted)
|
|
7593
|
+
return;
|
|
7594
|
+
cmdPromote(hippoRoot, id);
|
|
7595
|
+
}
|
|
7596
|
+
function handleShare({ hippoRoot, args, flags }) {
|
|
7597
|
+
const shareId = args[0];
|
|
7598
|
+
if (shareId === '--auto' || flags['auto']) {
|
|
7599
|
+
// Auto-share mode
|
|
7600
|
+
requireInit(hippoRoot);
|
|
7601
|
+
const minScore = parseFloat(String(flags['min-score'] ?? '0.6'));
|
|
7602
|
+
const dryRun = Boolean(flags['dry-run']);
|
|
7603
|
+
const results = autoShare(hippoRoot, { minScore, dryRun, tenantId: resolveTenantId({}) });
|
|
7604
|
+
if (results.length === 0) {
|
|
7605
|
+
console.log('No memories meet the sharing threshold.');
|
|
7606
|
+
}
|
|
7607
|
+
else if (dryRun) {
|
|
7608
|
+
console.log(`Would share ${results.length} memories:\n`);
|
|
7609
|
+
for (const e of results) {
|
|
7610
|
+
const score = transferScore(e);
|
|
7611
|
+
console.log(` ${e.id} (transfer=${fmt(score)}) ${e.content.slice(0, 80)}...`);
|
|
7612
|
+
}
|
|
7613
|
+
}
|
|
7614
|
+
else {
|
|
7615
|
+
console.log(`Shared ${results.length} memories to global store.`);
|
|
7616
|
+
for (const e of results) {
|
|
7617
|
+
console.log(` ${e.id} <- ${e.source}`);
|
|
7618
|
+
}
|
|
7619
|
+
}
|
|
7620
|
+
}
|
|
7621
|
+
else if (shareId) {
|
|
7622
|
+
requireInit(hippoRoot);
|
|
7623
|
+
const force = Boolean(flags['force']);
|
|
7624
|
+
const tenantId = resolveTenantId({});
|
|
7625
|
+
const result = shareMemory(hippoRoot, shareId, { force, tenantId });
|
|
7626
|
+
if (result) {
|
|
7627
|
+
console.log(`Shared [${result.id}] to global store.`);
|
|
7628
|
+
console.log(` Source: ${result.source}`);
|
|
7629
|
+
}
|
|
7630
|
+
else {
|
|
7631
|
+
const entry = readEntry(hippoRoot, shareId, tenantId);
|
|
7632
|
+
if (entry) {
|
|
7633
|
+
const score = transferScore(entry);
|
|
7634
|
+
console.log(`Transfer score too low (${fmt(score)}). This memory looks project-specific.`);
|
|
7635
|
+
console.log('Use --force to share anyway.');
|
|
7636
|
+
}
|
|
7637
|
+
else {
|
|
7638
|
+
printError(`Memory not found: ${shareId}`);
|
|
7639
|
+
process.exit(1);
|
|
7640
|
+
}
|
|
7641
|
+
}
|
|
7642
|
+
}
|
|
7643
|
+
else {
|
|
7644
|
+
printError('Usage: hippo share <memory_id> [--force] or hippo share --auto [--dry-run]');
|
|
7645
|
+
process.exit(1);
|
|
7646
|
+
}
|
|
7647
|
+
}
|
|
7648
|
+
function handlePeers({ flags }) {
|
|
7649
|
+
// D4 v1.12.10: tenant-scoped by default. --all-tenants restores the
|
|
7650
|
+
// pre-D4 host-wide view for the rare operator who genuinely wants
|
|
7651
|
+
// cross-tenant peer discovery.
|
|
7652
|
+
const allTenants = flags['all-tenants'] === true;
|
|
7653
|
+
const tenantScope = allTenants ? undefined : resolveTenantId({});
|
|
7654
|
+
const peers = listPeers(undefined, tenantScope);
|
|
7655
|
+
if (peers.length === 0) {
|
|
7656
|
+
console.log('No peers found. Share memories with: hippo share <id>');
|
|
7657
|
+
}
|
|
7658
|
+
else {
|
|
7659
|
+
const scopeLabel = allTenants ? 'global store (all tenants)' : `global store (tenant "${tenantScope}")`;
|
|
7660
|
+
console.log(`${peers.length} project${peers.length === 1 ? '' : 's'} contributing to ${scopeLabel}:\n`);
|
|
7661
|
+
for (const p of peers) {
|
|
7662
|
+
console.log(` ${p.project.padEnd(25)} ${String(p.count).padStart(4)} memories (latest: ${p.latest.slice(0, 10)})`);
|
|
7663
|
+
}
|
|
7664
|
+
}
|
|
7665
|
+
}
|
|
7666
|
+
function handleExport({ hippoRoot, args, flags }) {
|
|
7667
|
+
requireInit(hippoRoot);
|
|
7668
|
+
const format = flags['format'] || 'json';
|
|
7669
|
+
const outputPath = args[0] || null;
|
|
7670
|
+
const entries = loadAllEntries(hippoRoot, resolveTenantId({}));
|
|
7671
|
+
let output;
|
|
7672
|
+
if (format === 'markdown' || format === 'md') {
|
|
7673
|
+
output = entries.map(e => {
|
|
7674
|
+
const meta = [
|
|
7675
|
+
`id: ${e.id}`,
|
|
7676
|
+
`created: ${e.created}`,
|
|
7677
|
+
`tags: ${e.tags.join(', ')}`,
|
|
7678
|
+
`confidence: ${e.confidence}`,
|
|
7679
|
+
`half_life: ${e.half_life_days}d`,
|
|
7680
|
+
`strength: ${e.strength.toFixed(2)}`,
|
|
7681
|
+
].join(' | ');
|
|
7682
|
+
return `### ${e.id}\n\n${e.content}\n\n_${meta}_`;
|
|
7683
|
+
}).join('\n\n---\n\n');
|
|
7684
|
+
}
|
|
7685
|
+
else {
|
|
7686
|
+
output = JSON.stringify(entries, null, 2);
|
|
7687
|
+
}
|
|
7688
|
+
if (outputPath) {
|
|
7689
|
+
fs.writeFileSync(outputPath, output, 'utf8');
|
|
7690
|
+
console.log(`Exported ${entries.length} memories to ${outputPath}`);
|
|
7691
|
+
}
|
|
7692
|
+
else {
|
|
7693
|
+
console.log(output);
|
|
7694
|
+
}
|
|
7695
|
+
}
|
|
7696
|
+
async function handleCapture({ hippoRoot, flags }) {
|
|
7697
|
+
let captureSource = null;
|
|
7698
|
+
let captureFile;
|
|
7699
|
+
let transcriptPath;
|
|
7700
|
+
if (flags['stdin']) {
|
|
7701
|
+
captureSource = 'stdin';
|
|
7702
|
+
}
|
|
7703
|
+
else if (flags['file']) {
|
|
7704
|
+
captureSource = 'file';
|
|
7705
|
+
captureFile = String(flags['file']);
|
|
7706
|
+
}
|
|
7707
|
+
else if (flags['last-session']) {
|
|
7708
|
+
captureSource = 'last-session';
|
|
7709
|
+
}
|
|
7710
|
+
if (flags['transcript']) {
|
|
7711
|
+
transcriptPath = String(flags['transcript']);
|
|
7712
|
+
if (!captureSource)
|
|
7713
|
+
captureSource = 'last-session';
|
|
7714
|
+
}
|
|
7715
|
+
if (!captureSource) {
|
|
7716
|
+
printError('Usage: hippo capture --stdin|--file <path>|--last-session [--transcript <path>] [--log-file <path>] [--dry-run] [--global]');
|
|
7717
|
+
process.exit(1);
|
|
7718
|
+
}
|
|
7719
|
+
// Bounded, and only when last-session has no explicit path: the
|
|
7720
|
+
// --stdin source keeps its own blocking read in capture.ts by design.
|
|
7721
|
+
const bounded = captureSource === 'last-session' && !transcriptPath
|
|
7722
|
+
? await readStdinBounded()
|
|
7723
|
+
: { text: undefined, timedOut: false };
|
|
7724
|
+
cmdCapture(hippoRoot, {
|
|
7725
|
+
source: captureSource,
|
|
7726
|
+
filePath: captureFile,
|
|
7727
|
+
transcriptPath,
|
|
7728
|
+
stdinText: bounded.text,
|
|
7729
|
+
stdinTimedOut: bounded.timedOut,
|
|
7730
|
+
logFile: typeof flags['log-file'] === 'string' ? flags['log-file'] : undefined,
|
|
7731
|
+
dryRun: Boolean(flags['dry-run']),
|
|
7732
|
+
global: Boolean(flags['global']),
|
|
7733
|
+
tenantId: resolveTenantId({}),
|
|
7734
|
+
});
|
|
7735
|
+
}
|
|
7736
|
+
async function handleDashboard({ hippoRoot, flags }) {
|
|
7737
|
+
requireInit(hippoRoot);
|
|
7738
|
+
const port = parseInt(String(flags['port'] ?? '3333'), 10);
|
|
7739
|
+
const { serveDashboard } = await import('./dashboard.js');
|
|
7740
|
+
serveDashboard(hippoRoot, port);
|
|
7741
|
+
await new Promise(() => { }); // run until Ctrl+C
|
|
7742
|
+
}
|
|
7743
|
+
async function handleMcp() {
|
|
7744
|
+
// Start MCP server over stdio. Dynamic import keeps main CLI lean; the
|
|
7745
|
+
// dispatcher itself is transport-agnostic, so we explicitly attach the
|
|
7746
|
+
// stdio loop here. (HTTP/SSE transport is wired in src/server.ts and
|
|
7747
|
+
// imports the same module without triggering stdin handlers.)
|
|
7748
|
+
const mod = await import('./mcp/server.js');
|
|
7749
|
+
mod.startStdioLoop();
|
|
7750
|
+
// Server runs until stdin closes, so we never reach here
|
|
7751
|
+
await new Promise(() => { }); // hang forever
|
|
7752
|
+
}
|
|
7753
|
+
async function handleServe({ hippoRoot, flags }) {
|
|
7754
|
+
requireInit(hippoRoot);
|
|
7755
|
+
const portRaw = flags['port'] ?? process.env['HIPPO_PORT'] ?? '6789';
|
|
7756
|
+
const port = Number(portRaw);
|
|
7757
|
+
if (!Number.isFinite(port) || port < 0) {
|
|
7758
|
+
printError(`Invalid --port: ${String(portRaw)}`);
|
|
7759
|
+
process.exit(1);
|
|
7760
|
+
}
|
|
7761
|
+
const host = typeof flags['host'] === 'string' ? flags['host'] : '127.0.0.1';
|
|
7762
|
+
const { serve } = await import('./server.js');
|
|
7763
|
+
const handle = await serve({ hippoRoot, port, host, handleSignals: true });
|
|
7764
|
+
console.log(`hippo serve listening on ${handle.url} (pid ${process.pid})`);
|
|
7765
|
+
console.log(`pidfile: ${path.join(hippoRoot, 'server.pid')}`);
|
|
7766
|
+
console.log('press Ctrl+C to stop');
|
|
7767
|
+
// The SIGINT/SIGTERM handlers stop the server and exit. Hang until then.
|
|
7768
|
+
await new Promise(() => { });
|
|
7769
|
+
}
|
|
7770
|
+
function handleInvalidate({ hippoRoot, args, flags }) {
|
|
7771
|
+
requireInit(hippoRoot);
|
|
7772
|
+
if (flags['churn'] === true) {
|
|
7773
|
+
if (args[0] || flags['id'] !== undefined) {
|
|
7774
|
+
printError('Usage: hippo invalidate --churn [--dry-run]');
|
|
7775
|
+
printError('--churn takes no pattern or --id.');
|
|
7776
|
+
process.exit(1);
|
|
7777
|
+
}
|
|
7778
|
+
if (!isGitRepo(process.cwd())) {
|
|
7779
|
+
printError('hippo invalidate --churn must run inside a git repository.');
|
|
7780
|
+
process.exit(1);
|
|
7781
|
+
}
|
|
7782
|
+
const churnDryRun = flags['dry-run'] === true;
|
|
7783
|
+
let churnFailed = false;
|
|
7784
|
+
for (const { root, result } of runChurnStaleForRepo(hippoRoot, churnDryRun)) {
|
|
7785
|
+
if (result.error) {
|
|
7786
|
+
printError(`Churn-staleness check failed for ${root}: ${result.error}`);
|
|
7787
|
+
churnFailed = true;
|
|
7788
|
+
continue;
|
|
7789
|
+
}
|
|
7790
|
+
if (result.preview.length === 0) {
|
|
7791
|
+
console.log(`No churn-stale candidates in ${root}.`);
|
|
7792
|
+
}
|
|
7793
|
+
else if (churnDryRun) {
|
|
7794
|
+
console.log(`DRY RUN - ${result.marked} memories in ${root} WOULD be tagged churn-stale (${result.alreadyMarked} already tagged):`);
|
|
7795
|
+
}
|
|
7796
|
+
else {
|
|
7797
|
+
console.log(`Tagged ${result.marked} memories churn-stale in ${root} (${result.alreadyMarked} already tagged):`);
|
|
7798
|
+
}
|
|
7799
|
+
result.preview.forEach(p => console.log(` ${p.id} ${p.evidence} ${p.already ? '(already) ' : ''}${p.headline}`));
|
|
7800
|
+
if (result.skippedPinned.length > 0) {
|
|
7801
|
+
console.log(`Skipped ${result.skippedPinned.length} pinned: ${result.skippedPinned.join(', ')}`);
|
|
7802
|
+
}
|
|
7803
|
+
}
|
|
7804
|
+
if (churnFailed)
|
|
7805
|
+
process.exit(1);
|
|
7806
|
+
return;
|
|
7807
|
+
}
|
|
7808
|
+
const target = args[0];
|
|
7809
|
+
if (flags['id'] === true) {
|
|
7810
|
+
// Value-less --id must never silently fall through to pattern mode
|
|
7811
|
+
// (pattern mode writes broadly; an ignored --id reverses user intent).
|
|
7812
|
+
printError('--id requires a memory id');
|
|
7813
|
+
process.exit(1);
|
|
7814
|
+
}
|
|
7815
|
+
const onlyId = typeof flags['id'] === 'string' ? flags['id'] : undefined;
|
|
7816
|
+
if (typeof flags['dry-run'] === 'string') {
|
|
7817
|
+
// Dead: the earlier global BOOLEAN_FLAGS guard now exits first on any --dry-run=<v>.
|
|
7818
|
+
// Kept as defence in depth on a destructive command.
|
|
7819
|
+
printError('--dry-run takes no value');
|
|
7820
|
+
process.exit(1);
|
|
7821
|
+
}
|
|
7822
|
+
const dryRun = flags['dry-run'] === true;
|
|
7823
|
+
if ((target && onlyId) || (!target && !onlyId)) {
|
|
7824
|
+
printError('Usage: hippo invalidate "<old pattern>" [--dry-run] [--reason "<why>"]');
|
|
7825
|
+
printError(' hippo invalidate --id <memory-id> [--dry-run] [--reason "<why>"]');
|
|
7826
|
+
printError('Pass a pattern OR --id, not both. Tag matching is EXACT: the full pattern must equal a tag.');
|
|
7827
|
+
process.exit(1);
|
|
7828
|
+
}
|
|
7829
|
+
const reason = flags['reason'] || null;
|
|
7830
|
+
const invTarget = {
|
|
7831
|
+
from: target ?? `id:${onlyId}`,
|
|
7832
|
+
to: reason,
|
|
7833
|
+
type: 'migration',
|
|
7834
|
+
};
|
|
7835
|
+
const result = invalidateMatching(hippoRoot, invTarget, resolveTenantId({}), { dryRun, onlyId });
|
|
7836
|
+
const label = target ? `"${target}"` : `--id ${onlyId}`;
|
|
7837
|
+
if (result.dryRun) {
|
|
7838
|
+
if (result.invalidated === 0) {
|
|
7839
|
+
console.log(`DRY RUN - no memories would match ${label}.`);
|
|
7840
|
+
}
|
|
7841
|
+
else {
|
|
7842
|
+
console.log(`DRY RUN - ${result.invalidated} memories WOULD be invalidated:`);
|
|
7843
|
+
result.preview.forEach(p => console.log(` ${p.id} ${p.headline}`));
|
|
7844
|
+
}
|
|
7845
|
+
}
|
|
7846
|
+
else if (result.invalidated === 0) {
|
|
7847
|
+
console.log(`No memories matched ${label}.`);
|
|
7848
|
+
}
|
|
7849
|
+
else {
|
|
7850
|
+
console.log(`Invalidated ${result.invalidated} memories referencing ${label}.`);
|
|
7851
|
+
result.targets.forEach(id => console.log(` ${id}`));
|
|
7852
|
+
}
|
|
7853
|
+
if (result.skippedPinned.length > 0) {
|
|
7854
|
+
console.log(`Skipped ${result.skippedPinned.length} pinned: ${result.skippedPinned.join(', ')}`);
|
|
7855
|
+
}
|
|
7856
|
+
}
|
|
7857
|
+
/** Every verb main() dispatches, keyed by name, with its handler, aliases and help blocks. */
|
|
7858
|
+
export const COMMANDS = {
|
|
7859
|
+
init: {
|
|
7860
|
+
run: ({ hippoRoot, flags }) => { cmdInit(hippoRoot, flags); },
|
|
7861
|
+
usage: [`
|
|
7212
7862
|
init Create .hippo/ structure in current directory
|
|
7213
7863
|
--scan [dir] Find all git repos under dir (default: ~) and init each
|
|
7214
7864
|
--days <n> Days of git history to seed (default: 365 for --scan, 30 for single)
|
|
@@ -7217,7 +7867,11 @@ Commands:
|
|
|
7217
7867
|
(HIPPO_SKIP_AUTO_INTEGRATIONS=1 does the same)
|
|
7218
7868
|
--no-schedule Skip auto-creating the machine-level daily runner
|
|
7219
7869
|
--no-learn Skip seeding memories from git history and importing
|
|
7220
|
-
coding agents' own memories (every init imports those)
|
|
7870
|
+
coding agents' own memories (every init imports those)`],
|
|
7871
|
+
},
|
|
7872
|
+
remember: {
|
|
7873
|
+
run: handleRemember,
|
|
7874
|
+
usage: [`
|
|
7221
7875
|
remember <text> Store a memory
|
|
7222
7876
|
--tag <tag> Add a tag (repeatable)
|
|
7223
7877
|
--error Tag as error (boosts retention)
|
|
@@ -7225,11 +7879,11 @@ Commands:
|
|
|
7225
7879
|
--verified Set confidence: verified (default)
|
|
7226
7880
|
--observed Set confidence: observed
|
|
7227
7881
|
--inferred Set confidence: inferred
|
|
7228
|
-
--global Store in global store ($HIPPO_HOME or ~/.hippo/)
|
|
7229
|
-
|
|
7230
|
-
|
|
7231
|
-
|
|
7232
|
-
|
|
7882
|
+
--global Store in global store ($HIPPO_HOME or ~/.hippo/)`],
|
|
7883
|
+
},
|
|
7884
|
+
recall: {
|
|
7885
|
+
run: handleRecall,
|
|
7886
|
+
usage: [`
|
|
7233
7887
|
recall <query> Search and retrieve memories (local + global)
|
|
7234
7888
|
--budget <n> Token budget for the whole printed block (default: 4000)
|
|
7235
7889
|
--min-results <n> Minimum results regardless of budget (default: 1)
|
|
@@ -7314,24 +7968,48 @@ Commands:
|
|
|
7314
7968
|
where you left off in one call. Anchored on the
|
|
7315
7969
|
active snapshot's session_id; no anchor = no
|
|
7316
7970
|
handoff/events (use 'hippo session resume' for
|
|
7317
|
-
the explicit handoff-without-snapshot path)
|
|
7971
|
+
the explicit handoff-without-snapshot path).`],
|
|
7972
|
+
},
|
|
7973
|
+
drill: {
|
|
7974
|
+
run: handleDrill,
|
|
7975
|
+
usage: [`
|
|
7976
|
+
drill <summary-id> Walk down a DAG level-2 summary to its children
|
|
7977
|
+
--limit N Cap children list (default 50)
|
|
7978
|
+
--budget N Token budget for the printed children (≈ chars/4)
|
|
7979
|
+
--json Output as JSON`],
|
|
7980
|
+
},
|
|
7981
|
+
assemble: {
|
|
7982
|
+
run: handleAssemble,
|
|
7983
|
+
usage: [`
|
|
7984
|
+
assemble --session <id> Build a session's chronological context window
|
|
7985
|
+
--budget N Token budget for the printed window (default 4000)
|
|
7986
|
+
--fresh-tail N Recent rows always kept verbatim (default 10)
|
|
7987
|
+
--no-summarize-older Disable older-row summary substitution
|
|
7988
|
+
--scope <s> Restrict to exact scope (default: deny *:private:*)
|
|
7989
|
+
--json Output as JSON`],
|
|
7990
|
+
},
|
|
7991
|
+
supersede: {
|
|
7992
|
+
run: handleSupersede,
|
|
7993
|
+
usage: [`
|
|
7994
|
+
supersede <id> "<text>" Replace a memory with a new version; the old one points at it
|
|
7995
|
+
--layer <layer> Layer for the new memory (default: the old memory's layer)
|
|
7996
|
+
--tag <tag> Tag for the new memory (repeatable; default: the old memory's tags)
|
|
7997
|
+
--pin Pin the new memory (default: pinned if the old one was)`],
|
|
7998
|
+
},
|
|
7999
|
+
explain: {
|
|
8000
|
+
run: handleExplain,
|
|
8001
|
+
usage: [`
|
|
7318
8002
|
explain <query> Show full score breakdown for each retrieved memory
|
|
7319
8003
|
--budget <n> Token budget, counted as recall prints (default: 4000)
|
|
7320
8004
|
--limit <n> Cap the number of results displayed
|
|
7321
8005
|
--json Output as JSON
|
|
7322
8006
|
--physics | --classic Force search mode (default: from config)
|
|
7323
8007
|
--no-mmr Disable MMR diversity re-ranking
|
|
7324
|
-
--mmr-lambda <f> MMR balance 0..1 (default: 0.7, 1.0 = pure relevance)
|
|
7325
|
-
|
|
7326
|
-
|
|
7327
|
-
|
|
7328
|
-
|
|
7329
|
-
--limit <n> Cap the number of memories processed this run
|
|
7330
|
-
--all Ignore \`llm-refined\` tag and re-refine everything
|
|
7331
|
-
--dry-run Call the API but don't write results back
|
|
7332
|
-
--model <id> Override the default model (claude-sonnet-4-6)
|
|
7333
|
-
--json Output summary as JSON
|
|
7334
|
-
(requires ANTHROPIC_API_KEY in env)
|
|
8008
|
+
--mmr-lambda <f> MMR balance 0..1 (default: 0.7, 1.0 = pure relevance)`],
|
|
8009
|
+
},
|
|
8010
|
+
eval: {
|
|
8011
|
+
run: handleEval,
|
|
8012
|
+
usage: [`
|
|
7335
8013
|
eval [<corpus.json>] Measure recall quality against a test corpus
|
|
7336
8014
|
--bootstrap Generate a synthetic corpus from current memories
|
|
7337
8015
|
--out <path> With --bootstrap, write to file instead of stdout
|
|
@@ -7344,34 +8022,150 @@ Commands:
|
|
|
7344
8022
|
--local-bump <f> Local-over-global priority multiplier (default: 1.2)
|
|
7345
8023
|
--equal-sources Shortcut for --local-bump 1.0
|
|
7346
8024
|
--min-mrr <f> Exit non-zero if mean MRR falls below this
|
|
7347
|
-
--json Output full summary as JSON
|
|
7348
|
-
|
|
7349
|
-
|
|
7350
|
-
|
|
7351
|
-
|
|
7352
|
-
|
|
7353
|
-
|
|
7354
|
-
--
|
|
7355
|
-
|
|
8025
|
+
--json Output full summary as JSON`],
|
|
8026
|
+
},
|
|
8027
|
+
trace: {
|
|
8028
|
+
run: handleTrace,
|
|
8029
|
+
usage: [`
|
|
8030
|
+
trace <id> Memory dossier: content, decay trajectory, retrievals,
|
|
8031
|
+
outcomes, consolidation parents, open conflicts
|
|
8032
|
+
--json Output as JSON`],
|
|
8033
|
+
},
|
|
8034
|
+
refine: {
|
|
8035
|
+
run: async ({ hippoRoot, flags }) => { await cmdRefine(hippoRoot, flags); },
|
|
8036
|
+
usage: [`
|
|
8037
|
+
refine Rewrite consolidated semantic memories with Claude
|
|
8038
|
+
--limit <n> Cap the number of memories processed this run
|
|
8039
|
+
--all Ignore \`llm-refined\` tag and re-refine everything
|
|
8040
|
+
--dry-run Call the API but don't write results back
|
|
8041
|
+
--model <id> Override the default model (claude-sonnet-4-6)
|
|
8042
|
+
--json Output summary as JSON
|
|
8043
|
+
(requires ANTHROPIC_API_KEY in env)`],
|
|
8044
|
+
},
|
|
8045
|
+
sleep: {
|
|
8046
|
+
run: async ({ hippoRoot, flags }) => { await (await import('./cli/sleep.js')).cmdSleep(hippoRoot, flags); },
|
|
8047
|
+
usage: [`
|
|
7356
8048
|
sleep Run consolidation pass (auto-learns + dedup + auto-shares)
|
|
7357
8049
|
Runs at Claude Code and OpenCode session end and in the daily job.
|
|
7358
8050
|
With ANTHROPIC_API_KEY set it sends memory text to Anthropic for
|
|
7359
8051
|
fact extraction; {"extraction":{"enabled":false}} turns that off
|
|
7360
8052
|
--dry-run Preview without writing
|
|
7361
8053
|
--no-learn Skip auto git-learn and the agent memory import before consolidation
|
|
7362
|
-
--no-share Skip auto-sharing to global store
|
|
7363
|
-
|
|
8054
|
+
--no-share Skip auto-sharing to global store`],
|
|
8055
|
+
},
|
|
8056
|
+
'last-sleep': {
|
|
8057
|
+
run: ({ flags }) => { cmdLastSleep(flags); },
|
|
8058
|
+
usage: [`
|
|
8059
|
+
last-sleep Print the last 'hippo sleep --log-file' output to stderr and clear it
|
|
8060
|
+
--path <p> Log path (default: ~/.hippo/logs/last-sleep.log)
|
|
8061
|
+
--keep Print without clearing`],
|
|
8062
|
+
},
|
|
8063
|
+
'session-end': {
|
|
8064
|
+
run: async ({ hippoRoot, flags }) => { await cmdSessionEnd(hippoRoot, flags); },
|
|
8065
|
+
usage: [`
|
|
8066
|
+
session-end SessionEnd hook: count this session's re-read tokens, run sleep, then
|
|
8067
|
+
capture from the session's last 20 user and 10 assistant messages,
|
|
8068
|
+
in a detached worker
|
|
8069
|
+
--log-file <path> Tee the worker's output to a log file (paired with 'hippo last-sleep')`],
|
|
8070
|
+
},
|
|
8071
|
+
'__session-end-worker': {
|
|
8072
|
+
run: async ({ hippoRoot, flags }) => { await cmdSessionEndWorker(hippoRoot, flags); },
|
|
8073
|
+
usage: [],
|
|
8074
|
+
},
|
|
8075
|
+
'pre-compact': {
|
|
8076
|
+
run: handlePreCompact,
|
|
8077
|
+
usage: [`
|
|
8078
|
+
pre-compact PreCompact hook: record the compaction, save a working-state snapshot, and
|
|
8079
|
+
ask the summariser to end with a "Memories for hippo" list
|
|
8080
|
+
--log-file <p> Diagnostic log path (default: ~/.hippo/logs/pre-compact.log)`],
|
|
8081
|
+
},
|
|
8082
|
+
'post-compact': {
|
|
8083
|
+
run: handlePostCompact,
|
|
8084
|
+
usage: [`
|
|
8085
|
+
post-compact PostCompact hook: keep that list as memories (a busy store leaves the save to
|
|
8086
|
+
the next hippo sleep) and print one line saying how many
|
|
8087
|
+
--log-file <p> Same log path as pre-compact (default: ~/.hippo/logs/pre-compact.log)`],
|
|
8088
|
+
},
|
|
8089
|
+
'capture-error': {
|
|
8090
|
+
run: handleCaptureError,
|
|
8091
|
+
usage: [`
|
|
8092
|
+
capture-error Store a failed tool call as an error memory (reads the Claude Code
|
|
8093
|
+
PostToolUseFailure hook payload on stdin; skips routine failures)`],
|
|
8094
|
+
},
|
|
8095
|
+
'compact-resume': {
|
|
8096
|
+
run: handleCompactResume,
|
|
8097
|
+
usage: [`
|
|
8098
|
+
compact-resume SessionStart(compact) hook: re-print the snapshot, if under 15 minutes old`],
|
|
8099
|
+
},
|
|
8100
|
+
'codex-run': {
|
|
8101
|
+
run: ({ hippoRoot, args }) => { cmdCodexRun(hippoRoot, args); },
|
|
8102
|
+
usage: [`
|
|
8103
|
+
codex-run [-- ...args] Launch real Codex behind Hippo's session-end wrapper`],
|
|
8104
|
+
},
|
|
8105
|
+
'__codex-session-end-worker': {
|
|
8106
|
+
run: async ({ hippoRoot, flags }) => { await cmdCodexSessionEndWorker(hippoRoot, flags); },
|
|
8107
|
+
usage: [],
|
|
8108
|
+
},
|
|
8109
|
+
dedup: {
|
|
8110
|
+
run: ({ hippoRoot, flags }) => { cmdDedup(hippoRoot, flags); },
|
|
8111
|
+
usage: [`
|
|
7364
8112
|
dedup Remove duplicate memories (keeps stronger copy)
|
|
7365
8113
|
--dry-run Preview without removing
|
|
7366
|
-
--threshold <n> Ignored, kept for old scripts: a duplicate is the same text apart from spacing
|
|
7367
|
-
|
|
7368
|
-
|
|
7369
|
-
|
|
7370
|
-
|
|
7371
|
-
|
|
7372
|
-
|
|
7373
|
-
|
|
7374
|
-
|
|
8114
|
+
--threshold <n> Ignored, kept for old scripts: a duplicate is the same text apart from spacing`],
|
|
8115
|
+
},
|
|
8116
|
+
dag: {
|
|
8117
|
+
run: ({ hippoRoot, flags }) => { cmdDag(hippoRoot, flags); },
|
|
8118
|
+
usage: [`
|
|
8119
|
+
dag Show the summary tree: entity profiles, topic summaries, facts
|
|
8120
|
+
--stats Count memories per DAG level instead`],
|
|
8121
|
+
},
|
|
8122
|
+
auth: {
|
|
8123
|
+
run: ({ hippoRoot, args, flags }) => { cmdAuth(hippoRoot, args, flags); },
|
|
8124
|
+
usage: [`
|
|
8125
|
+
auth <sub> Manage API keys (A5 stub auth)
|
|
8126
|
+
auth create Mint a new API key (plaintext shown ONCE)
|
|
8127
|
+
--label <s> Optional human label
|
|
8128
|
+
--role <r> admin | member (default: admin; member blocked from /v1/sleep)
|
|
8129
|
+
--tenant <id> Override tenant (defaults to HIPPO_TENANT)
|
|
8130
|
+
--json Output as JSON
|
|
8131
|
+
--global Operate on the global store
|
|
8132
|
+
auth list List API keys (active by default)
|
|
8133
|
+
--all Include revoked keys
|
|
8134
|
+
--json Output as JSON
|
|
8135
|
+
--global Operate on the global store
|
|
8136
|
+
auth revoke <key_id> Revoke an API key (subsequent validate fails)
|
|
8137
|
+
--json Output as JSON
|
|
8138
|
+
--global Operate on the global store
|
|
8139
|
+
auth grant <key_id> <scope> Let a member key read one restricted scope
|
|
8140
|
+
--json Output as JSON
|
|
8141
|
+
--global Operate on the global store
|
|
8142
|
+
auth ungrant <key_id> <scope> Remove a scope grant
|
|
8143
|
+
--json Output as JSON
|
|
8144
|
+
--global Operate on the global store`],
|
|
8145
|
+
},
|
|
8146
|
+
goal: {
|
|
8147
|
+
run: ({ hippoRoot, args, flags }) => { cmdGoal(hippoRoot, args, flags); },
|
|
8148
|
+
usage: [`
|
|
8149
|
+
goal <sub> dlPFC goal stack (B3) — scoped per session
|
|
8150
|
+
goal push <name> Push a new active goal; prints the new goal id
|
|
8151
|
+
--policy <type> schema-fit-biased | error-prioritized |
|
|
8152
|
+
recency-first | hybrid
|
|
8153
|
+
--success "<cond>" Optional success condition text
|
|
8154
|
+
--level <n> Goal level (default: 0)
|
|
8155
|
+
--parent <goalId> Parent goal id (for sub-goals)
|
|
8156
|
+
--session-id <s> Override session (defaults to HIPPO_SESSION_ID)
|
|
8157
|
+
--tenant-id <t> Override tenant (defaults to HIPPO_TENANT)
|
|
8158
|
+
goal list Show active goals as a table
|
|
8159
|
+
--all Include suspended/completed goals
|
|
8160
|
+
goal complete <id> Mark a goal completed
|
|
8161
|
+
--outcome <0..1> Outcome score; >=0.7 boosts, <0.3 decays recalled mems
|
|
8162
|
+
--no-propagate Close the goal without applying strength side-effects
|
|
8163
|
+
goal suspend <id> Move an active goal to suspended
|
|
8164
|
+
goal resume <id> Move a suspended goal back to active (depth-capped)`],
|
|
8165
|
+
},
|
|
8166
|
+
slack: {
|
|
8167
|
+
run: ({ hippoRoot, args, flags }) => { cmdSlack(hippoRoot, args, flags); },
|
|
8168
|
+
usage: [`
|
|
7375
8169
|
slack Slack connector subcommands (backfill, dlq, workspaces)
|
|
7376
8170
|
backfill --channel <id> [--since ISO]
|
|
7377
8171
|
Backfill a channel's history (needs SLACK_BOT_TOKEN)
|
|
@@ -7379,53 +8173,109 @@ Commands:
|
|
|
7379
8173
|
dlq replay <id> [--force]
|
|
7380
8174
|
Re-ingest a DLQ entry (--force skips sig check)
|
|
7381
8175
|
workspaces <add|list|remove>
|
|
7382
|
-
Map Slack workspaces (team ids) to tenants
|
|
7383
|
-
|
|
7384
|
-
|
|
7385
|
-
|
|
7386
|
-
|
|
7387
|
-
|
|
7388
|
-
|
|
7389
|
-
|
|
7390
|
-
|
|
7391
|
-
|
|
7392
|
-
|
|
7393
|
-
|
|
7394
|
-
|
|
7395
|
-
|
|
7396
|
-
|
|
7397
|
-
|
|
8176
|
+
Map Slack workspaces (team ids) to tenants`],
|
|
8177
|
+
},
|
|
8178
|
+
github: {
|
|
8179
|
+
run: async ({ hippoRoot, args, flags }) => { await cmdGithub(hippoRoot, args, flags); },
|
|
8180
|
+
usage: [`
|
|
8181
|
+
github GitHub connector subcommands (backfill, dlq)
|
|
8182
|
+
backfill --repo <owner/name> [--since ISO] [--max <N>]
|
|
8183
|
+
Paginated backfill of issues + comments
|
|
8184
|
+
dlq list List DLQ entries for the active tenant
|
|
8185
|
+
dlq replay <id> [--force]
|
|
8186
|
+
Re-ingest a DLQ entry (--force skips sig check)`],
|
|
8187
|
+
},
|
|
8188
|
+
audit: {
|
|
8189
|
+
run: handleAudit,
|
|
8190
|
+
usage: [`
|
|
8191
|
+
audit [--fix] Check memory quality (--fix removes junk)`, `
|
|
8192
|
+
audit <sub> Query the append-only audit log (A5 stub auth)
|
|
8193
|
+
audit list List audit events for the active tenant
|
|
8194
|
+
--op <op> Filter by op (remember | recall | promote |
|
|
8195
|
+
supersede | forget | archive_raw | auth_revoke)
|
|
8196
|
+
--since <iso> Lower bound on ts (ISO timestamp)
|
|
8197
|
+
--limit <n> Max events (default: 100, max: 10000)
|
|
8198
|
+
--json Output as JSON
|
|
8199
|
+
--global Operate on the global store`],
|
|
8200
|
+
},
|
|
8201
|
+
'correction-latency': {
|
|
8202
|
+
run: handleCorrectionLatency,
|
|
8203
|
+
usage: [`
|
|
7398
8204
|
correction-latency Wall-clock lag from receipt to supersession (p50/p95/max)
|
|
8205
|
+
--json Output as JSON`],
|
|
8206
|
+
},
|
|
8207
|
+
provenance: {
|
|
8208
|
+
run: handleProvenance,
|
|
8209
|
+
usage: [`
|
|
8210
|
+
provenance Provenance coverage gate for kind='raw' rows
|
|
7399
8211
|
--json Output as JSON
|
|
8212
|
+
--strict Exit non-zero when coverage < 100%`],
|
|
8213
|
+
},
|
|
8214
|
+
status: {
|
|
8215
|
+
run: ({ hippoRoot }) => { cmdStatus(hippoRoot); },
|
|
8216
|
+
usage: [`
|
|
8217
|
+
status Show memory health stats`],
|
|
8218
|
+
},
|
|
8219
|
+
outcome: {
|
|
8220
|
+
run: ({ hippoRoot, flags }) => { cmdOutcome(hippoRoot, flags); },
|
|
8221
|
+
usage: [`
|
|
7400
8222
|
outcome Apply feedback to last recall
|
|
7401
8223
|
--good Memories were helpful
|
|
7402
8224
|
--bad Memories were irrelevant
|
|
7403
|
-
--id <id> Target a specific memory
|
|
8225
|
+
--id <id> Target a specific memory`],
|
|
8226
|
+
},
|
|
8227
|
+
conflicts: {
|
|
8228
|
+
run: ({ hippoRoot, flags }) => { cmdConflicts(hippoRoot, flags); },
|
|
8229
|
+
usage: [`
|
|
7404
8230
|
conflicts List detected open memory conflicts
|
|
7405
8231
|
--status <status> Filter by status (default: open)
|
|
7406
|
-
--json Output as JSON
|
|
8232
|
+
--json Output as JSON`],
|
|
8233
|
+
},
|
|
8234
|
+
resolve: {
|
|
8235
|
+
run: ({ hippoRoot, args, flags }) => { cmdResolve(hippoRoot, args, flags); },
|
|
8236
|
+
usage: [`
|
|
7407
8237
|
resolve <conflict_id> Resolve a memory conflict
|
|
7408
8238
|
--keep <memory_id> Memory to keep (required)
|
|
7409
8239
|
--forget Delete the losing memory (default: halve half-life)
|
|
7410
8240
|
--reject-loser Tombstone the loser's value too (implies removal)
|
|
7411
|
-
--reason "<why>" Reason for --reject-loser (default: conflict context)
|
|
8241
|
+
--reason "<why>" Reason for --reject-loser (default: conflict context)`],
|
|
8242
|
+
},
|
|
8243
|
+
reject: {
|
|
8244
|
+
run: ({ hippoRoot, args, flags }) => { cmdReject(hippoRoot, args, flags); },
|
|
8245
|
+
usage: [`
|
|
7412
8246
|
reject <memory-id> Tombstone a value so it refuses re-ingestion
|
|
7413
8247
|
reject --value "<t>" Pre-emptive form: tombstone a value not (currently) stored
|
|
7414
8248
|
--reason "<why>" Required. The tombstone stores no content — this
|
|
7415
8249
|
is its only human-readable identity.
|
|
7416
|
-
--global Reject in the global store
|
|
8250
|
+
--global Reject in the global store`],
|
|
8251
|
+
},
|
|
8252
|
+
rejections: {
|
|
8253
|
+
run: ({ hippoRoot, flags }) => { cmdRejections(hippoRoot, flags); },
|
|
8254
|
+
usage: [`
|
|
7417
8255
|
rejections List rejected-value tombstones for the active tenant
|
|
7418
8256
|
--json Output as JSON
|
|
7419
|
-
--global Operate on the global store
|
|
8257
|
+
--global Operate on the global store`],
|
|
8258
|
+
},
|
|
8259
|
+
unreject: {
|
|
8260
|
+
run: ({ hippoRoot, args, flags }) => { cmdUnreject(hippoRoot, args, flags); },
|
|
8261
|
+
usage: [`
|
|
7420
8262
|
unreject <digest-prefix> Delete a tombstone (the only escape hatch)
|
|
7421
|
-
--global Operate on the global store
|
|
8263
|
+
--global Operate on the global store`],
|
|
8264
|
+
},
|
|
8265
|
+
dormant: {
|
|
8266
|
+
run: ({ hippoRoot, args, flags }) => { cmdDormant(hippoRoot, args, flags); },
|
|
8267
|
+
usage: [`
|
|
7422
8268
|
dormant [<query>] List faded memories sleep kept instead of deleting
|
|
7423
8269
|
(on by default; "dormant": {"enabled": false} deletes instead)
|
|
7424
8270
|
--limit <n> Max rows, newest first (default: 20)
|
|
7425
8271
|
--json Output as JSON
|
|
7426
8272
|
--global Operate on the global store
|
|
7427
8273
|
dormant restore <id> Bring a dormant memory back to active memory
|
|
7428
|
-
dormant forget <id> Delete a dormant memory permanently
|
|
8274
|
+
dormant forget <id> Delete a dormant memory permanently`],
|
|
8275
|
+
},
|
|
8276
|
+
projects: {
|
|
8277
|
+
run: async ({ hippoRoot, args, flags }) => { (await import('./cli/projects.js')).cmdProjects(hippoRoot, args, flags); },
|
|
8278
|
+
usage: [`
|
|
7429
8279
|
projects [list] List the project names in a store, with a hint for old worktree names
|
|
7430
8280
|
--json Output as JSON
|
|
7431
8281
|
--global Operate on the global store
|
|
@@ -7434,21 +8284,21 @@ Commands:
|
|
|
7434
8284
|
writes a backup and one audit event first)
|
|
7435
8285
|
projects repair [--apply]
|
|
7436
8286
|
Re-tag merged rows older versions of sleep saved as user-global,
|
|
7437
|
-
by their parents' project (dry run unless --apply)
|
|
8287
|
+
by their parents' project (dry run unless --apply)`],
|
|
8288
|
+
},
|
|
8289
|
+
quarantine: {
|
|
8290
|
+
run: ({ hippoRoot, args, flags }) => { cmdQuarantine(hippoRoot, args, flags); },
|
|
8291
|
+
usage: [`
|
|
7438
8292
|
quarantine [list] List memories a connector flagged as an instruction attempt, pending review
|
|
7439
8293
|
--all Include approved and rejected rows too (default: pending only)
|
|
7440
8294
|
--json Output as JSON
|
|
7441
8295
|
--global Operate on the global store
|
|
7442
8296
|
quarantine approve <id> Restore a quarantined memory to its original scope
|
|
7443
|
-
quarantine reject <id> Keep a quarantined memory hidden for good
|
|
7444
|
-
|
|
7445
|
-
|
|
7446
|
-
|
|
7447
|
-
|
|
7448
|
-
support-bundle Write a redacted JSON file for a support ticket: versions, doctor,
|
|
7449
|
-
config without secrets, store counts, log names; never memory text
|
|
7450
|
-
--out <file> Where to write it (default: hippo-support-<time>.json here)
|
|
7451
|
-
--include-logs Add the last ${TAIL_MAX_LINES} lines of each hippo log, known secret shapes removed
|
|
8297
|
+
quarantine reject <id> Keep a quarantined memory hidden for good`],
|
|
8298
|
+
},
|
|
8299
|
+
tokens: {
|
|
8300
|
+
run: ({ hippoRoot, flags }) => { cmdTokens(hippoRoot, flags); },
|
|
8301
|
+
usage: [`
|
|
7452
8302
|
tokens Tokens of memory text hippo handed agents, per surface
|
|
7453
8303
|
(hook, compact-resume, context, recall, MCP, HTTP), what
|
|
7454
8304
|
skipping unchanged hook blocks saved, and how much of the
|
|
@@ -7456,12 +8306,34 @@ Commands:
|
|
|
7456
8306
|
counted when a session ends. Estimates (characters / 4)
|
|
7457
8307
|
--days <n> Window in days (default: 30)
|
|
7458
8308
|
--json Output as JSON
|
|
7459
|
-
--global Operate on the global store
|
|
8309
|
+
--global Operate on the global store`],
|
|
8310
|
+
},
|
|
8311
|
+
failures: {
|
|
8312
|
+
run: ({ hippoRoot, flags }) => { cmdFailures(hippoRoot, flags); },
|
|
8313
|
+
usage: [`
|
|
7460
8314
|
failures Failed tool calls capture-error saw, by outcome, and how
|
|
7461
8315
|
many errors first happened in another session
|
|
7462
8316
|
--days <n> Window in days (default: 30)
|
|
7463
8317
|
--json Output as JSON
|
|
7464
|
-
--global Operate on the global store
|
|
8318
|
+
--global Operate on the global store`],
|
|
8319
|
+
},
|
|
8320
|
+
doctor: {
|
|
8321
|
+
run: handleDoctor,
|
|
8322
|
+
usage: [`
|
|
8323
|
+
doctor Check the install: Node, store, schema, sleep, agent hooks
|
|
8324
|
+
--json Machine-readable report (exit code 1 on any failure)`],
|
|
8325
|
+
},
|
|
8326
|
+
'support-bundle': {
|
|
8327
|
+
run: handleSupportBundle,
|
|
8328
|
+
usage: [`
|
|
8329
|
+
support-bundle Write a redacted JSON file for a support ticket: versions, doctor,
|
|
8330
|
+
config without secrets, store counts, log names; never memory text
|
|
8331
|
+
--out <file> Where to write it (default: hippo-support-<time>.json here)
|
|
8332
|
+
--include-logs Add the last ${TAIL_MAX_LINES} lines of each hippo log, known secret shapes removed`],
|
|
8333
|
+
},
|
|
8334
|
+
snapshot: {
|
|
8335
|
+
run: ({ hippoRoot, args, flags }) => { cmdSnapshot(hippoRoot, args, flags); },
|
|
8336
|
+
usage: [`
|
|
7465
8337
|
snapshot <sub> Persist or inspect the current active task
|
|
7466
8338
|
snapshot save Save active task state
|
|
7467
8339
|
--task <task>
|
|
@@ -7472,7 +8344,11 @@ Commands:
|
|
|
7472
8344
|
snapshot show Show the active task snapshot
|
|
7473
8345
|
--json Output as JSON
|
|
7474
8346
|
snapshot clear Clear the active task snapshot
|
|
7475
|
-
--status <status> Mark final status (default: cleared)
|
|
8347
|
+
--status <status> Mark final status (default: cleared)`],
|
|
8348
|
+
},
|
|
8349
|
+
session: {
|
|
8350
|
+
run: ({ hippoRoot, args, flags }) => { cmdSession(hippoRoot, args, flags); },
|
|
8351
|
+
usage: [`
|
|
7476
8352
|
session <sub> Append or inspect short-term session history
|
|
7477
8353
|
session log Append a structured session event
|
|
7478
8354
|
--id <session-id>
|
|
@@ -7489,7 +8365,11 @@ Commands:
|
|
|
7489
8365
|
--id <session-id> Filter by session
|
|
7490
8366
|
--json Output as JSON
|
|
7491
8367
|
session resume Re-inject latest handoff as context output
|
|
7492
|
-
--id <session-id> Filter by session
|
|
8368
|
+
--id <session-id> Filter by session`],
|
|
8369
|
+
},
|
|
8370
|
+
handoff: {
|
|
8371
|
+
run: ({ hippoRoot, args, flags }) => { cmdHandoff(hippoRoot, args, flags); },
|
|
8372
|
+
usage: [`
|
|
7493
8373
|
handoff <sub> Manage session handoffs for continuity
|
|
7494
8374
|
handoff create Create a new session handoff
|
|
7495
8375
|
--summary <text> Handoff summary (required)
|
|
@@ -7505,7 +8385,11 @@ Commands:
|
|
|
7505
8385
|
handoff latest Show the most recent handoff
|
|
7506
8386
|
--session <id> Filter by session
|
|
7507
8387
|
--json Output as JSON
|
|
7508
|
-
handoff show <id> Show a specific handoff by ID
|
|
8388
|
+
handoff show <id> Show a specific handoff by ID`],
|
|
8389
|
+
},
|
|
8390
|
+
card: {
|
|
8391
|
+
run: ({ hippoRoot, args, flags }) => { cmdCard(hippoRoot, args, flags); },
|
|
8392
|
+
usage: [`
|
|
7509
8393
|
card <sub> Manage claimable work-queue cards
|
|
7510
8394
|
card create Create a new card
|
|
7511
8395
|
--title <text> Card title (required)
|
|
@@ -7533,29 +8417,134 @@ Commands:
|
|
|
7533
8417
|
card reclaim Return every running card whose lease has expired to ready
|
|
7534
8418
|
card comment <id> Add a comment to a card
|
|
7535
8419
|
--body <text> Comment body (required)
|
|
7536
|
-
--author <name> Comment author (default: cli)
|
|
8420
|
+
--author <name> Comment author (default: cli)`],
|
|
8421
|
+
},
|
|
8422
|
+
predict: {
|
|
8423
|
+
run: ({ hippoRoot, args, flags }) => { cmdPredict(hippoRoot, args, flags); },
|
|
8424
|
+
usage: [`
|
|
8425
|
+
predict "<claim>" Record a prediction to score against the actual outcome later
|
|
8426
|
+
--class <c> Reference class (required)
|
|
8427
|
+
--estimate <v> Numeric estimate
|
|
8428
|
+
--unit <u> Unit of the estimate
|
|
8429
|
+
--target <YYYY-MM-DD> When the outcome is due
|
|
8430
|
+
predict close <id> Close a prediction
|
|
8431
|
+
--state <s> closed | closed-unknown (required)
|
|
8432
|
+
--actual <v> The actual value
|
|
8433
|
+
--note "<text>" Closure note
|
|
8434
|
+
predict list [--class <c>] [--status open|closed|closed-unknown|all] [--limit N]
|
|
8435
|
+
List predictions (closed and closed-unknown need --class)
|
|
8436
|
+
predict show <id> Show one prediction
|
|
8437
|
+
predict baserate --class <c>
|
|
8438
|
+
How past estimates in a class compared with the actuals`],
|
|
8439
|
+
},
|
|
8440
|
+
current: {
|
|
8441
|
+
run: ({ hippoRoot, args, flags }) => { cmdCurrent(hippoRoot, args, flags); },
|
|
8442
|
+
usage: [`
|
|
7537
8443
|
current <sub> Show compact current state for agent injection
|
|
7538
8444
|
current show Active task + recent session events (default)
|
|
7539
|
-
--json Output as JSON
|
|
8445
|
+
--json Output as JSON`],
|
|
8446
|
+
},
|
|
8447
|
+
forget: {
|
|
8448
|
+
run: handleForget,
|
|
8449
|
+
usage: [`
|
|
7540
8450
|
forget <id> Force remove a memory
|
|
7541
8451
|
--archive Archive a raw (append-only) memory instead of deleting
|
|
7542
|
-
--reason "<why>" Reason recorded on the archive (required with --archive)
|
|
7543
|
-
|
|
8452
|
+
--reason "<why>" Reason recorded on the archive (required with --archive)`],
|
|
8453
|
+
},
|
|
8454
|
+
inspect: {
|
|
8455
|
+
run: handleInspect,
|
|
8456
|
+
usage: [`
|
|
8457
|
+
inspect <id> Show full memory detail`],
|
|
8458
|
+
},
|
|
8459
|
+
context: {
|
|
8460
|
+
run: handleContext,
|
|
8461
|
+
usage: [`
|
|
8462
|
+
context Smart context injection for AI agents
|
|
8463
|
+
--auto Auto-detect task from git state
|
|
8464
|
+
--budget <n> Token budget for the whole printed block (default: 1500)
|
|
8465
|
+
--pinned-only Only inject pinned memories (used by UserPromptSubmit hook)
|
|
8466
|
+
--include-recent <n> With --pinned-only, also inject the last N writes regardless of pinning
|
|
8467
|
+
(the hook payload's "prompt" drives prompt recall instead of --include-recent when pinnedInject.promptRecall is on, the default)
|
|
8468
|
+
--format <fmt> Output format: markdown (default), json, or additional-context (Claude Code hook JSON)
|
|
8469
|
+
--framing <mode> Framing: observe (default), suggest, assert`],
|
|
8470
|
+
},
|
|
8471
|
+
hook: {
|
|
8472
|
+
run: ({ args, flags }) => { cmdHook(args, flags); },
|
|
8473
|
+
usage: [`
|
|
8474
|
+
hook <sub> [target] Manage framework integrations
|
|
8475
|
+
hook list Show available hooks
|
|
8476
|
+
hook install <target> Install hook (claude-code|codex|cursor|openclaw|opencode|pi)
|
|
8477
|
+
claude-code adds 7 hooks to ~/.claude/settings.json;
|
|
8478
|
+
opencode installs a plugin; codex adds 2 hooks to
|
|
8479
|
+
$CODEX_HOME/hooks.json (trust them once in /hooks) and
|
|
8480
|
+
wraps the detected launcher in place; all but claude-code
|
|
8481
|
+
also patch an existing AGENTS.md
|
|
8482
|
+
hook uninstall <target> Remove hook`],
|
|
8483
|
+
},
|
|
8484
|
+
setup: {
|
|
8485
|
+
run: ({ flags }) => { cmdSetup(flags); },
|
|
8486
|
+
usage: [`
|
|
8487
|
+
setup One-shot: detect installed AI tools and install their hooks:
|
|
8488
|
+
claude-code gets 7 hooks in ~/.claude/settings.json, opencode
|
|
8489
|
+
a plugin, codex 2 hooks in its hooks.json plus a launcher
|
|
8490
|
+
wrapper; other tools get a hint. Then imports each agent's
|
|
8491
|
+
user-level memories into the global store
|
|
8492
|
+
--all Install for every JSON-hook tool, even if not detected
|
|
8493
|
+
--dry-run Show what would be installed without writing
|
|
8494
|
+
--no-schedule Skip installing or repairing the daily runner
|
|
8495
|
+
--no-learn Skip the agent memory import`],
|
|
8496
|
+
},
|
|
8497
|
+
'daily-runner': {
|
|
8498
|
+
run: () => { cmdDailyRunner(); },
|
|
8499
|
+
usage: [`
|
|
8500
|
+
daily-runner Sweep registered workspaces and run daily learn+sleep`],
|
|
8501
|
+
},
|
|
8502
|
+
embed: {
|
|
8503
|
+
run: async ({ hippoRoot, flags }) => { await cmdEmbed(hippoRoot, flags); },
|
|
8504
|
+
usage: [`
|
|
7544
8505
|
embed Embed all memories for semantic search
|
|
7545
|
-
--status Show embedding coverage
|
|
7546
|
-
|
|
8506
|
+
--status Show embedding coverage`],
|
|
8507
|
+
},
|
|
8508
|
+
watch: {
|
|
8509
|
+
run: handleWatch,
|
|
8510
|
+
usage: [`
|
|
8511
|
+
watch "<command>" Run command, auto-learn from failures`],
|
|
8512
|
+
},
|
|
8513
|
+
learn: {
|
|
8514
|
+
run: ({ hippoRoot, flags }) => { cmdLearn(hippoRoot, flags); },
|
|
8515
|
+
usage: [`
|
|
7547
8516
|
learn Learn lessons from repository history
|
|
7548
8517
|
--git Scan recent git commits for lessons
|
|
7549
8518
|
--days <n> Scan this many days back (default: 7)
|
|
7550
|
-
--repos <paths> Comma-separated repo paths to scan
|
|
7551
|
-
|
|
8519
|
+
--repos <paths> Comma-separated repo paths to scan`],
|
|
8520
|
+
},
|
|
8521
|
+
promote: {
|
|
8522
|
+
run: handlePromote,
|
|
8523
|
+
usage: [`
|
|
8524
|
+
promote <id> Copy a local memory to the global store`],
|
|
8525
|
+
},
|
|
8526
|
+
sync: {
|
|
8527
|
+
run: ({ hippoRoot, flags }) => { cmdSync(hippoRoot, flags); },
|
|
8528
|
+
usage: [`
|
|
8529
|
+
sync Pull global memories into local project`],
|
|
8530
|
+
},
|
|
8531
|
+
share: {
|
|
8532
|
+
run: handleShare,
|
|
8533
|
+
usage: [`
|
|
7552
8534
|
share <id> Share a memory with attribution + transfer scoring
|
|
7553
8535
|
--force Share even if transfer score is low
|
|
7554
8536
|
--auto Auto-share all high-transfer-score memories
|
|
7555
8537
|
--dry-run Preview what would be shared
|
|
7556
|
-
--min-score <n> Minimum transfer score (default: 0.6)
|
|
7557
|
-
|
|
7558
|
-
|
|
8538
|
+
--min-score <n> Minimum transfer score (default: 0.6)`],
|
|
8539
|
+
},
|
|
8540
|
+
peers: {
|
|
8541
|
+
run: handlePeers,
|
|
8542
|
+
usage: [`
|
|
8543
|
+
peers List projects contributing to global store`],
|
|
8544
|
+
},
|
|
8545
|
+
import: {
|
|
8546
|
+
run: ({ hippoRoot, args, flags }) => { cmdImport(hippoRoot, args, flags); },
|
|
8547
|
+
usage: [`
|
|
7559
8548
|
import Import memories from other AI tools
|
|
7560
8549
|
--chatgpt <path> Import from ChatGPT memory export (JSON or txt)
|
|
7561
8550
|
--claude <path> Import from CLAUDE.md or Claude memory.json
|
|
@@ -7572,9 +8561,17 @@ Commands:
|
|
|
7572
8561
|
picks the tools; "none" or [] turns the import off
|
|
7573
8562
|
--dry-run Preview without writing
|
|
7574
8563
|
--global Write to global store ($HIPPO_HOME or ~/.hippo/)
|
|
7575
|
-
--tag <tag> Add extra tag (repeatable)
|
|
8564
|
+
--tag <tag> Add extra tag (repeatable)`],
|
|
8565
|
+
},
|
|
8566
|
+
export: {
|
|
8567
|
+
run: handleExport,
|
|
8568
|
+
usage: [`
|
|
7576
8569
|
export [file] Export all memories (default: stdout)
|
|
7577
|
-
--format <fmt> Output format: json (default) or markdown
|
|
8570
|
+
--format <fmt> Output format: json (default) or markdown`],
|
|
8571
|
+
},
|
|
8572
|
+
capture: {
|
|
8573
|
+
run: handleCapture,
|
|
8574
|
+
usage: [`
|
|
7578
8575
|
capture Extract memories from conversation text
|
|
7579
8576
|
--stdin Read from piped input
|
|
7580
8577
|
--file <path> Read from a file
|
|
@@ -7583,61 +8580,80 @@ Commands:
|
|
|
7583
8580
|
--transcript <path> Explicit transcript path (implies --last-session)
|
|
7584
8581
|
--log-file <path> Tee output to a log file (paired with 'hippo last-sleep')
|
|
7585
8582
|
--dry-run Preview without writing
|
|
7586
|
-
--global Write to global store ($HIPPO_HOME or ~/.hippo/)
|
|
7587
|
-
|
|
7588
|
-
|
|
7589
|
-
|
|
7590
|
-
|
|
7591
|
-
|
|
7592
|
-
--
|
|
7593
|
-
|
|
7594
|
-
|
|
7595
|
-
|
|
7596
|
-
|
|
7597
|
-
|
|
7598
|
-
|
|
7599
|
-
|
|
7600
|
-
|
|
7601
|
-
|
|
7602
|
-
|
|
7603
|
-
|
|
7604
|
-
|
|
7605
|
-
|
|
7606
|
-
|
|
7607
|
-
|
|
7608
|
-
|
|
7609
|
-
|
|
7610
|
-
|
|
7611
|
-
|
|
7612
|
-
|
|
7613
|
-
|
|
7614
|
-
|
|
7615
|
-
|
|
7616
|
-
|
|
7617
|
-
|
|
7618
|
-
|
|
7619
|
-
|
|
7620
|
-
|
|
7621
|
-
|
|
7622
|
-
|
|
7623
|
-
|
|
7624
|
-
|
|
7625
|
-
|
|
7626
|
-
--
|
|
7627
|
-
|
|
7628
|
-
|
|
7629
|
-
|
|
7630
|
-
|
|
7631
|
-
|
|
7632
|
-
|
|
7633
|
-
|
|
8583
|
+
--global Write to global store ($HIPPO_HOME or ~/.hippo/)`],
|
|
8584
|
+
},
|
|
8585
|
+
dashboard: {
|
|
8586
|
+
run: handleDashboard,
|
|
8587
|
+
usage: [`
|
|
8588
|
+
dashboard Open web dashboard for memory health
|
|
8589
|
+
--port <n> Port to serve on (default: 3333)`],
|
|
8590
|
+
},
|
|
8591
|
+
wm: {
|
|
8592
|
+
run: ({ hippoRoot, args, flags }) => { cmdWm(hippoRoot, args, flags); },
|
|
8593
|
+
usage: [`
|
|
8594
|
+
wm <sub> Working memory — bounded buffer for current state
|
|
8595
|
+
wm push Push a working memory entry
|
|
8596
|
+
--scope <scope> Scope name (default: default)
|
|
8597
|
+
--content <text> Content to store (required)
|
|
8598
|
+
--importance <n> Priority 0-1 (default: 0.5)
|
|
8599
|
+
--session <id> Session ID
|
|
8600
|
+
--task <id> Task ID
|
|
8601
|
+
wm read Read working memory entries
|
|
8602
|
+
--scope <scope> Filter by scope
|
|
8603
|
+
--session <id> Filter by session
|
|
8604
|
+
--limit <n> Max entries (default: 20)
|
|
8605
|
+
--json Output as JSON
|
|
8606
|
+
wm clear Clear working memory entries
|
|
8607
|
+
--scope <scope> Filter by scope
|
|
8608
|
+
--session <id> Filter by session
|
|
8609
|
+
wm flush Same as clear; nothing runs it at session end
|
|
8610
|
+
--scope <scope> Filter by scope
|
|
8611
|
+
--session <id> Filter by session`],
|
|
8612
|
+
},
|
|
8613
|
+
mcp: {
|
|
8614
|
+
run: handleMcp,
|
|
8615
|
+
usage: [`
|
|
8616
|
+
mcp Start MCP server (stdio transport)`],
|
|
8617
|
+
},
|
|
8618
|
+
serve: {
|
|
8619
|
+
run: handleServe,
|
|
8620
|
+
usage: [`
|
|
8621
|
+
serve Start the HTTP API server for this store (Ctrl+C stops it)
|
|
8622
|
+
--port <n> Port to serve on (default: $HIPPO_PORT or 6789)
|
|
8623
|
+
--host <host> Address to bind (default: 127.0.0.1)`],
|
|
8624
|
+
},
|
|
8625
|
+
invalidate: {
|
|
8626
|
+
run: handleInvalidate,
|
|
8627
|
+
usage: [`
|
|
8628
|
+
invalidate "<pattern>" Actively weaken memories matching an old pattern
|
|
8629
|
+
(content overlap, or a tag EXACTLY equal to the
|
|
8630
|
+
full pattern - never token-level tag matching)
|
|
8631
|
+
--id <memory-id> Invalidate exactly one memory (instead of a pattern)
|
|
8632
|
+
--dry-run Preview what would be hit; writes nothing
|
|
8633
|
+
Note: a pattern equal to the system tag
|
|
8634
|
+
'invalidated' re-weakens previously invalidated
|
|
8635
|
+
memories - preview with --dry-run first
|
|
8636
|
+
--reason "<why>" Optional: what replaced it
|
|
8637
|
+
invalidate --churn FE2: tag memories 'churn-stale' whose named file
|
|
8638
|
+
changed or was deleted, or whose named symbol or
|
|
8639
|
+
npm script was removed, in this repo's git history
|
|
8640
|
+
since the memory was stored or confirmed
|
|
8641
|
+
--dry-run Preview what would be tagged; writes nothing`],
|
|
8642
|
+
},
|
|
8643
|
+
decide: {
|
|
8644
|
+
run: ({ hippoRoot, args, flags }) => { cmdDecide(hippoRoot, args, flags); },
|
|
8645
|
+
usage: [`
|
|
7634
8646
|
decide "<decision>" Record a decision (first-class object + memory mirror)
|
|
7635
8647
|
--context "<why>" Why this decision was made
|
|
7636
8648
|
--supersedes <mem-id> Supersede the decision backed by this memory id
|
|
7637
8649
|
decide list [--status active|superseded|closed|all] [--limit N]
|
|
7638
8650
|
List decisions (table is authoritative, survives decay)
|
|
7639
8651
|
decide get <id> Show a decision by its table id
|
|
7640
|
-
decide close <id> Retire (close) an active decision by its table id
|
|
8652
|
+
decide close <id> Retire (close) an active decision by its table id`],
|
|
8653
|
+
},
|
|
8654
|
+
incident: {
|
|
8655
|
+
run: ({ hippoRoot, args, flags }) => { cmdIncident(hippoRoot, args, flags); },
|
|
8656
|
+
usage: [`
|
|
7641
8657
|
incident "<incident>" Record an incident (first-class object + memory mirror)
|
|
7642
8658
|
--context "<details>" What happened / surrounding detail
|
|
7643
8659
|
--link <mem-id> Link a memory as evidence (repeatable)
|
|
@@ -7646,7 +8662,11 @@ Commands:
|
|
|
7646
8662
|
incident get <id> Show an incident by its table id
|
|
7647
8663
|
incident resolve <id> Resolve an open incident (open -> resolved)
|
|
7648
8664
|
--resolution "<text>" How it was resolved (required)
|
|
7649
|
-
incident close <id> Retire (close) an open or resolved incident by its table id
|
|
8665
|
+
incident close <id> Retire (close) an open or resolved incident by its table id`],
|
|
8666
|
+
},
|
|
8667
|
+
process: {
|
|
8668
|
+
run: ({ hippoRoot, args, flags }) => { cmdProcess(hippoRoot, args, flags); },
|
|
8669
|
+
usage: [`
|
|
7650
8670
|
process new "<name>" Record a process map (first-class object + memory mirror)
|
|
7651
8671
|
--step "<text>" An ordered step (repeatable)
|
|
7652
8672
|
--description "<text>" Optional summary of the process
|
|
@@ -7657,7 +8677,11 @@ Commands:
|
|
|
7657
8677
|
--step "<text>" A step of the new version (repeatable, required)
|
|
7658
8678
|
--change "<summary>" What changed in this version (the delta note)
|
|
7659
8679
|
--description "<text>" Optional summary of the new version
|
|
7660
|
-
process close <id> Retire (close) an active process by its table id
|
|
8680
|
+
process close <id> Retire (close) an active process by its table id`],
|
|
8681
|
+
},
|
|
8682
|
+
policy: {
|
|
8683
|
+
run: ({ hippoRoot, args, flags }) => { cmdPolicy(hippoRoot, args, flags); },
|
|
8684
|
+
usage: [`
|
|
7661
8685
|
policy new "<name>" Record a policy (bi-temporal first-class object + mirror)
|
|
7662
8686
|
--text "<rule>" The policy rule/statement (required)
|
|
7663
8687
|
--from "<iso>" Effective-from date (default: now)
|
|
@@ -7672,7 +8696,11 @@ Commands:
|
|
|
7672
8696
|
--from "<iso>" New effective-from (default: now)
|
|
7673
8697
|
--to "<iso>" New effective-to (optional)
|
|
7674
8698
|
--change "<summary>" What changed in this version (the delta note)
|
|
7675
|
-
policy close <id> Retire (close) an active policy by its table id
|
|
8699
|
+
policy close <id> Retire (close) an active policy by its table id`],
|
|
8700
|
+
},
|
|
8701
|
+
skill: {
|
|
8702
|
+
run: ({ hippoRoot, args, flags }) => { cmdSkill(hippoRoot, args, flags); },
|
|
8703
|
+
usage: [`
|
|
7676
8704
|
skill new "<name>" Record a skill (reusable agent-followable capability)
|
|
7677
8705
|
--instructions "<txt>" The skill body (required)
|
|
7678
8706
|
--trigger "<when>" Optional: when to apply this skill
|
|
@@ -7684,7 +8712,12 @@ Commands:
|
|
|
7684
8712
|
--instructions "<txt>" The new skill body (required)
|
|
7685
8713
|
--trigger "<when>" Optional new trigger
|
|
7686
8714
|
--change "<summary>" What changed in this version (the delta note)
|
|
7687
|
-
skill close <id> Retire (close) an active skill by its table id
|
|
8715
|
+
skill close <id> Retire (close) an active skill by its table id`],
|
|
8716
|
+
},
|
|
8717
|
+
brief: {
|
|
8718
|
+
run: ({ hippoRoot, args, flags }) => { cmdProjectBrief(hippoRoot, args, flags); },
|
|
8719
|
+
aliases: ['project-brief'],
|
|
8720
|
+
usage: [`
|
|
7688
8721
|
brief new "<repo>" Record a repo-scoped project brief
|
|
7689
8722
|
--summary "<text>" The brief body (required)
|
|
7690
8723
|
brief list [--status active|superseded|closed|all] [--repo "<repo>"] [--limit N]
|
|
@@ -7695,7 +8728,12 @@ Commands:
|
|
|
7695
8728
|
--change "<summary>" What changed in this version (the delta note)
|
|
7696
8729
|
brief close <id> Retire (close) an active project brief by its table id
|
|
7697
8730
|
brief refresh "<repo>" Auto-assemble the brief from the repo's receipts (path:<repo>)
|
|
7698
|
-
--dry-run Print the assembled brief without writing it
|
|
8731
|
+
--dry-run Print the assembled brief without writing it`],
|
|
8732
|
+
},
|
|
8733
|
+
note: {
|
|
8734
|
+
run: ({ hippoRoot, args, flags }) => { cmdCustomerNote(hippoRoot, args, flags); },
|
|
8735
|
+
aliases: ['customer-note'],
|
|
8736
|
+
usage: [`
|
|
7699
8737
|
note new "<customer>" Record a customer/account-scoped note
|
|
7700
8738
|
--text "<note>" The note body (required)
|
|
7701
8739
|
note list [--status active|superseded|closed|all] [--customer "<id>"] [--limit N]
|
|
@@ -7704,91 +8742,35 @@ Commands:
|
|
|
7704
8742
|
note supersede <id> Record a new version that supersedes an active note
|
|
7705
8743
|
--text "<note>" The new note body (required)
|
|
7706
8744
|
--change "<summary>" What changed in this version (the delta note)
|
|
7707
|
-
note close <id> Retire (close) an active customer note by its table id
|
|
8745
|
+
note close <id> Retire (close) an active customer note by its table id`],
|
|
8746
|
+
},
|
|
8747
|
+
graph: {
|
|
8748
|
+
run: ({ hippoRoot, args, flags }) => { cmdGraph(hippoRoot, args, flags); },
|
|
8749
|
+
usage: [`
|
|
7708
8750
|
graph extract Rebuild the entity/relation graph from consolidated objects
|
|
7709
|
-
(decisions/policies/customer-notes/project-briefs); idempotent
|
|
7710
|
-
|
|
7711
|
-
|
|
7712
|
-
|
|
7713
|
-
|
|
7714
|
-
|
|
7715
|
-
|
|
7716
|
-
|
|
7717
|
-
|
|
7718
|
-
|
|
7719
|
-
|
|
7720
|
-
|
|
7721
|
-
|
|
7722
|
-
|
|
7723
|
-
|
|
7724
|
-
|
|
7725
|
-
|
|
7726
|
-
|
|
7727
|
-
|
|
7728
|
-
|
|
7729
|
-
|
|
7730
|
-
|
|
7731
|
-
|
|
7732
|
-
--scope <scope> Filter by scope
|
|
7733
|
-
--session <id> Filter by session
|
|
7734
|
-
--limit <n> Max entries (default: 20)
|
|
7735
|
-
--json Output as JSON
|
|
7736
|
-
wm clear Clear working memory entries
|
|
7737
|
-
--scope <scope> Filter by scope
|
|
7738
|
-
--session <id> Filter by session
|
|
7739
|
-
wm flush Same as clear; nothing runs it at session end
|
|
7740
|
-
--scope <scope> Filter by scope
|
|
7741
|
-
--session <id> Filter by session
|
|
7742
|
-
dashboard Open web dashboard for memory health
|
|
7743
|
-
--port <n> Port to serve on (default: 3333)
|
|
7744
|
-
mcp Start MCP server (stdio transport)
|
|
7745
|
-
serve Start the HTTP API server for this store (Ctrl+C stops it)
|
|
7746
|
-
--port <n> Port to serve on (default: $HIPPO_PORT or 6789)
|
|
7747
|
-
--host <host> Address to bind (default: 127.0.0.1)
|
|
7748
|
-
goal <sub> dlPFC goal stack (B3) — scoped per session
|
|
7749
|
-
goal push <name> Push a new active goal; prints the new goal id
|
|
7750
|
-
--policy <type> schema-fit-biased | error-prioritized |
|
|
7751
|
-
recency-first | hybrid
|
|
7752
|
-
--success "<cond>" Optional success condition text
|
|
7753
|
-
--level <n> Goal level (default: 0)
|
|
7754
|
-
--parent <goalId> Parent goal id (for sub-goals)
|
|
7755
|
-
--session-id <s> Override session (defaults to HIPPO_SESSION_ID)
|
|
7756
|
-
--tenant-id <t> Override tenant (defaults to HIPPO_TENANT)
|
|
7757
|
-
goal list Show active goals as a table
|
|
7758
|
-
--all Include suspended/completed goals
|
|
7759
|
-
goal complete <id> Mark a goal completed
|
|
7760
|
-
--outcome <0..1> Outcome score; >=0.7 boosts, <0.3 decays recalled mems
|
|
7761
|
-
--no-propagate Close the goal without applying strength side-effects
|
|
7762
|
-
goal suspend <id> Move an active goal to suspended
|
|
7763
|
-
goal resume <id> Move a suspended goal back to active (depth-capped)
|
|
7764
|
-
auth <sub> Manage API keys (A5 stub auth)
|
|
7765
|
-
auth create Mint a new API key (plaintext shown ONCE)
|
|
7766
|
-
--label <s> Optional human label
|
|
7767
|
-
--role <r> admin | member (default: admin; member blocked from /v1/sleep)
|
|
7768
|
-
--tenant <id> Override tenant (defaults to HIPPO_TENANT)
|
|
7769
|
-
--json Output as JSON
|
|
7770
|
-
--global Operate on the global store
|
|
7771
|
-
auth list List API keys (active by default)
|
|
7772
|
-
--all Include revoked keys
|
|
7773
|
-
--json Output as JSON
|
|
7774
|
-
--global Operate on the global store
|
|
7775
|
-
auth revoke <key_id> Revoke an API key (subsequent validate fails)
|
|
7776
|
-
--json Output as JSON
|
|
7777
|
-
--global Operate on the global store
|
|
7778
|
-
auth grant <key_id> <scope> Let a member key read one restricted scope
|
|
7779
|
-
--json Output as JSON
|
|
7780
|
-
--global Operate on the global store
|
|
7781
|
-
auth ungrant <key_id> <scope> Remove a scope grant
|
|
7782
|
-
--json Output as JSON
|
|
7783
|
-
--global Operate on the global store
|
|
7784
|
-
audit <sub> Query the append-only audit log (A5 stub auth)
|
|
7785
|
-
audit list List audit events for the active tenant
|
|
7786
|
-
--op <op> Filter by op (remember | recall | promote |
|
|
7787
|
-
supersede | forget | archive_raw | auth_revoke)
|
|
7788
|
-
--since <iso> Lower bound on ts (ISO timestamp)
|
|
7789
|
-
--limit <n> Max events (default: 100, max: 10000)
|
|
7790
|
-
--json Output as JSON
|
|
7791
|
-
--global Operate on the global store
|
|
8751
|
+
(decisions/policies/customer-notes/project-briefs); idempotent`],
|
|
8752
|
+
},
|
|
8753
|
+
};
|
|
8754
|
+
const COMMAND_INDEX = new Map(Object.entries(COMMANDS).flatMap(([verb, spec]) => [verb, ...(spec.aliases ?? [])].map((name) => [name, spec])));
|
|
8755
|
+
// The full listing order; a verb listed twice prints its next help block.
|
|
8756
|
+
const USAGE_ORDER = [
|
|
8757
|
+
'init', 'remember', 'supersede', 'recall', 'explain', 'trace', 'refine', 'eval', 'context', 'sleep',
|
|
8758
|
+
'daily-runner', 'dedup', 'status', 'audit', 'github', 'slack', 'provenance', 'dag', 'drill', 'assemble',
|
|
8759
|
+
'correction-latency', 'outcome', 'conflicts', 'resolve', 'reject', 'rejections', 'unreject', 'dormant',
|
|
8760
|
+
'projects', 'quarantine', 'capture-error', 'doctor', 'support-bundle', 'tokens', 'failures', 'snapshot',
|
|
8761
|
+
'session', 'handoff', 'card', 'current', 'forget', 'inspect', 'embed', 'watch', 'learn', 'promote',
|
|
8762
|
+
'share', 'peers', 'sync', 'import', 'export', 'capture', 'setup', 'last-sleep', 'session-end',
|
|
8763
|
+
'pre-compact', 'compact-resume', 'post-compact', 'codex-run', 'hook', 'predict', 'decide', 'incident',
|
|
8764
|
+
'process', 'policy', 'skill', 'brief', 'note', 'graph', 'invalidate', 'wm', 'dashboard', 'mcp', 'serve',
|
|
8765
|
+
'goal', 'auth', 'audit',
|
|
8766
|
+
];
|
|
8767
|
+
const USAGE_HEADER = `
|
|
8768
|
+
Hippo - memory for AI agents that learns what is wrong and ranks it down
|
|
8769
|
+
|
|
8770
|
+
Usage: hippo <command> [options]
|
|
8771
|
+
|
|
8772
|
+
Commands:`;
|
|
8773
|
+
const USAGE_EXAMPLES = `
|
|
7792
8774
|
|
|
7793
8775
|
Examples:
|
|
7794
8776
|
hippo init
|
|
@@ -7843,25 +8825,22 @@ Examples:
|
|
|
7843
8825
|
hippo outcome --good
|
|
7844
8826
|
hippo status
|
|
7845
8827
|
`;
|
|
8828
|
+
export function usageText() {
|
|
8829
|
+
const printed = new Map();
|
|
8830
|
+
const blocks = USAGE_ORDER.map((verb) => {
|
|
8831
|
+
const index = printed.get(verb) ?? 0;
|
|
8832
|
+
printed.set(verb, index + 1);
|
|
8833
|
+
return COMMANDS[verb].usage[index];
|
|
8834
|
+
});
|
|
8835
|
+
return USAGE_HEADER + blocks.join('') + USAGE_EXAMPLES;
|
|
7846
8836
|
}
|
|
7847
8837
|
function printUsage() {
|
|
7848
8838
|
console.log(usageText());
|
|
7849
8839
|
}
|
|
7850
|
-
|
|
7851
|
-
// Cut from usageText() so a verb's help can never drift from the full listing.
|
|
8840
|
+
/** One verb's help blocks as `hippo <verb> --help` prints them, or null for a verb with none. */
|
|
7852
8841
|
export function verbUsage(verb) {
|
|
7853
|
-
const
|
|
7854
|
-
|
|
7855
|
-
let inBlock = false;
|
|
7856
|
-
for (const line of usageText().split('\n')) {
|
|
7857
|
-
if (/^ {2}\S/.test(line))
|
|
7858
|
-
inBlock = line.trimStart().split(' ', 1)[0] === name;
|
|
7859
|
-
else if (!line.startsWith(' '))
|
|
7860
|
-
inBlock = false;
|
|
7861
|
-
if (inBlock)
|
|
7862
|
-
block.push(line);
|
|
7863
|
-
}
|
|
7864
|
-
return block.length > 0 ? block.join('\n') : null;
|
|
8842
|
+
const usage = COMMAND_INDEX.get(verb)?.usage ?? [];
|
|
8843
|
+
return usage.length > 0 ? usage.join('').slice(1) : null;
|
|
7865
8844
|
}
|
|
7866
8845
|
// These sub-commands have fuller usage text than their lines in usageText().
|
|
7867
8846
|
const SUBCOMMAND_USAGE = new Map([
|
|
@@ -7948,837 +8927,13 @@ async function main(command, args, flags, hippoRoot) {
|
|
|
7948
8927
|
printError(refusal);
|
|
7949
8928
|
process.exit(2);
|
|
7950
8929
|
}
|
|
7951
|
-
|
|
7952
|
-
|
|
7953
|
-
|
|
7954
|
-
|
|
7955
|
-
|
|
7956
|
-
let text;
|
|
7957
|
-
if (args.length === 1 && args[0] === '-') {
|
|
7958
|
-
text = fs.readFileSync(0, 'utf-8').trim();
|
|
7959
|
-
}
|
|
7960
|
-
else {
|
|
7961
|
-
text = args.join(' ').trim();
|
|
7962
|
-
}
|
|
7963
|
-
if (!text || text.length < 3) {
|
|
7964
|
-
printError('Memory content too short (minimum 3 characters).');
|
|
7965
|
-
process.exit(1);
|
|
7966
|
-
}
|
|
7967
|
-
// Thin-client routing. When a server is up, simple `remember` calls go
|
|
7968
|
-
// over HTTP so the daemon stays single-writer (footgun #2). Rich CLI
|
|
7969
|
-
// flags (--pin, --layer, --extract, --global) still need the direct
|
|
7970
|
-
// path; we only intercept the minimal envelope. The salience gate is
|
|
7971
|
-
// NOT in richFlag and the route does not apply it, so a routed remember
|
|
7972
|
-
// stores what a direct one would skip. Measured 2026-09-07, tracked in
|
|
7973
|
-
// TODOS.md; do not read this list as covering salience.
|
|
7974
|
-
const richFlag = flags['pin'] || flags['global'] || flags['extract'] || flags['force'] ||
|
|
7975
|
-
flags['observed'] || flags['inferred'] || flags['verified'] ||
|
|
7976
|
-
flags['layer'] !== undefined;
|
|
7977
|
-
if (!richFlag) {
|
|
7978
|
-
const rememberKindRaw = typeof flags['kind'] === 'string' ? flags['kind'].toLowerCase() : undefined;
|
|
7979
|
-
const rememberKindAllowed = ['distilled', 'superseded'];
|
|
7980
|
-
if (rememberKindRaw === undefined || rememberKindAllowed.includes(rememberKindRaw)) {
|
|
7981
|
-
const tags = rememberTags(flags, process.cwd()).all;
|
|
7982
|
-
// B2 v1.12.6 — validate --owner on the thin-client path too.
|
|
7983
|
-
// Failure on this path exits early so the user gets the same
|
|
7984
|
-
// validation experience whether or not a server is up.
|
|
7985
|
-
const thinOwnerRaw = typeof flags['owner'] === 'string' ? flags['owner'] : undefined;
|
|
7986
|
-
const thinOwnerCheck = validateOwner(thinOwnerRaw, { strict: isStrictOwnerEnv() });
|
|
7987
|
-
if (!thinOwnerCheck.ok) {
|
|
7988
|
-
printError(thinOwnerCheck.message);
|
|
7989
|
-
process.exit(1);
|
|
7990
|
-
}
|
|
7991
|
-
if (thinOwnerCheck.message)
|
|
7992
|
-
printError(thinOwnerCheck.message);
|
|
7993
|
-
const remembered = await runViaServerIfAvailable(hippoRoot, async (info, apiKey) => {
|
|
7994
|
-
const result = await client.remember(info.url, apiKey, {
|
|
7995
|
-
content: text,
|
|
7996
|
-
kind: rememberKindRaw,
|
|
7997
|
-
scope: typeof flags['scope'] === 'string' ? flags['scope'] : undefined,
|
|
7998
|
-
owner: thinOwnerCheck.value,
|
|
7999
|
-
artifactRef: typeof flags['artifact-ref'] === 'string' ? flags['artifact-ref'] : undefined,
|
|
8000
|
-
tags,
|
|
8001
|
-
});
|
|
8002
|
-
console.log(`Remembered [${result.id}] (via ${info.url})`);
|
|
8003
|
-
console.log(` Kind: ${result.kind} | Tenant: ${result.tenantId}`);
|
|
8004
|
-
for (const w of result.warnings ?? [])
|
|
8005
|
-
printError(`Warning: ${w}`);
|
|
8006
|
-
});
|
|
8007
|
-
if (remembered)
|
|
8008
|
-
break;
|
|
8009
|
-
}
|
|
8010
|
-
}
|
|
8011
|
-
await cmdRemember(hippoRoot, text, flags);
|
|
8012
|
-
break;
|
|
8013
|
-
}
|
|
8014
|
-
case 'recall': {
|
|
8015
|
-
const query = args.join(' ').trim();
|
|
8016
|
-
if (!query) {
|
|
8017
|
-
printError('Please provide a search query.');
|
|
8018
|
-
process.exit(1);
|
|
8019
|
-
}
|
|
8020
|
-
await cmdRecall(hookStoreRoot(hippoRoot), query, flags);
|
|
8021
|
-
break;
|
|
8022
|
-
}
|
|
8023
|
-
case 'drill': {
|
|
8024
|
-
const summaryId = args[0];
|
|
8025
|
-
if (!summaryId) {
|
|
8026
|
-
printError('Usage: hippo drill <summary-id> [--limit N] [--budget N]');
|
|
8027
|
-
process.exit(1);
|
|
8028
|
-
}
|
|
8029
|
-
cmdDrillDown(hippoRoot, summaryId, flags);
|
|
8030
|
-
break;
|
|
8031
|
-
}
|
|
8032
|
-
case 'assemble': {
|
|
8033
|
-
const sessionId = typeof flags['session'] === 'string' ? flags['session'] : args[0];
|
|
8034
|
-
if (!sessionId) {
|
|
8035
|
-
printError('Usage: hippo assemble --session <id> [--budget N] [--fresh-tail N] [--no-summarize-older] [--json]');
|
|
8036
|
-
process.exit(1);
|
|
8037
|
-
}
|
|
8038
|
-
cmdAssemble(hippoRoot, sessionId, flags);
|
|
8039
|
-
break;
|
|
8040
|
-
}
|
|
8041
|
-
case 'supersede': {
|
|
8042
|
-
const oldId = args[0];
|
|
8043
|
-
const newContent = args.slice(1).join(' ').trim();
|
|
8044
|
-
if (!oldId || !newContent) {
|
|
8045
|
-
printError('Usage: hippo supersede <old-id> "<new content>" [--layer L] [--tag T] [--pin]');
|
|
8046
|
-
process.exit(1);
|
|
8047
|
-
}
|
|
8048
|
-
cmdSupersede(hippoRoot, oldId, newContent, flags);
|
|
8049
|
-
break;
|
|
8050
|
-
}
|
|
8051
|
-
case 'explain': {
|
|
8052
|
-
const query = args.join(' ').trim();
|
|
8053
|
-
if (!query) {
|
|
8054
|
-
printError('Please provide a search query.');
|
|
8055
|
-
process.exit(1);
|
|
8056
|
-
}
|
|
8057
|
-
await cmdExplain(hippoRoot, query, flags);
|
|
8058
|
-
break;
|
|
8059
|
-
}
|
|
8060
|
-
case 'eval': {
|
|
8061
|
-
const corpusPath = args[0] ? String(args[0]) : null;
|
|
8062
|
-
await cmdEval(hippoRoot, corpusPath, flags);
|
|
8063
|
-
break;
|
|
8064
|
-
}
|
|
8065
|
-
case 'trace': {
|
|
8066
|
-
const sub = args[0] ? String(args[0]) : '';
|
|
8067
|
-
if (sub === 'record') {
|
|
8068
|
-
cmdTraceRecord(hippoRoot, flags);
|
|
8069
|
-
break;
|
|
8070
|
-
}
|
|
8071
|
-
if (!sub) {
|
|
8072
|
-
printError('Usage: hippo trace <memory-id> | hippo trace record --task <t> --steps <json> --outcome <o>');
|
|
8073
|
-
process.exit(1);
|
|
8074
|
-
}
|
|
8075
|
-
cmdTrace(hippoRoot, sub, flags);
|
|
8076
|
-
break;
|
|
8077
|
-
}
|
|
8078
|
-
case 'refine':
|
|
8079
|
-
await cmdRefine(hippoRoot, flags);
|
|
8080
|
-
break;
|
|
8081
|
-
case 'sleep':
|
|
8082
|
-
await (await import('./cli/sleep.js')).cmdSleep(hippoRoot, flags);
|
|
8083
|
-
break;
|
|
8084
|
-
case 'last-sleep':
|
|
8085
|
-
cmdLastSleep(flags);
|
|
8086
|
-
break;
|
|
8087
|
-
case 'session-end':
|
|
8088
|
-
await cmdSessionEnd(hippoRoot, flags);
|
|
8089
|
-
break;
|
|
8090
|
-
case '__session-end-worker':
|
|
8091
|
-
await cmdSessionEndWorker(hippoRoot, flags);
|
|
8092
|
-
break;
|
|
8093
|
-
case 'pre-compact': {
|
|
8094
|
-
// Bounded wait, not a TTY guard: an idle non-TTY pipe must not hang.
|
|
8095
|
-
const { text: stdinText, timedOut: stdinTimedOut } = await readStdinBounded();
|
|
8096
|
-
await runHookWithStores(async () => {
|
|
8097
|
-
resetHookInjection(hippoRoot, stdinText, null);
|
|
8098
|
-
await cmdPreCompact(hookStoreRoot(hippoRoot), {
|
|
8099
|
-
stdinText,
|
|
8100
|
-
stdinTimedOut,
|
|
8101
|
-
logFile: typeof flags['log-file'] === 'string' ? flags['log-file'] : undefined,
|
|
8102
|
-
});
|
|
8103
|
-
});
|
|
8104
|
-
break;
|
|
8105
|
-
}
|
|
8106
|
-
case 'post-compact': {
|
|
8107
|
-
// PostCompact hook: saves the compaction summary and its memories, then prints one plain line, because Claude Code shows this hook's stdout as-is. Always exits 0.
|
|
8108
|
-
const { text } = await readStdinBounded();
|
|
8109
|
-
const logFlag = flags['log-file'];
|
|
8110
|
-
const store = hookStoreRoot(hippoRoot);
|
|
8111
|
-
const line = await runHookWithStores(() => cmdPostCompact(store, {
|
|
8112
|
-
stdinText: text,
|
|
8113
|
-
logFile: logFlag === true || logFlag === false || Array.isArray(logFlag) ? undefined : logFlag,
|
|
8114
|
-
// Passed in, since capture.ts importing the sync would close an import cycle.
|
|
8115
|
-
afterSave: (transcriptPath, originProject, log) => {
|
|
8116
|
-
const report = importAtCompaction(store, transcriptPath, originProject, { machine: currentMachine(), busyWaitMs: COMPACTION_DB_WAIT_MS });
|
|
8117
|
-
const summary = summaryLine(report);
|
|
8118
|
-
if (summary !== null)
|
|
8119
|
-
log(summary);
|
|
8120
|
-
for (const warning of report.warnings)
|
|
8121
|
-
log(`agent memories: ${warning}`);
|
|
8122
|
-
},
|
|
8123
|
-
}));
|
|
8124
|
-
if (line !== null && line !== undefined)
|
|
8125
|
-
console.log(line);
|
|
8126
|
-
break;
|
|
8127
|
-
}
|
|
8128
|
-
case 'capture-error': {
|
|
8129
|
-
// PostToolUseFailure hook: every path exits 0, and nothing is created
|
|
8130
|
-
// when no store exists (the hook fires in every directory).
|
|
8131
|
-
const { text } = await readStdinBounded();
|
|
8132
|
-
try {
|
|
8133
|
-
const root = hookStoreRoot(hippoRoot);
|
|
8134
|
-
const payload = (text ?? '').trim();
|
|
8135
|
-
if (isInitialized(root) && payload) {
|
|
8136
|
-
// SAFETY: JSON.parse returns a JSON value by definition.
|
|
8137
|
-
const failure = JSON.parse(payload);
|
|
8138
|
-
await runHookWithStores(() => captureToolFailure(root, resolveTenantId({}), failure));
|
|
8139
|
-
}
|
|
8140
|
-
}
|
|
8141
|
-
catch {
|
|
8142
|
-
// A malformed payload or store error must never fail the agent's tool call.
|
|
8143
|
-
}
|
|
8144
|
-
break;
|
|
8145
|
-
}
|
|
8146
|
-
case 'compact-resume': {
|
|
8147
|
-
const { text: stdinText, timedOut: stdinTimedOut } = await readStdinBounded();
|
|
8148
|
-
await runHookWithStores(() => {
|
|
8149
|
-
resetHookInjection(hippoRoot, stdinText, 'compact');
|
|
8150
|
-
cmdCompactResume(hookStoreRoot(hippoRoot), stdinText, stdinTimedOut);
|
|
8151
|
-
});
|
|
8152
|
-
break;
|
|
8153
|
-
}
|
|
8154
|
-
case 'codex-run':
|
|
8155
|
-
cmdCodexRun(hippoRoot, args);
|
|
8156
|
-
break;
|
|
8157
|
-
case '__codex-session-end-worker':
|
|
8158
|
-
await cmdCodexSessionEndWorker(hippoRoot, flags);
|
|
8159
|
-
break;
|
|
8160
|
-
case 'dedup':
|
|
8161
|
-
cmdDedup(hippoRoot, flags);
|
|
8162
|
-
break;
|
|
8163
|
-
case 'dag':
|
|
8164
|
-
cmdDag(hippoRoot, flags);
|
|
8165
|
-
break;
|
|
8166
|
-
case 'auth':
|
|
8167
|
-
cmdAuth(hippoRoot, args, flags);
|
|
8168
|
-
break;
|
|
8169
|
-
case 'goal':
|
|
8170
|
-
cmdGoal(hippoRoot, args, flags);
|
|
8171
|
-
break;
|
|
8172
|
-
case 'slack':
|
|
8173
|
-
cmdSlack(hippoRoot, args, flags);
|
|
8174
|
-
break;
|
|
8175
|
-
case 'github':
|
|
8176
|
-
await cmdGithub(hippoRoot, args, flags);
|
|
8177
|
-
break;
|
|
8178
|
-
case 'audit': {
|
|
8179
|
-
// `audit list` and `audit prune` -> A5 audit-log subcommands.
|
|
8180
|
-
// Other forms (no sub, --fix) keep the existing memory-quality auditor
|
|
8181
|
-
// for backwards compatibility.
|
|
8182
|
-
if (args[0] === 'list' || args[0] === 'prune') {
|
|
8183
|
-
cmdAuditLog(hippoRoot, args, flags);
|
|
8184
|
-
break;
|
|
8185
|
-
}
|
|
8186
|
-
requireInit(hippoRoot);
|
|
8187
|
-
const entries = loadAllEntries(hippoRoot, resolveTenantId({}));
|
|
8188
|
-
const result = auditMemories(entries, memoriesBackingObjects(hippoRoot));
|
|
8189
|
-
const shouldFix = Boolean(flags['fix']);
|
|
8190
|
-
if (result.issues.length === 0) {
|
|
8191
|
-
console.log(`All ${result.total} memories passed quality checks.`);
|
|
8192
|
-
}
|
|
8193
|
-
else {
|
|
8194
|
-
console.log(`Audited ${result.total} memories: ${result.clean} clean, ${result.issues.length} issues\n`);
|
|
8195
|
-
for (const issue of result.issues) {
|
|
8196
|
-
const icon = issue.severity === 'error' ? 'ERR' : 'WARN';
|
|
8197
|
-
console.log(` [${icon}] ${issue.memoryId}: ${issue.reason}`);
|
|
8198
|
-
console.log(` "${issue.content.slice(0, 80)}${issue.content.length > 80 ? '...' : ''}"`);
|
|
8199
|
-
}
|
|
8200
|
-
if (shouldFix) {
|
|
8201
|
-
const errors = result.issues.filter(i => i.severity === 'error');
|
|
8202
|
-
if (errors.length > 0 && flags['dry-run'] === true) {
|
|
8203
|
-
console.log(`\nWould remove ${errors.length} error-severity memories (dry run, nothing deleted).`);
|
|
8204
|
-
console.log(`${result.issues.length - errors.length} warnings would remain (review manually).`);
|
|
8205
|
-
}
|
|
8206
|
-
else if (errors.length > 0) {
|
|
8207
|
-
const removedCount = errors.filter((issue) => deleteEntry(hippoRoot, issue.memoryId, { reason: `audit --fix: ${issue.reason}`, automatic: true })).length;
|
|
8208
|
-
console.log(`\nRemoved ${removedCount} error-severity memories.`);
|
|
8209
|
-
console.log(`${result.issues.length - errors.length} warnings remain (review manually).`);
|
|
8210
|
-
}
|
|
8211
|
-
else {
|
|
8212
|
-
console.log(`\nNo error-severity issues. Warnings require manual review.`);
|
|
8213
|
-
}
|
|
8214
|
-
}
|
|
8215
|
-
else {
|
|
8216
|
-
console.log(`\nRun with --fix to auto-remove error-severity issues.`);
|
|
8217
|
-
}
|
|
8218
|
-
}
|
|
8219
|
-
break;
|
|
8220
|
-
}
|
|
8221
|
-
case 'correction-latency': {
|
|
8222
|
-
requireInit(hippoRoot);
|
|
8223
|
-
const entries = loadAllEntries(hippoRoot);
|
|
8224
|
-
const report = buildCorrectionLatency(entries);
|
|
8225
|
-
if (flags['json']) {
|
|
8226
|
-
console.log(JSON.stringify(report, null, 2));
|
|
8227
|
-
}
|
|
8228
|
-
else if (report.count === 0) {
|
|
8229
|
-
console.log('No supersessions found. Correction latency is undefined.');
|
|
8230
|
-
}
|
|
8231
|
-
else {
|
|
8232
|
-
const fmt = (ms) => {
|
|
8233
|
-
if (ms === null)
|
|
8234
|
-
return 'n/a';
|
|
8235
|
-
if (ms < 1000)
|
|
8236
|
-
return `${ms}ms`;
|
|
8237
|
-
if (ms < 60_000)
|
|
8238
|
-
return `${(ms / 1000).toFixed(1)}s`;
|
|
8239
|
-
if (ms < 3_600_000)
|
|
8240
|
-
return `${(ms / 60_000).toFixed(1)}m`;
|
|
8241
|
-
return `${(ms / 3_600_000).toFixed(1)}h`;
|
|
8242
|
-
};
|
|
8243
|
-
console.log(`Corrections: ${report.count} total (${report.extractionCount} extraction-driven, ${report.manualCount} manual)`);
|
|
8244
|
-
console.log(`Latency p50: ${fmt(report.p50Ms)}, p95: ${fmt(report.p95Ms)}, max: ${fmt(report.maxMs)}`);
|
|
8245
|
-
if (report.extractionCount === 0 && report.manualCount > 0) {
|
|
8246
|
-
console.log(`\nAll ${report.manualCount} corrections were manual supersedes: no measurable observation lag.`);
|
|
8247
|
-
console.log(`To measure latency, route corrections through extraction (set new.extracted_from to the raw receipt).`);
|
|
8248
|
-
}
|
|
8249
|
-
}
|
|
8250
|
-
break;
|
|
8251
|
-
}
|
|
8252
|
-
case 'provenance': {
|
|
8253
|
-
requireInit(hippoRoot);
|
|
8254
|
-
const entries = loadAllEntries(hippoRoot);
|
|
8255
|
-
const coverage = buildProvenanceCoverage(entries);
|
|
8256
|
-
if (flags['json']) {
|
|
8257
|
-
console.log(JSON.stringify(coverage, null, 2));
|
|
8258
|
-
}
|
|
8259
|
-
else if (coverage.rawTotal === 0) {
|
|
8260
|
-
console.log('No kind=raw memories present. Coverage gate trivially satisfied.');
|
|
8261
|
-
}
|
|
8262
|
-
else {
|
|
8263
|
-
const pct = (coverage.coverage * 100).toFixed(1);
|
|
8264
|
-
console.log(`Provenance coverage: ${coverage.rawWithEnvelope}/${coverage.rawTotal} raw rows envelope-complete (${pct}%)`);
|
|
8265
|
-
if (coverage.gaps.length > 0) {
|
|
8266
|
-
console.log(`\nGaps:`);
|
|
8267
|
-
for (const g of coverage.gaps) {
|
|
8268
|
-
console.log(` ${g.id}: missing ${g.missing.join(', ')}`);
|
|
8269
|
-
}
|
|
8270
|
-
}
|
|
8271
|
-
}
|
|
8272
|
-
if (flags['strict'] && coverage.coverage < 1) {
|
|
8273
|
-
process.exit(1);
|
|
8274
|
-
}
|
|
8275
|
-
break;
|
|
8276
|
-
}
|
|
8277
|
-
case 'status':
|
|
8278
|
-
cmdStatus(hippoRoot);
|
|
8279
|
-
break;
|
|
8280
|
-
case 'outcome':
|
|
8281
|
-
cmdOutcome(hippoRoot, flags);
|
|
8282
|
-
break;
|
|
8283
|
-
case 'conflicts':
|
|
8284
|
-
cmdConflicts(hippoRoot, flags);
|
|
8285
|
-
break;
|
|
8286
|
-
case 'resolve':
|
|
8287
|
-
cmdResolve(hippoRoot, args, flags);
|
|
8288
|
-
break;
|
|
8289
|
-
case 'reject':
|
|
8290
|
-
cmdReject(hippoRoot, args, flags);
|
|
8291
|
-
break;
|
|
8292
|
-
case 'rejections':
|
|
8293
|
-
cmdRejections(hippoRoot, flags);
|
|
8294
|
-
break;
|
|
8295
|
-
case 'unreject':
|
|
8296
|
-
cmdUnreject(hippoRoot, args, flags);
|
|
8297
|
-
break;
|
|
8298
|
-
case 'dormant':
|
|
8299
|
-
cmdDormant(hippoRoot, args, flags);
|
|
8300
|
-
break;
|
|
8301
|
-
case 'projects':
|
|
8302
|
-
(await import('./cli/projects.js')).cmdProjects(hippoRoot, args, flags);
|
|
8303
|
-
break;
|
|
8304
|
-
case 'quarantine':
|
|
8305
|
-
cmdQuarantine(hippoRoot, args, flags);
|
|
8306
|
-
break;
|
|
8307
|
-
case 'tokens':
|
|
8308
|
-
cmdTokens(hippoRoot, flags);
|
|
8309
|
-
break;
|
|
8310
|
-
case 'failures':
|
|
8311
|
-
cmdFailures(hippoRoot, flags);
|
|
8312
|
-
break;
|
|
8313
|
-
case 'doctor': {
|
|
8314
|
-
// SAFETY: package.json always carries a string "version" (checked at release by check-manifest-versions).
|
|
8315
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'package.json'), 'utf-8'));
|
|
8316
|
-
const report = runDoctor({ version: pkg.version });
|
|
8317
|
-
console.log(flags['json'] ? JSON.stringify(report, null, 2) : formatDoctor(report));
|
|
8318
|
-
if (!report.ok)
|
|
8319
|
-
process.exit(1);
|
|
8320
|
-
break;
|
|
8321
|
-
}
|
|
8322
|
-
case 'support-bundle': {
|
|
8323
|
-
const outFlag = cardStringFlag(flags, 'out');
|
|
8324
|
-
if (outFlag === '') {
|
|
8325
|
-
printError('--out requires a file path.');
|
|
8326
|
-
process.exit(1);
|
|
8327
|
-
}
|
|
8328
|
-
const includeLogs = flags['include-logs'] === true;
|
|
8329
|
-
const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
8330
|
-
const now = new Date();
|
|
8331
|
-
const bundle = buildSupportBundle({ cwd: process.cwd(), home, version: PACKAGE_VERSION, includeLogs, now });
|
|
8332
|
-
const stamp = now.toISOString().replace(/[:.]/g, '-');
|
|
8333
|
-
const file = outFlag ?? path.join(process.cwd(), `hippo-support-${stamp}.json`);
|
|
8334
|
-
const json = JSON.stringify(bundle, null, 2);
|
|
8335
|
-
try {
|
|
8336
|
-
fs.writeFileSync(file, `${json}\n`, { flag: 'wx', mode: 0o600 });
|
|
8337
|
-
}
|
|
8338
|
-
catch (err) {
|
|
8339
|
-
if (err instanceof Error && 'code' in err && err.code === 'EEXIST') {
|
|
8340
|
-
printError(`${file} already exists; pass --out to choose another file. Nothing was written.`);
|
|
8341
|
-
}
|
|
8342
|
-
else {
|
|
8343
|
-
printError(err instanceof Error ? err.message : String(err));
|
|
8344
|
-
}
|
|
8345
|
-
process.exit(1);
|
|
8346
|
-
}
|
|
8347
|
-
const kb = Math.round(Buffer.byteLength(json) / 1024);
|
|
8348
|
-
console.log(`Wrote ${file} (${kb} KB).`);
|
|
8349
|
-
console.log(includeLogs
|
|
8350
|
-
? `It holds versions, doctor checks, config with secrets removed, store counts, and the last ${TAIL_MAX_LINES} lines of each hippo log with known secret shapes removed. Those log lines can quote memory text. Read it before you attach it to a ticket.`
|
|
8351
|
-
: 'It holds versions, doctor checks, config with secrets removed, store counts and log file names. It never holds memory text. Read it before you attach it to a ticket.');
|
|
8352
|
-
break;
|
|
8353
|
-
}
|
|
8354
|
-
case 'snapshot':
|
|
8355
|
-
cmdSnapshot(hippoRoot, args, flags);
|
|
8356
|
-
break;
|
|
8357
|
-
case 'session':
|
|
8358
|
-
cmdSession(hippoRoot, args, flags);
|
|
8359
|
-
break;
|
|
8360
|
-
case 'handoff':
|
|
8361
|
-
cmdHandoff(hippoRoot, args, flags);
|
|
8362
|
-
break;
|
|
8363
|
-
case 'card':
|
|
8364
|
-
cmdCard(hippoRoot, args, flags);
|
|
8365
|
-
break;
|
|
8366
|
-
case 'predict':
|
|
8367
|
-
cmdPredict(hippoRoot, args, flags);
|
|
8368
|
-
break;
|
|
8369
|
-
case 'current':
|
|
8370
|
-
cmdCurrent(hippoRoot, args, flags);
|
|
8371
|
-
break;
|
|
8372
|
-
case 'forget': {
|
|
8373
|
-
const id = args[0];
|
|
8374
|
-
if (!id) {
|
|
8375
|
-
printError('Please provide a memory ID.');
|
|
8376
|
-
process.exit(1);
|
|
8377
|
-
}
|
|
8378
|
-
// Archive has its own HTTP route (POST /v1/memories/:id/archive); route
|
|
8379
|
-
// both branches the same way the direct path does.
|
|
8380
|
-
const archive = flags['archive'] === true;
|
|
8381
|
-
const reason = typeof flags['reason'] === 'string' ? flags['reason'] : null;
|
|
8382
|
-
if (archive && !reason) {
|
|
8383
|
-
printError(ARCHIVE_REASON_REQUIRED);
|
|
8384
|
-
process.exit(1);
|
|
8385
|
-
}
|
|
8386
|
-
if (flags['dry-run'] === true) {
|
|
8387
|
-
previewForget(hippoRoot, id, archive);
|
|
8388
|
-
break;
|
|
8389
|
-
}
|
|
8390
|
-
const routed = await runViaServerIfAvailable(hippoRoot, async (info, apiKey) => {
|
|
8391
|
-
try {
|
|
8392
|
-
if (archive) {
|
|
8393
|
-
await client.archiveRaw(info.url, apiKey, id, reason);
|
|
8394
|
-
console.log(`Archived ${id}`);
|
|
8395
|
-
}
|
|
8396
|
-
else {
|
|
8397
|
-
await client.forget(info.url, apiKey, id);
|
|
8398
|
-
console.log(`Forgot ${id}`);
|
|
8399
|
-
}
|
|
8400
|
-
}
|
|
8401
|
-
catch (err) {
|
|
8402
|
-
// A server that died after the health probe is the caller's transport
|
|
8403
|
-
// fallback to handle, not an error to report to the user.
|
|
8404
|
-
if (client.classifyTransportFailure(err) !== 'none')
|
|
8405
|
-
throw err;
|
|
8406
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
8407
|
-
printError(archive ? `Could not archive ${id}: ${msg}` : msg);
|
|
8408
|
-
process.exit(1);
|
|
8409
|
-
}
|
|
8410
|
-
});
|
|
8411
|
-
if (routed)
|
|
8412
|
-
break;
|
|
8413
|
-
cmdForget(hippoRoot, id, flags);
|
|
8414
|
-
break;
|
|
8415
|
-
}
|
|
8416
|
-
case 'inspect': {
|
|
8417
|
-
const id = args[0];
|
|
8418
|
-
if (!id) {
|
|
8419
|
-
printError('Please provide a memory ID.');
|
|
8420
|
-
process.exit(1);
|
|
8421
|
-
}
|
|
8422
|
-
cmdInspect(hippoRoot, id);
|
|
8423
|
-
break;
|
|
8424
|
-
}
|
|
8425
|
-
case 'context': {
|
|
8426
|
-
// Bounded, not a TTY guard (DF1 T2, docs/plans/2026-08-23-df1-snapshot-lifecycle.md):
|
|
8427
|
-
// the hot stdin path and a manual run share this one command.
|
|
8428
|
-
const { text: stdinText } = await readStdinBounded();
|
|
8429
|
-
await runHookWithStores(() => cmdContext(hookStoreRoot(hippoRoot), args, flags, stdinText));
|
|
8430
|
-
break;
|
|
8431
|
-
}
|
|
8432
|
-
case 'hook':
|
|
8433
|
-
cmdHook(args, flags);
|
|
8434
|
-
break;
|
|
8435
|
-
case 'setup':
|
|
8436
|
-
cmdSetup(flags);
|
|
8437
|
-
break;
|
|
8438
|
-
case 'daily-runner':
|
|
8439
|
-
cmdDailyRunner();
|
|
8440
|
-
break;
|
|
8441
|
-
case 'embed':
|
|
8442
|
-
await cmdEmbed(hippoRoot, flags);
|
|
8443
|
-
break;
|
|
8444
|
-
case 'watch': {
|
|
8445
|
-
const watchCmd = args.join(' ').trim();
|
|
8446
|
-
await cmdWatch(watchCmd, hippoRoot);
|
|
8447
|
-
break;
|
|
8448
|
-
}
|
|
8449
|
-
case 'learn':
|
|
8450
|
-
cmdLearn(hippoRoot, flags);
|
|
8451
|
-
break;
|
|
8452
|
-
case 'promote': {
|
|
8453
|
-
const id = args[0];
|
|
8454
|
-
if (!id) {
|
|
8455
|
-
printError('Please provide a memory ID.');
|
|
8456
|
-
process.exit(1);
|
|
8457
|
-
}
|
|
8458
|
-
const promoted = await runViaServerIfAvailable(hippoRoot, async (info, apiKey) => {
|
|
8459
|
-
try {
|
|
8460
|
-
const result = await client.promote(info.url, apiKey, id);
|
|
8461
|
-
console.log(`Promoted ${id} to global store as ${result.globalId}`);
|
|
8462
|
-
}
|
|
8463
|
-
catch (err) {
|
|
8464
|
-
printError(`Failed to promote: ${err.message}`);
|
|
8465
|
-
process.exit(1);
|
|
8466
|
-
}
|
|
8467
|
-
});
|
|
8468
|
-
if (promoted)
|
|
8469
|
-
break;
|
|
8470
|
-
cmdPromote(hippoRoot, id);
|
|
8471
|
-
break;
|
|
8472
|
-
}
|
|
8473
|
-
case 'sync':
|
|
8474
|
-
cmdSync(hippoRoot, flags);
|
|
8475
|
-
break;
|
|
8476
|
-
case 'share': {
|
|
8477
|
-
const shareId = args[0];
|
|
8478
|
-
if (shareId === '--auto' || flags['auto']) {
|
|
8479
|
-
// Auto-share mode
|
|
8480
|
-
requireInit(hippoRoot);
|
|
8481
|
-
const minScore = parseFloat(String(flags['min-score'] ?? '0.6'));
|
|
8482
|
-
const dryRun = Boolean(flags['dry-run']);
|
|
8483
|
-
const results = autoShare(hippoRoot, { minScore, dryRun, tenantId: resolveTenantId({}) });
|
|
8484
|
-
if (results.length === 0) {
|
|
8485
|
-
console.log('No memories meet the sharing threshold.');
|
|
8486
|
-
}
|
|
8487
|
-
else if (dryRun) {
|
|
8488
|
-
console.log(`Would share ${results.length} memories:\n`);
|
|
8489
|
-
for (const e of results) {
|
|
8490
|
-
const score = transferScore(e);
|
|
8491
|
-
console.log(` ${e.id} (transfer=${fmt(score)}) ${e.content.slice(0, 80)}...`);
|
|
8492
|
-
}
|
|
8493
|
-
}
|
|
8494
|
-
else {
|
|
8495
|
-
console.log(`Shared ${results.length} memories to global store.`);
|
|
8496
|
-
for (const e of results) {
|
|
8497
|
-
console.log(` ${e.id} <- ${e.source}`);
|
|
8498
|
-
}
|
|
8499
|
-
}
|
|
8500
|
-
}
|
|
8501
|
-
else if (shareId) {
|
|
8502
|
-
requireInit(hippoRoot);
|
|
8503
|
-
const force = Boolean(flags['force']);
|
|
8504
|
-
const tenantId = resolveTenantId({});
|
|
8505
|
-
const result = shareMemory(hippoRoot, shareId, { force, tenantId });
|
|
8506
|
-
if (result) {
|
|
8507
|
-
console.log(`Shared [${result.id}] to global store.`);
|
|
8508
|
-
console.log(` Source: ${result.source}`);
|
|
8509
|
-
}
|
|
8510
|
-
else {
|
|
8511
|
-
const entry = readEntry(hippoRoot, shareId, tenantId);
|
|
8512
|
-
if (entry) {
|
|
8513
|
-
const score = transferScore(entry);
|
|
8514
|
-
console.log(`Transfer score too low (${fmt(score)}). This memory looks project-specific.`);
|
|
8515
|
-
console.log('Use --force to share anyway.');
|
|
8516
|
-
}
|
|
8517
|
-
else {
|
|
8518
|
-
printError(`Memory not found: ${shareId}`);
|
|
8519
|
-
process.exit(1);
|
|
8520
|
-
}
|
|
8521
|
-
}
|
|
8522
|
-
}
|
|
8523
|
-
else {
|
|
8524
|
-
printError('Usage: hippo share <memory_id> [--force] or hippo share --auto [--dry-run]');
|
|
8525
|
-
process.exit(1);
|
|
8526
|
-
}
|
|
8527
|
-
break;
|
|
8528
|
-
}
|
|
8529
|
-
case 'peers': {
|
|
8530
|
-
// D4 v1.12.10: tenant-scoped by default. --all-tenants restores the
|
|
8531
|
-
// pre-D4 host-wide view for the rare operator who genuinely wants
|
|
8532
|
-
// cross-tenant peer discovery.
|
|
8533
|
-
const allTenants = flags['all-tenants'] === true;
|
|
8534
|
-
const tenantScope = allTenants ? undefined : resolveTenantId({});
|
|
8535
|
-
const peers = listPeers(undefined, tenantScope);
|
|
8536
|
-
if (peers.length === 0) {
|
|
8537
|
-
console.log('No peers found. Share memories with: hippo share <id>');
|
|
8538
|
-
}
|
|
8539
|
-
else {
|
|
8540
|
-
const scopeLabel = allTenants ? 'global store (all tenants)' : `global store (tenant "${tenantScope}")`;
|
|
8541
|
-
console.log(`${peers.length} project${peers.length === 1 ? '' : 's'} contributing to ${scopeLabel}:\n`);
|
|
8542
|
-
for (const p of peers) {
|
|
8543
|
-
console.log(` ${p.project.padEnd(25)} ${String(p.count).padStart(4)} memories (latest: ${p.latest.slice(0, 10)})`);
|
|
8544
|
-
}
|
|
8545
|
-
}
|
|
8546
|
-
break;
|
|
8547
|
-
}
|
|
8548
|
-
case 'import':
|
|
8549
|
-
cmdImport(hippoRoot, args, flags);
|
|
8550
|
-
break;
|
|
8551
|
-
case 'export': {
|
|
8552
|
-
requireInit(hippoRoot);
|
|
8553
|
-
const format = flags['format'] || 'json';
|
|
8554
|
-
const outputPath = args[0] || null;
|
|
8555
|
-
const entries = loadAllEntries(hippoRoot, resolveTenantId({}));
|
|
8556
|
-
let output;
|
|
8557
|
-
if (format === 'markdown' || format === 'md') {
|
|
8558
|
-
output = entries.map(e => {
|
|
8559
|
-
const meta = [
|
|
8560
|
-
`id: ${e.id}`,
|
|
8561
|
-
`created: ${e.created}`,
|
|
8562
|
-
`tags: ${e.tags.join(', ')}`,
|
|
8563
|
-
`confidence: ${e.confidence}`,
|
|
8564
|
-
`half_life: ${e.half_life_days}d`,
|
|
8565
|
-
`strength: ${e.strength.toFixed(2)}`,
|
|
8566
|
-
].join(' | ');
|
|
8567
|
-
return `### ${e.id}\n\n${e.content}\n\n_${meta}_`;
|
|
8568
|
-
}).join('\n\n---\n\n');
|
|
8569
|
-
}
|
|
8570
|
-
else {
|
|
8571
|
-
output = JSON.stringify(entries, null, 2);
|
|
8572
|
-
}
|
|
8573
|
-
if (outputPath) {
|
|
8574
|
-
fs.writeFileSync(outputPath, output, 'utf8');
|
|
8575
|
-
console.log(`Exported ${entries.length} memories to ${outputPath}`);
|
|
8576
|
-
}
|
|
8577
|
-
else {
|
|
8578
|
-
console.log(output);
|
|
8579
|
-
}
|
|
8580
|
-
break;
|
|
8581
|
-
}
|
|
8582
|
-
case 'capture': {
|
|
8583
|
-
let captureSource = null;
|
|
8584
|
-
let captureFile;
|
|
8585
|
-
let transcriptPath;
|
|
8586
|
-
if (flags['stdin']) {
|
|
8587
|
-
captureSource = 'stdin';
|
|
8588
|
-
}
|
|
8589
|
-
else if (flags['file']) {
|
|
8590
|
-
captureSource = 'file';
|
|
8591
|
-
captureFile = String(flags['file']);
|
|
8592
|
-
}
|
|
8593
|
-
else if (flags['last-session']) {
|
|
8594
|
-
captureSource = 'last-session';
|
|
8595
|
-
}
|
|
8596
|
-
if (flags['transcript']) {
|
|
8597
|
-
transcriptPath = String(flags['transcript']);
|
|
8598
|
-
if (!captureSource)
|
|
8599
|
-
captureSource = 'last-session';
|
|
8600
|
-
}
|
|
8601
|
-
if (!captureSource) {
|
|
8602
|
-
printError('Usage: hippo capture --stdin|--file <path>|--last-session [--transcript <path>] [--log-file <path>] [--dry-run] [--global]');
|
|
8603
|
-
process.exit(1);
|
|
8604
|
-
}
|
|
8605
|
-
// Bounded, and only when last-session has no explicit path: the
|
|
8606
|
-
// --stdin source keeps its own blocking read in capture.ts by design.
|
|
8607
|
-
const bounded = captureSource === 'last-session' && !transcriptPath
|
|
8608
|
-
? await readStdinBounded()
|
|
8609
|
-
: { text: undefined, timedOut: false };
|
|
8610
|
-
cmdCapture(hippoRoot, {
|
|
8611
|
-
source: captureSource,
|
|
8612
|
-
filePath: captureFile,
|
|
8613
|
-
transcriptPath,
|
|
8614
|
-
stdinText: bounded.text,
|
|
8615
|
-
stdinTimedOut: bounded.timedOut,
|
|
8616
|
-
logFile: typeof flags['log-file'] === 'string' ? flags['log-file'] : undefined,
|
|
8617
|
-
dryRun: Boolean(flags['dry-run']),
|
|
8618
|
-
global: Boolean(flags['global']),
|
|
8619
|
-
tenantId: resolveTenantId({}),
|
|
8620
|
-
});
|
|
8621
|
-
break;
|
|
8622
|
-
}
|
|
8623
|
-
case 'dashboard': {
|
|
8624
|
-
requireInit(hippoRoot);
|
|
8625
|
-
const port = parseInt(String(flags['port'] ?? '3333'), 10);
|
|
8626
|
-
const { serveDashboard } = await import('./dashboard.js');
|
|
8627
|
-
serveDashboard(hippoRoot, port);
|
|
8628
|
-
await new Promise(() => { }); // run until Ctrl+C
|
|
8629
|
-
break;
|
|
8630
|
-
}
|
|
8631
|
-
case 'wm':
|
|
8632
|
-
cmdWm(hippoRoot, args, flags);
|
|
8633
|
-
break;
|
|
8634
|
-
case 'mcp': {
|
|
8635
|
-
// Start MCP server over stdio. Dynamic import keeps main CLI lean; the
|
|
8636
|
-
// dispatcher itself is transport-agnostic, so we explicitly attach the
|
|
8637
|
-
// stdio loop here. (HTTP/SSE transport is wired in src/server.ts and
|
|
8638
|
-
// imports the same module without triggering stdin handlers.)
|
|
8639
|
-
const mod = await import('./mcp/server.js');
|
|
8640
|
-
mod.startStdioLoop();
|
|
8641
|
-
// Server runs until stdin closes, so we never reach here
|
|
8642
|
-
await new Promise(() => { }); // hang forever
|
|
8643
|
-
break;
|
|
8644
|
-
}
|
|
8645
|
-
case 'serve': {
|
|
8646
|
-
requireInit(hippoRoot);
|
|
8647
|
-
const portRaw = flags['port'] ?? process.env['HIPPO_PORT'] ?? '6789';
|
|
8648
|
-
const port = Number(portRaw);
|
|
8649
|
-
if (!Number.isFinite(port) || port < 0) {
|
|
8650
|
-
printError(`Invalid --port: ${String(portRaw)}`);
|
|
8651
|
-
process.exit(1);
|
|
8652
|
-
}
|
|
8653
|
-
const host = typeof flags['host'] === 'string' ? flags['host'] : '127.0.0.1';
|
|
8654
|
-
const { serve } = await import('./server.js');
|
|
8655
|
-
const handle = await serve({ hippoRoot, port, host, handleSignals: true });
|
|
8656
|
-
console.log(`hippo serve listening on ${handle.url} (pid ${process.pid})`);
|
|
8657
|
-
console.log(`pidfile: ${path.join(hippoRoot, 'server.pid')}`);
|
|
8658
|
-
console.log('press Ctrl+C to stop');
|
|
8659
|
-
// The SIGINT/SIGTERM handlers stop the server and exit. Hang until then.
|
|
8660
|
-
await new Promise(() => { });
|
|
8661
|
-
break;
|
|
8662
|
-
}
|
|
8663
|
-
case 'invalidate': {
|
|
8664
|
-
requireInit(hippoRoot);
|
|
8665
|
-
if (flags['churn'] === true) {
|
|
8666
|
-
if (args[0] || flags['id'] !== undefined) {
|
|
8667
|
-
printError('Usage: hippo invalidate --churn [--dry-run]');
|
|
8668
|
-
printError('--churn takes no pattern or --id.');
|
|
8669
|
-
process.exit(1);
|
|
8670
|
-
}
|
|
8671
|
-
if (!isGitRepo(process.cwd())) {
|
|
8672
|
-
printError('hippo invalidate --churn must run inside a git repository.');
|
|
8673
|
-
process.exit(1);
|
|
8674
|
-
}
|
|
8675
|
-
const churnDryRun = flags['dry-run'] === true;
|
|
8676
|
-
let churnFailed = false;
|
|
8677
|
-
for (const { root, result } of runChurnStaleForRepo(hippoRoot, churnDryRun)) {
|
|
8678
|
-
if (result.error) {
|
|
8679
|
-
printError(`Churn-staleness check failed for ${root}: ${result.error}`);
|
|
8680
|
-
churnFailed = true;
|
|
8681
|
-
continue;
|
|
8682
|
-
}
|
|
8683
|
-
if (result.preview.length === 0) {
|
|
8684
|
-
console.log(`No churn-stale candidates in ${root}.`);
|
|
8685
|
-
}
|
|
8686
|
-
else if (churnDryRun) {
|
|
8687
|
-
console.log(`DRY RUN - ${result.marked} memories in ${root} WOULD be tagged churn-stale (${result.alreadyMarked} already tagged):`);
|
|
8688
|
-
}
|
|
8689
|
-
else {
|
|
8690
|
-
console.log(`Tagged ${result.marked} memories churn-stale in ${root} (${result.alreadyMarked} already tagged):`);
|
|
8691
|
-
}
|
|
8692
|
-
result.preview.forEach(p => console.log(` ${p.id} ${p.evidence} ${p.already ? '(already) ' : ''}${p.headline}`));
|
|
8693
|
-
if (result.skippedPinned.length > 0) {
|
|
8694
|
-
console.log(`Skipped ${result.skippedPinned.length} pinned: ${result.skippedPinned.join(', ')}`);
|
|
8695
|
-
}
|
|
8696
|
-
}
|
|
8697
|
-
if (churnFailed)
|
|
8698
|
-
process.exit(1);
|
|
8699
|
-
break;
|
|
8700
|
-
}
|
|
8701
|
-
const target = args[0];
|
|
8702
|
-
if (flags['id'] === true) {
|
|
8703
|
-
// Value-less --id must never silently fall through to pattern mode
|
|
8704
|
-
// (pattern mode writes broadly; an ignored --id reverses user intent).
|
|
8705
|
-
printError('--id requires a memory id');
|
|
8706
|
-
process.exit(1);
|
|
8707
|
-
}
|
|
8708
|
-
const onlyId = typeof flags['id'] === 'string' ? flags['id'] : undefined;
|
|
8709
|
-
if (typeof flags['dry-run'] === 'string') {
|
|
8710
|
-
// Dead: the earlier global BOOLEAN_FLAGS guard now exits first on any --dry-run=<v>.
|
|
8711
|
-
// Kept as defence in depth on a destructive command.
|
|
8712
|
-
printError('--dry-run takes no value');
|
|
8713
|
-
process.exit(1);
|
|
8714
|
-
}
|
|
8715
|
-
const dryRun = flags['dry-run'] === true;
|
|
8716
|
-
if ((target && onlyId) || (!target && !onlyId)) {
|
|
8717
|
-
printError('Usage: hippo invalidate "<old pattern>" [--dry-run] [--reason "<why>"]');
|
|
8718
|
-
printError(' hippo invalidate --id <memory-id> [--dry-run] [--reason "<why>"]');
|
|
8719
|
-
printError('Pass a pattern OR --id, not both. Tag matching is EXACT: the full pattern must equal a tag.');
|
|
8720
|
-
process.exit(1);
|
|
8721
|
-
}
|
|
8722
|
-
const reason = flags['reason'] || null;
|
|
8723
|
-
const invTarget = {
|
|
8724
|
-
from: target ?? `id:${onlyId}`,
|
|
8725
|
-
to: reason,
|
|
8726
|
-
type: 'migration',
|
|
8727
|
-
};
|
|
8728
|
-
const result = invalidateMatching(hippoRoot, invTarget, resolveTenantId({}), { dryRun, onlyId });
|
|
8729
|
-
const label = target ? `"${target}"` : `--id ${onlyId}`;
|
|
8730
|
-
if (result.dryRun) {
|
|
8731
|
-
if (result.invalidated === 0) {
|
|
8732
|
-
console.log(`DRY RUN - no memories would match ${label}.`);
|
|
8733
|
-
}
|
|
8734
|
-
else {
|
|
8735
|
-
console.log(`DRY RUN - ${result.invalidated} memories WOULD be invalidated:`);
|
|
8736
|
-
result.preview.forEach(p => console.log(` ${p.id} ${p.headline}`));
|
|
8737
|
-
}
|
|
8738
|
-
}
|
|
8739
|
-
else if (result.invalidated === 0) {
|
|
8740
|
-
console.log(`No memories matched ${label}.`);
|
|
8741
|
-
}
|
|
8742
|
-
else {
|
|
8743
|
-
console.log(`Invalidated ${result.invalidated} memories referencing ${label}.`);
|
|
8744
|
-
result.targets.forEach(id => console.log(` ${id}`));
|
|
8745
|
-
}
|
|
8746
|
-
if (result.skippedPinned.length > 0) {
|
|
8747
|
-
console.log(`Skipped ${result.skippedPinned.length} pinned: ${result.skippedPinned.join(', ')}`);
|
|
8748
|
-
}
|
|
8749
|
-
break;
|
|
8750
|
-
}
|
|
8751
|
-
case 'decide':
|
|
8752
|
-
cmdDecide(hippoRoot, args, flags);
|
|
8753
|
-
break;
|
|
8754
|
-
case 'incident':
|
|
8755
|
-
cmdIncident(hippoRoot, args, flags);
|
|
8756
|
-
break;
|
|
8757
|
-
case 'process':
|
|
8758
|
-
cmdProcess(hippoRoot, args, flags);
|
|
8759
|
-
break;
|
|
8760
|
-
case 'policy':
|
|
8761
|
-
cmdPolicy(hippoRoot, args, flags);
|
|
8762
|
-
break;
|
|
8763
|
-
case 'skill':
|
|
8764
|
-
cmdSkill(hippoRoot, args, flags);
|
|
8765
|
-
break;
|
|
8766
|
-
case 'brief':
|
|
8767
|
-
case 'project-brief':
|
|
8768
|
-
cmdProjectBrief(hippoRoot, args, flags);
|
|
8769
|
-
break;
|
|
8770
|
-
case 'note':
|
|
8771
|
-
case 'customer-note':
|
|
8772
|
-
cmdCustomerNote(hippoRoot, args, flags);
|
|
8773
|
-
break;
|
|
8774
|
-
case 'graph':
|
|
8775
|
-
cmdGraph(hippoRoot, args, flags);
|
|
8776
|
-
break;
|
|
8777
|
-
default:
|
|
8778
|
-
printError(`Unknown command: ${command}`);
|
|
8779
|
-
printUsage();
|
|
8780
|
-
process.exit(1);
|
|
8930
|
+
const spec = COMMAND_INDEX.get(command);
|
|
8931
|
+
if (!spec) {
|
|
8932
|
+
printError(`Unknown command: ${command}`);
|
|
8933
|
+
printUsage();
|
|
8934
|
+
process.exit(1);
|
|
8781
8935
|
}
|
|
8936
|
+
await spec.run({ hippoRoot, args, flags });
|
|
8782
8937
|
}
|
|
8783
8938
|
export async function runCli(argv = process.argv) {
|
|
8784
8939
|
const { command, args, flags } = parseArgs(argv);
|