hippo-memory 1.47.0 → 1.48.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/api.js +4 -4
- package/dist/cli.js +40 -2
- package/dist/db.d.ts +7 -0
- package/dist/db.js +37 -1
- package/dist/doctor.js +52 -41
- package/dist/reject-flow.d.ts +2 -2
- package/dist/reject-flow.js +4 -7
- package/dist/secret-detect.d.ts +2 -0
- package/dist/secret-detect.js +18 -6
- package/dist/store.d.ts +9 -26
- package/dist/store.js +18 -36
- package/dist/support-bundle.d.ts +12 -0
- package/dist/support-bundle.js +292 -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/README.md
CHANGED
|
@@ -256,7 +256,7 @@ hippo recall "data pipeline" --why --limit 5
|
|
|
256
256
|
|
|
257
257
|
Input enters the buffer. Important things get encoded into episodic memory. During "sleep," repeated episodes compress into semantic patterns. Weak memories decay and disappear.
|
|
258
258
|
|
|
259
|
-
The store is SQLite (`.hippo/hippo.db`). The markdown files
|
|
259
|
+
The store is SQLite (`.hippo/hippo.db`). The markdown files are mirrors written after each change. `index.json` is no longer refreshed by writes, deletes or recalls: it is written only when you call `rebuildIndex()` from the package, so a copy an older version left on disk goes stale. Read the store through the CLI, the MCP server or the HTTP API.
|
|
260
260
|
|
|
261
261
|
```mermaid
|
|
262
262
|
flowchart TD
|
|
@@ -629,6 +629,7 @@ hippo watch "npm run build"
|
|
|
629
629
|
| `hippo dormant restore <id>` | Bring a dormant memory back to active memory |
|
|
630
630
|
| `hippo dormant forget <id>` | Delete a dormant memory permanently |
|
|
631
631
|
| `hippo doctor [--json]` | Check the install: Node, store, schema, sleep, agent hooks; each problem names its fix |
|
|
632
|
+
| `hippo support-bundle [--out <file>] [--include-logs]` | Write a redacted JSON file for a support ticket: versions, doctor checks, config, store counts and log names, never memory text; `--include-logs` adds each log's last 200 lines, which can quote it |
|
|
632
633
|
| `hippo tokens [--days n]` | Estimated tokens of memory text handed to agents, per surface, and what skipping unchanged hook blocks saved |
|
|
633
634
|
| `hippo failures [--days n]` | Failed tool calls the capture-error hook saw, by outcome, and how many errors first happened in another session |
|
|
634
635
|
| `hippo embed` | Embed all memories for semantic search |
|
package/dist/api.js
CHANGED
|
@@ -1216,11 +1216,11 @@ export function supersede(ctx, oldId, newContent) {
|
|
|
1216
1216
|
}
|
|
1217
1217
|
// Mirrors after COMMIT, while the db handle is still open. Same
|
|
1218
1218
|
// invariant as the original writeEntry: a mirror failure leaves disk
|
|
1219
|
-
// MISSING the markdown for the new memory (
|
|
1220
|
-
//
|
|
1219
|
+
// MISSING the markdown for the new memory (rebuildIndex rewrites every
|
|
1220
|
+
// markdown mirror from the DB) but DOES NOT desync the DB or
|
|
1221
1221
|
// roll back the supersede. Logged + swallowed, non-fatal.
|
|
1222
1222
|
try {
|
|
1223
|
-
writeEntryMirrors(ctx.hippoRoot,
|
|
1223
|
+
writeEntryMirrors(ctx.hippoRoot, newEntry);
|
|
1224
1224
|
}
|
|
1225
1225
|
catch (mirrorErr) {
|
|
1226
1226
|
console.error('supersede: mirror write failed (non-fatal, will self-heal):', mirrorErr);
|
|
@@ -1998,7 +1998,7 @@ export function restoreDormant(ctx, id) {
|
|
|
1998
1998
|
}
|
|
1999
1999
|
throw err;
|
|
2000
2000
|
}
|
|
2001
|
-
writeEntryMirrors(ctx.hippoRoot,
|
|
2001
|
+
writeEntryMirrors(ctx.hippoRoot, restored);
|
|
2002
2002
|
return restored;
|
|
2003
2003
|
}
|
|
2004
2004
|
finally {
|
package/dist/cli.js
CHANGED
|
@@ -60,6 +60,8 @@ import { computeSystemEnergy, vecNorm } from './physics.js';
|
|
|
60
60
|
import { loadConfig } from './config.js';
|
|
61
61
|
import { openHippoDb, closeHippoDb } from './db.js';
|
|
62
62
|
import { runDoctor, formatDoctor } from './doctor.js';
|
|
63
|
+
import { buildSupportBundle, TAIL_MAX_LINES } from './support-bundle.js';
|
|
64
|
+
import { PACKAGE_VERSION } from './version.js';
|
|
63
65
|
import { captureToolFailure } from './capture-error.js';
|
|
64
66
|
import { blockHash, hookPayloadSessionId, lastSentState, recordTokenUse, shouldSkipUnchanged } from './token-ledger.js';
|
|
65
67
|
import { FAILURE_LOG_RETENTION_DAYS } from './failure-log.js';
|
|
@@ -264,7 +266,7 @@ export const BOOLEAN_FLAGS = new Set([
|
|
|
264
266
|
'all', 'all-tenants', 'archive', 'auto', 'bad', 'bootstrap', 'classic', 'continuity',
|
|
265
267
|
'cross-project', 'dry-run', 'equal-sources', 'error', 'evc-adaptive', 'extract',
|
|
266
268
|
'filter-conflicts', 'fix', 'force', 'forget', 'git', 'global', 'good', 'graph-stream',
|
|
267
|
-
'help', 'include-superseded', 'inferred', 'json', 'last-session', 'multihop', 'no-hooks',
|
|
269
|
+
'help', 'include-logs', 'include-superseded', 'inferred', 'json', 'last-session', 'multihop', 'no-hooks',
|
|
268
270
|
'no-learn', 'no-mmr', 'no-propagate', 'no-schedule', 'no-share', 'no-summarize-older',
|
|
269
271
|
'observed', 'open', 'physics', 'pin', 'pinned-only', 'reject-loser', 'rerank-utility',
|
|
270
272
|
'reset-physics', 'save-baseline', 'show-cases', 'stats', 'stdin',
|
|
@@ -488,7 +490,7 @@ function cmdInit(hippoRoot, flags) {
|
|
|
488
490
|
initStore(hippoRoot);
|
|
489
491
|
console.log('Initialized Hippo at', hippoRoot);
|
|
490
492
|
console.log(' Directories: buffer/ episodic/ semantic/ conflicts/');
|
|
491
|
-
console.log(' Files: hippo.db
|
|
493
|
+
console.log(' Files: hippo.db stats.json');
|
|
492
494
|
}
|
|
493
495
|
const globalRoot = getGlobalRoot();
|
|
494
496
|
registerWorkspace(globalRoot, path.dirname(hippoRoot));
|
|
@@ -8675,6 +8677,10 @@ Commands:
|
|
|
8675
8677
|
PostToolUseFailure hook payload on stdin; skips routine failures)
|
|
8676
8678
|
doctor Check the install: Node, store, schema, sleep, agent hooks
|
|
8677
8679
|
--json Machine-readable report (exit code 1 on any failure)
|
|
8680
|
+
support-bundle Write a redacted JSON file for a support ticket: versions, doctor,
|
|
8681
|
+
config without secrets, store counts, log names; never memory text
|
|
8682
|
+
--out <file> Where to write it (default: hippo-support-<time>.json here)
|
|
8683
|
+
--include-logs Add the last ${TAIL_MAX_LINES} lines of each hippo log, known secret shapes removed
|
|
8678
8684
|
tokens Tokens of memory text hippo handed agents, per surface
|
|
8679
8685
|
(hook, context, recall, MCP, HTTP), and what skipping
|
|
8680
8686
|
unchanged hook blocks saved
|
|
@@ -9433,6 +9439,38 @@ async function main(command, args, flags, hippoRoot) {
|
|
|
9433
9439
|
process.exit(1);
|
|
9434
9440
|
break;
|
|
9435
9441
|
}
|
|
9442
|
+
case 'support-bundle': {
|
|
9443
|
+
const outFlag = cardStringFlag(flags, 'out');
|
|
9444
|
+
if (outFlag === '') {
|
|
9445
|
+
console.error('--out requires a file path.');
|
|
9446
|
+
process.exit(1);
|
|
9447
|
+
}
|
|
9448
|
+
const includeLogs = flags['include-logs'] === true;
|
|
9449
|
+
const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
9450
|
+
const now = new Date();
|
|
9451
|
+
const bundle = buildSupportBundle({ cwd: process.cwd(), home, version: PACKAGE_VERSION, includeLogs, now });
|
|
9452
|
+
const stamp = now.toISOString().replace(/[:.]/g, '-');
|
|
9453
|
+
const file = outFlag ?? path.join(process.cwd(), `hippo-support-${stamp}.json`);
|
|
9454
|
+
const json = JSON.stringify(bundle, null, 2);
|
|
9455
|
+
try {
|
|
9456
|
+
fs.writeFileSync(file, `${json}\n`, { flag: 'wx', mode: 0o600 });
|
|
9457
|
+
}
|
|
9458
|
+
catch (err) {
|
|
9459
|
+
if (err instanceof Error && 'code' in err && err.code === 'EEXIST') {
|
|
9460
|
+
console.error(`${file} already exists; pass --out to choose another file. Nothing was written.`);
|
|
9461
|
+
}
|
|
9462
|
+
else {
|
|
9463
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
9464
|
+
}
|
|
9465
|
+
process.exit(1);
|
|
9466
|
+
}
|
|
9467
|
+
const kb = Math.round(Buffer.byteLength(json) / 1024);
|
|
9468
|
+
console.log(`Wrote ${file} (${kb} KB).`);
|
|
9469
|
+
console.log(includeLogs
|
|
9470
|
+
? `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.`
|
|
9471
|
+
: '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.');
|
|
9472
|
+
break;
|
|
9473
|
+
}
|
|
9436
9474
|
case 'snapshot':
|
|
9437
9475
|
cmdSnapshot(hippoRoot, args, flags);
|
|
9438
9476
|
break;
|
package/dist/db.d.ts
CHANGED
|
@@ -13,11 +13,18 @@ export interface DatabaseSyncLike {
|
|
|
13
13
|
}
|
|
14
14
|
export declare function getHippoDbPath(hippoRoot: string): string;
|
|
15
15
|
export declare function getCurrentSchemaVersion(): number;
|
|
16
|
+
/** Thrown by {@link assertBinaryCompatible}; doctor uses it to pick the upgrade fix over a generic permissions fix. */
|
|
17
|
+
export declare class IncompatibleBinaryError extends Error {
|
|
18
|
+
}
|
|
16
19
|
export declare function openHippoDb(hippoRoot: string): DatabaseSyncLike;
|
|
20
|
+
/** Open an existing store without changing it: no mkdir, WAL switch, migration or mirror cleanup. Throws when hippo.db is missing. */
|
|
21
|
+
export declare function openHippoDbReadOnly(hippoRoot: string): DatabaseSyncLike;
|
|
17
22
|
export declare function getSchemaVersion(db: DatabaseSyncLike): number;
|
|
18
23
|
export declare function closeHippoDb(db: DatabaseSyncLike): void;
|
|
19
24
|
export declare function getMeta(db: DatabaseSyncLike, key: string, fallback?: string): string;
|
|
20
25
|
export declare function setMeta(db: DatabaseSyncLike, key: string, value: string): void;
|
|
26
|
+
/** Row count of one table; null when the table is missing or unreadable, which callers show as unknown. */
|
|
27
|
+
export declare function countTableRows(db: DatabaseSyncLike, table: string): number | null;
|
|
21
28
|
export declare function isFtsAvailable(db: DatabaseSyncLike): boolean;
|
|
22
29
|
export declare function pruneConsolidationRuns(db: DatabaseSyncLike, keep?: number): void;
|
|
23
30
|
export {};
|
package/dist/db.js
CHANGED
|
@@ -2469,11 +2469,14 @@ export function getHippoDbPath(hippoRoot) {
|
|
|
2469
2469
|
export function getCurrentSchemaVersion() {
|
|
2470
2470
|
return CURRENT_SCHEMA_VERSION;
|
|
2471
2471
|
}
|
|
2472
|
+
/** Thrown by {@link assertBinaryCompatible}; doctor uses it to pick the upgrade fix over a generic permissions fix. */
|
|
2473
|
+
export class IncompatibleBinaryError extends Error {
|
|
2474
|
+
}
|
|
2472
2475
|
/** Refuse a store stamped for a newer binary. Fails closed: runMigrations creates meta first, so a failed read is a real error. */
|
|
2473
2476
|
function assertBinaryCompatible(db) {
|
|
2474
2477
|
const minRequired = getMeta(db, 'min_compatible_binary');
|
|
2475
2478
|
if (minRequired && compareSemver(minRequired, PACKAGE_VERSION) > 0) {
|
|
2476
|
-
throw new
|
|
2479
|
+
throw new IncompatibleBinaryError(`hippo-memory: this database requires hippo-memory >= ${minRequired}, but the running binary is ${PACKAGE_VERSION}. ` +
|
|
2477
2480
|
`Upgrade hippo-memory to open it; an older binary does not know this schema and could expose private rows or damage the store.`);
|
|
2478
2481
|
}
|
|
2479
2482
|
}
|
|
@@ -2530,6 +2533,25 @@ export function openHippoDb(hippoRoot) {
|
|
|
2530
2533
|
throw error;
|
|
2531
2534
|
}
|
|
2532
2535
|
}
|
|
2536
|
+
/** Open an existing store without changing it: no mkdir, WAL switch, migration or mirror cleanup. Throws when hippo.db is missing. */
|
|
2537
|
+
export function openHippoDbReadOnly(hippoRoot) {
|
|
2538
|
+
const db = new DatabaseSync(getHippoDbPath(hippoRoot), { readOnly: true });
|
|
2539
|
+
try {
|
|
2540
|
+
db.exec('PRAGMA busy_timeout = 5000');
|
|
2541
|
+
if (tableExists(db, 'meta'))
|
|
2542
|
+
assertBinaryCompatible(db);
|
|
2543
|
+
return db;
|
|
2544
|
+
}
|
|
2545
|
+
catch (error) {
|
|
2546
|
+
try {
|
|
2547
|
+
db.close();
|
|
2548
|
+
}
|
|
2549
|
+
catch {
|
|
2550
|
+
// Best effort only.
|
|
2551
|
+
}
|
|
2552
|
+
throw error;
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2533
2555
|
function runMigrations(db, hippoRoot) {
|
|
2534
2556
|
ensureMetaTable(db);
|
|
2535
2557
|
// Before anything writes, so a stale binary never repairs or migrates a store it does not understand.
|
|
@@ -2578,6 +2600,8 @@ function ensureMetaTable(db) {
|
|
|
2578
2600
|
`);
|
|
2579
2601
|
}
|
|
2580
2602
|
export function getSchemaVersion(db) {
|
|
2603
|
+
if (!tableExists(db, 'meta'))
|
|
2604
|
+
return 0;
|
|
2581
2605
|
// SAFETY: row's shape matches the single `value` column named in the
|
|
2582
2606
|
// SELECT above.
|
|
2583
2607
|
const row = db.prepare(`SELECT value FROM meta WHERE key = 'schema_version'`).get();
|
|
@@ -2784,6 +2808,18 @@ export function getMeta(db, key, fallback = '') {
|
|
|
2784
2808
|
export function setMeta(db, key, value) {
|
|
2785
2809
|
db.prepare(`INSERT INTO meta(key, value) VALUES(?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value`).run(key, value);
|
|
2786
2810
|
}
|
|
2811
|
+
/** Row count of one table; null when the table is missing or unreadable, which callers show as unknown. */
|
|
2812
|
+
export function countTableRows(db, table) {
|
|
2813
|
+
try {
|
|
2814
|
+
// SAFETY: COUNT(*) returns one row with one numeric column.
|
|
2815
|
+
const row = db.prepare(`SELECT COUNT(*) AS n FROM "${table.replace(/"/g, '""')}"`).get();
|
|
2816
|
+
return Number(row?.n ?? 0);
|
|
2817
|
+
}
|
|
2818
|
+
catch {
|
|
2819
|
+
// Callers treat an uncountable table as unknown; the bundle and doctor still finish.
|
|
2820
|
+
return null;
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2787
2823
|
export function isFtsAvailable(db) {
|
|
2788
2824
|
return getMeta(db, 'fts5_available', '0') === '1';
|
|
2789
2825
|
}
|
package/dist/doctor.js
CHANGED
|
@@ -9,8 +9,8 @@ import * as os from 'node:os';
|
|
|
9
9
|
import * as path from 'node:path';
|
|
10
10
|
import { findHippoStoreDir } from './project-identity.js';
|
|
11
11
|
import { getGlobalRoot } from './shared.js';
|
|
12
|
-
import { isInitialized
|
|
13
|
-
import {
|
|
12
|
+
import { isInitialized } from './store.js';
|
|
13
|
+
import { openHippoDbReadOnly, closeHippoDb, getSchemaVersion, getCurrentSchemaVersion, countTableRows, IncompatibleBinaryError } from './db.js';
|
|
14
14
|
import { isEmbeddingAvailable } from './embeddings.js';
|
|
15
15
|
/** Minimum Node.js version hippo supports (package.json engines). */
|
|
16
16
|
export const MIN_NODE = '22.16.0';
|
|
@@ -34,14 +34,42 @@ function readJson(file) {
|
|
|
34
34
|
return null;
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
-
|
|
37
|
+
// Migration 46 creates failure_log; a read-only open no longer creates it on an older store.
|
|
38
|
+
const FAILURE_LOG_SCHEMA = 46;
|
|
39
|
+
/** The failed-tool-call count over the last 7 days, or why it could not be read. */
|
|
40
|
+
function failuresCheck(db, since, schemaVersion) {
|
|
38
41
|
try {
|
|
39
|
-
// SAFETY: COUNT
|
|
40
|
-
const row = db.prepare(`SELECT COUNT(*) AS n FROM
|
|
41
|
-
return Number(row?.n ?? 0);
|
|
42
|
+
// SAFETY: COUNT aggregate row.
|
|
43
|
+
const row = db.prepare(`SELECT COUNT(*) AS n FROM failure_log WHERE ts >= ?`).get(since);
|
|
44
|
+
return { id: 'failures', status: 'info', detail: `${Number(row?.n ?? 0)} failed tool calls logged in 7 days (hippo failures for detail)` };
|
|
42
45
|
}
|
|
43
|
-
catch {
|
|
44
|
-
|
|
46
|
+
catch (err) {
|
|
47
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
48
|
+
if (!message.includes('no such table')) {
|
|
49
|
+
return { id: 'failures', status: 'warn', detail: `cannot read the failure log: ${message}` };
|
|
50
|
+
}
|
|
51
|
+
return schemaVersion < FAILURE_LOG_SCHEMA
|
|
52
|
+
? { id: 'failures', status: 'info', detail: 'no failure log yet (hippo creates it on the next write)' }
|
|
53
|
+
: { id: 'failures', status: 'warn', detail: 'the failure_log table is missing, so failed tool calls are not being logged' };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** How long ago the store last slept (consolidated), or why that history could not be read. */
|
|
57
|
+
function sleepCheck(db, now) {
|
|
58
|
+
try {
|
|
59
|
+
// SAFETY: row's shape matches the single `timestamp` column named in the SELECT above.
|
|
60
|
+
const row = db.prepare(`SELECT timestamp FROM consolidation_runs ORDER BY timestamp DESC, id DESC LIMIT 1`).get();
|
|
61
|
+
const when = row?.timestamp !== undefined ? Date.parse(row.timestamp) : Number.NaN;
|
|
62
|
+
if (Number.isNaN(when)) {
|
|
63
|
+
return { id: 'sleep', status: 'warn', detail: 'hippo has never slept (consolidated) in this store', fix: 'hippo sleep (the session-end hook runs it automatically)' };
|
|
64
|
+
}
|
|
65
|
+
const days = Math.floor((now.getTime() - when) / 86_400_000);
|
|
66
|
+
return days > 7
|
|
67
|
+
? { id: 'sleep', status: 'warn', detail: `last sleep ${days} days ago`, fix: 'hippo sleep, and check the session-end hook is installed' }
|
|
68
|
+
: { id: 'sleep', status: 'pass', detail: `last sleep ${days === 0 ? 'today' : `${days} day${days === 1 ? '' : 's'} ago`}` };
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
72
|
+
return { id: 'sleep', status: 'info', detail: `sleep history unavailable (${message})` };
|
|
45
73
|
}
|
|
46
74
|
}
|
|
47
75
|
/** Run every check. Never throws for a broken install; broken parts become failed checks. */
|
|
@@ -58,10 +86,14 @@ export function runDoctor(opts) {
|
|
|
58
86
|
const globalRoot = getGlobalRoot();
|
|
59
87
|
const hasGlobal = isInitialized(globalRoot);
|
|
60
88
|
let store = null;
|
|
61
|
-
if (local !== null) {
|
|
89
|
+
if (local !== null && isInitialized(local)) {
|
|
62
90
|
store = local;
|
|
63
91
|
checks.push({ id: 'store', status: 'pass', detail: `project store at ${local}${hasGlobal ? ` (global store at ${globalRoot} too)` : ''}` });
|
|
64
92
|
}
|
|
93
|
+
else if (local !== null) {
|
|
94
|
+
// The walk stops at the first .hippo it finds, so a bare one (no hippo.db) blocks a parent or global store too.
|
|
95
|
+
checks.push({ id: 'store', status: 'fail', detail: `${local} has no hippo.db, so hippo commands run here stop at it`, fix: `run hippo init in ${path.dirname(local)}, or remove that .hippo folder` });
|
|
96
|
+
}
|
|
65
97
|
else if (hasGlobal) {
|
|
66
98
|
store = globalRoot;
|
|
67
99
|
checks.push({ id: 'store', status: 'warn', detail: `no project store here; using the global store at ${globalRoot}`, fix: 'hippo init (in the project root)' });
|
|
@@ -72,7 +104,7 @@ export function runDoctor(opts) {
|
|
|
72
104
|
if (store !== null) {
|
|
73
105
|
let db = null;
|
|
74
106
|
try {
|
|
75
|
-
db =
|
|
107
|
+
db = openHippoDbReadOnly(store);
|
|
76
108
|
const have = getSchemaVersion(db);
|
|
77
109
|
const want = getCurrentSchemaVersion();
|
|
78
110
|
checks.push(have === want
|
|
@@ -80,8 +112,8 @@ export function runDoctor(opts) {
|
|
|
80
112
|
: have > want
|
|
81
113
|
? { id: 'schema', status: 'fail', detail: `database schema v${have} is newer than this hippo (v${want})`, fix: 'npm install -g hippo-memory@latest' }
|
|
82
114
|
: { id: 'schema', status: 'info', detail: `database schema v${have}; hippo migrates it to v${want} on the next write` });
|
|
83
|
-
const memories =
|
|
84
|
-
const dormant =
|
|
115
|
+
const memories = countTableRows(db, 'memories');
|
|
116
|
+
const dormant = countTableRows(db, 'dormant_memories');
|
|
85
117
|
const memoryCheck = { id: 'memories', status: 'info', detail: `${memories ?? '?'} memories${dormant !== null ? `, ${dormant} dormant` : ''}` };
|
|
86
118
|
if (memories === 0) {
|
|
87
119
|
memoryCheck.status = 'warn';
|
|
@@ -97,42 +129,21 @@ export function runDoctor(opts) {
|
|
|
97
129
|
catch {
|
|
98
130
|
checks.push({ id: 'tokens', status: 'info', detail: 'no token ledger yet (created on the next write)' });
|
|
99
131
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const row = db.prepare(`SELECT COUNT(*) AS n FROM failure_log WHERE ts >= ?`).get(since);
|
|
103
|
-
checks.push({ id: 'failures', status: 'info', detail: `${Number(row?.n ?? 0)} failed tool calls logged in 7 days (hippo failures for detail)` });
|
|
104
|
-
}
|
|
105
|
-
catch {
|
|
106
|
-
// Opening the store migrates it, so a missing table was dropped: the hook is logging nothing.
|
|
107
|
-
checks.push({ id: 'failures', status: 'warn', detail: 'the failure_log table is missing, so failed tool calls are not being logged' });
|
|
108
|
-
}
|
|
132
|
+
checks.push(failuresCheck(db, since, have));
|
|
133
|
+
checks.push(sleepCheck(db, now));
|
|
109
134
|
}
|
|
110
135
|
catch (err) {
|
|
111
|
-
checks.push({
|
|
136
|
+
checks.push({
|
|
137
|
+
id: 'schema',
|
|
138
|
+
status: 'fail',
|
|
139
|
+
detail: `cannot open the database: ${err instanceof Error ? err.message : String(err)}`,
|
|
140
|
+
fix: err instanceof IncompatibleBinaryError ? 'npm install -g hippo-memory@latest' : 'check file permissions on the .hippo folder',
|
|
141
|
+
});
|
|
112
142
|
}
|
|
113
143
|
finally {
|
|
114
144
|
if (db !== null)
|
|
115
145
|
closeHippoDb(db);
|
|
116
146
|
}
|
|
117
|
-
try {
|
|
118
|
-
const runs = loadStats(store)['consolidation_runs'];
|
|
119
|
-
const last = Array.isArray(runs) && runs.length > 0 ? runs[runs.length - 1] : null;
|
|
120
|
-
// SAFETY: the constructor check above narrows `last` to a plain JSON object.
|
|
121
|
-
const ts = last !== null && last !== undefined && !Array.isArray(last) && last.constructor === Object ? last.timestamp : undefined;
|
|
122
|
-
const when = ts !== undefined && ts !== null ? Date.parse(String(ts)) : Number.NaN;
|
|
123
|
-
if (Number.isNaN(when)) {
|
|
124
|
-
checks.push({ id: 'sleep', status: 'warn', detail: 'hippo has never slept (consolidated) in this store', fix: 'hippo sleep (the session-end hook runs it automatically)' });
|
|
125
|
-
}
|
|
126
|
-
else {
|
|
127
|
-
const days = Math.floor((now.getTime() - when) / 86_400_000);
|
|
128
|
-
checks.push(days > 7
|
|
129
|
-
? { id: 'sleep', status: 'warn', detail: `last sleep ${days} days ago`, fix: 'hippo sleep, and check the session-end hook is installed' }
|
|
130
|
-
: { id: 'sleep', status: 'pass', detail: `last sleep ${days === 0 ? 'today' : `${days} day${days === 1 ? '' : 's'} ago`}` });
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
catch {
|
|
134
|
-
checks.push({ id: 'sleep', status: 'info', detail: 'sleep history unavailable' });
|
|
135
|
-
}
|
|
136
147
|
}
|
|
137
148
|
const claudeDir = path.join(home, '.claude');
|
|
138
149
|
if (fs.existsSync(claudeDir)) {
|
package/dist/reject-flow.d.ts
CHANGED
|
@@ -41,8 +41,8 @@ export interface RejectFlowResult {
|
|
|
41
41
|
* normalized digest matches (kind-aware), one aggregate `reject_value`
|
|
42
42
|
* audit, COMMIT. Then post-commit (mirrors the existing purge+reaper
|
|
43
43
|
* pattern verbatim from api.archiveRaw, api.ts:1913-1938): best-effort
|
|
44
|
-
* mirror purge per removed id, `mirror_cleaned_at` stamps for raw ids
|
|
45
|
-
* index
|
|
44
|
+
* mirror purge per removed id, `mirror_cleaned_at` stamps for raw ids.
|
|
45
|
+
* index.json itself is only refreshed by `rebuildIndex()`.
|
|
46
46
|
*/
|
|
47
47
|
export declare function rejectValue(opts: RejectFlowOpts): RejectFlowResult;
|
|
48
48
|
export type UnrejectOutcome = {
|
package/dist/reject-flow.js
CHANGED
|
@@ -15,7 +15,7 @@ import { openHippoDb, closeHippoDb } from './db.js';
|
|
|
15
15
|
import { appendAuditEvent } from './audit.js';
|
|
16
16
|
import { archiveRawMemory } from './raw-archive.js';
|
|
17
17
|
import { purgeDormantByDigest } from './dormant.js';
|
|
18
|
-
import { initStore, deleteEntryCore, purgeMirrorBestEffort,
|
|
18
|
+
import { initStore, deleteEntryCore, purgeMirrorBestEffort, } from './store.js';
|
|
19
19
|
import { rejectionDigest, normalizeValueForRejection, insertRejectedValue, deleteRejectedValue, listRejectedValues, } from './rejection.js';
|
|
20
20
|
/**
|
|
21
21
|
* `hippo reject` / `api.reject` core flow. ONE connection, one transaction:
|
|
@@ -23,8 +23,8 @@ import { rejectionDigest, normalizeValueForRejection, insertRejectedValue, delet
|
|
|
23
23
|
* normalized digest matches (kind-aware), one aggregate `reject_value`
|
|
24
24
|
* audit, COMMIT. Then post-commit (mirrors the existing purge+reaper
|
|
25
25
|
* pattern verbatim from api.archiveRaw, api.ts:1913-1938): best-effort
|
|
26
|
-
* mirror purge per removed id, `mirror_cleaned_at` stamps for raw ids
|
|
27
|
-
* index
|
|
26
|
+
* mirror purge per removed id, `mirror_cleaned_at` stamps for raw ids.
|
|
27
|
+
* index.json itself is only refreshed by `rebuildIndex()`.
|
|
28
28
|
*/
|
|
29
29
|
export function rejectValue(opts) {
|
|
30
30
|
if (!opts.reason.trim()) {
|
|
@@ -138,7 +138,7 @@ export function rejectValue(opts) {
|
|
|
138
138
|
}
|
|
139
139
|
// Post-commit, db handle still open (same pattern as api.archiveRaw):
|
|
140
140
|
// best-effort mirror purge per removed id, reaper-backstop stamp for
|
|
141
|
-
// raw ids
|
|
141
|
+
// raw ids.
|
|
142
142
|
for (const id of removedIds) {
|
|
143
143
|
// AT1 fix: purgeMirrorBestEffort retries once, then — for non-raw ids,
|
|
144
144
|
// which cleanupArchivedMirrors' reaper never scans — reports the
|
|
@@ -150,9 +150,6 @@ export function rejectValue(opts) {
|
|
|
150
150
|
db.prepare(`UPDATE raw_archive SET mirror_cleaned_at = ? WHERE memory_id = ?`).run(new Date().toISOString(), id);
|
|
151
151
|
}
|
|
152
152
|
}
|
|
153
|
-
if (removedIds.length > 0) {
|
|
154
|
-
writeIndexMirror(opts.hippoRoot, buildIndexFromDb(db));
|
|
155
|
-
}
|
|
156
153
|
return { digest, content, removedIds, removedRawIds };
|
|
157
154
|
}
|
|
158
155
|
finally {
|
package/dist/secret-detect.d.ts
CHANGED
|
@@ -38,4 +38,6 @@ export declare function detectSecret(entry: {
|
|
|
38
38
|
* CS1 pre-compact snapshot fields — can scrub it in place instead.
|
|
39
39
|
*/
|
|
40
40
|
export declare function redactSecrets(text: string): string;
|
|
41
|
+
/** Stricter redaction for text that leaves the machine: no co-occurrence guard, plus Bearer headers and JWTs. */
|
|
42
|
+
export declare function redactSecretsStrict(text: string): string;
|
|
41
43
|
//# sourceMappingURL=secret-detect.d.ts.map
|
package/dist/secret-detect.js
CHANGED
|
@@ -48,6 +48,11 @@ const SECRET_PATTERNS = [
|
|
|
48
48
|
];
|
|
49
49
|
const KEYISH_CONTEXT_RE = /key|token|secret|credential|bearer|auth|password/i;
|
|
50
50
|
const CO_OCCURRENCE_GUARDED = new Set(['sk-style-key', 'sk-underscore-key']);
|
|
51
|
+
// redactSecretsStrict-only: too noisy for whole-entry memory scanning, worth hiding once text leaves the machine.
|
|
52
|
+
const STRICT_ONLY_PATTERNS = [
|
|
53
|
+
/\b[Bb]earer\s+[A-Za-z0-9._~+/-]{16,}=*/g,
|
|
54
|
+
/\beyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]*/g,
|
|
55
|
+
];
|
|
51
56
|
/**
|
|
52
57
|
* Scan a memory's tags + content for secret material.
|
|
53
58
|
* Pure and deterministic; no filesystem or store access.
|
|
@@ -75,21 +80,28 @@ export function detectSecret(entry) {
|
|
|
75
80
|
* CS1 pre-compact snapshot fields — can scrub it in place instead.
|
|
76
81
|
*/
|
|
77
82
|
export function redactSecrets(text) {
|
|
83
|
+
return redactText(text, false);
|
|
84
|
+
}
|
|
85
|
+
/** Stricter redaction for text that leaves the machine: no co-occurrence guard, plus Bearer headers and JWTs. */
|
|
86
|
+
export function redactSecretsStrict(text) {
|
|
87
|
+
return redactText(text, true);
|
|
88
|
+
}
|
|
89
|
+
function redactText(text, strict) {
|
|
78
90
|
if (!text)
|
|
79
91
|
return text;
|
|
80
92
|
let result = text;
|
|
81
|
-
// PEM/OpenSSH blocks first: the
|
|
82
|
-
// BEGIN delimiter, which is fine for detectSecret's flag-or-not decision
|
|
83
|
-
// but would leave the base64 payload behind here. Consume through the
|
|
84
|
-
// matching END delimiter; a truncated block with no END is redacted to
|
|
85
|
-
// the end of the text (codex round 3).
|
|
93
|
+
// PEM/OpenSSH blocks first: SECRET_PATTERNS only matches BEGIN; consume through END, or to the end of a truncated block.
|
|
86
94
|
result = result.replace(/-----BEGIN [A-Z ]*PRIVATE KEY-----(?:[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----|[\s\S]*$)/g, '[REDACTED]');
|
|
87
95
|
for (const { name, re } of SECRET_PATTERNS) {
|
|
88
|
-
if (CO_OCCURRENCE_GUARDED.has(name) && !KEYISH_CONTEXT_RE.test(text))
|
|
96
|
+
if (!strict && CO_OCCURRENCE_GUARDED.has(name) && !KEYISH_CONTEXT_RE.test(text))
|
|
89
97
|
continue;
|
|
90
98
|
const flags = re.flags.includes('g') ? re.flags : `${re.flags}g`;
|
|
91
99
|
result = result.replace(new RegExp(re.source, flags), '[REDACTED]');
|
|
92
100
|
}
|
|
101
|
+
if (strict) {
|
|
102
|
+
for (const re of STRICT_ONLY_PATTERNS)
|
|
103
|
+
result = result.replace(re, '[REDACTED]');
|
|
104
|
+
}
|
|
93
105
|
return result;
|
|
94
106
|
}
|
|
95
107
|
//# sourceMappingURL=secret-detect.js.map
|
package/dist/store.d.ts
CHANGED
|
@@ -190,23 +190,13 @@ interface LegacyStats {
|
|
|
190
190
|
total_forgotten: JsonValue;
|
|
191
191
|
consolidation_runs: JsonValue;
|
|
192
192
|
}
|
|
193
|
-
/**
|
|
194
|
-
*
|
|
195
|
-
* meta) from SQLite, the source of truth. Exported (AT1) for the same
|
|
196
|
-
* reason as `writeIndexMirror` below: `src/reject-flow.ts` needs to rebuild
|
|
197
|
-
* the index mirror post-commit after a (possibly multi-row) reject removal,
|
|
198
|
-
* without duplicating this query.
|
|
199
|
-
*/
|
|
193
|
+
/** Derive the current `HippoIndex` from SQLite. Exported for `rebuildIndex`
|
|
194
|
+
* (the only index.json writer) and the longmemeval benchmark. */
|
|
200
195
|
export declare function buildIndexFromDb(db: ReturnType<typeof openHippoDb>): HippoIndex;
|
|
201
|
-
/**
|
|
202
|
-
*
|
|
203
|
-
* Exported (AT1) so `src/reject-flow.ts` can replicate `deleteEntry`'s exact
|
|
204
|
-
* post-commit "removeEntryMirrors then rewrite the index once" sequence for
|
|
205
|
-
* the reject verb's (possibly multi-row) removal, without duplicating
|
|
206
|
-
* `buildIndexFromDb`'s query.
|
|
207
|
-
*/
|
|
196
|
+
/** Write the `index.json` mirror file for an already-derived index. Exported for
|
|
197
|
+
* `rebuildIndex` (the only index.json writer) and the longmemeval benchmark. */
|
|
208
198
|
export declare function writeIndexMirror(hippoRoot: string, index: HippoIndex): void;
|
|
209
|
-
/** Load the derived index from SQLite. Read-only:
|
|
199
|
+
/** Load the derived index from SQLite. Read-only: index.json is only ever written by `rebuildIndex`. */
|
|
210
200
|
export declare function loadIndex(hippoRoot: string): HippoIndex;
|
|
211
201
|
/**
|
|
212
202
|
* Persist mutable index metadata. Entry rows themselves are derived from SQLite.
|
|
@@ -215,10 +205,8 @@ export declare function loadIndex(hippoRoot: string): HippoIndex;
|
|
|
215
205
|
* land atomically — callers (getContext, cmdRecall) fold a freshly-written
|
|
216
206
|
* trace id into `index.last_trace_id` before calling this, relying on BOTH
|
|
217
207
|
* meta keys committing together. Wrapped in BEGIN/COMMIT so a crash or a
|
|
218
|
-
* mid-write failure can never advance one key without the other.
|
|
219
|
-
*
|
|
220
|
-
* truth, the mirror is best-effort (matches every other per-call-handle
|
|
221
|
-
* site's convention).
|
|
208
|
+
* mid-write failure can never advance one key without the other. index.json
|
|
209
|
+
* is left untouched; only `rebuildIndex` writes it.
|
|
222
210
|
*/
|
|
223
211
|
export declare function saveIndex(hippoRoot: string, index: HippoIndex): void;
|
|
224
212
|
/**
|
|
@@ -277,13 +265,8 @@ export declare function writeEntryDbOnly(db: DatabaseSyncLike, entry: MemoryEntr
|
|
|
277
265
|
actor?: string;
|
|
278
266
|
afterWrite?: (db: DatabaseSyncLike, memoryId: string) => void;
|
|
279
267
|
}): void;
|
|
280
|
-
/**
|
|
281
|
-
|
|
282
|
-
* (used by `buildIndexFromDb` to derive the index from the source of truth).
|
|
283
|
-
* MUST be invoked AFTER the outer transaction commits — a mirror write
|
|
284
|
-
* during a tx that subsequently rolls back would leave orphan markdown.
|
|
285
|
-
*/
|
|
286
|
-
export declare function writeEntryMirrors(hippoRoot: string, db: DatabaseSyncLike, entry: MemoryEntry): void;
|
|
268
|
+
/** Markdown mirror path, invoked AFTER commit (a rolled-back tx must leave no orphan markdown). */
|
|
269
|
+
export declare function writeEntryMirrors(hippoRoot: string, entry: MemoryEntry): void;
|
|
287
270
|
/**
|
|
288
271
|
* Read a memory entry by ID.
|
|
289
272
|
*
|
package/dist/store.js
CHANGED
|
@@ -940,6 +940,7 @@ function upsertEntryRow(db, entry, bypassRejectionGuard = false) {
|
|
|
940
940
|
if (!bypassRejectionGuard) {
|
|
941
941
|
checkRejectionGuard(db, entry.tenantId ?? 'default', entry.id, entry.content);
|
|
942
942
|
}
|
|
943
|
+
const isNewRow = db.prepare(`SELECT 1 FROM memories WHERE id = ?`).get(entry.id) === undefined;
|
|
943
944
|
db.prepare(`
|
|
944
945
|
INSERT INTO memories(
|
|
945
946
|
id, created, last_retrieved, retrieval_count, strength, half_life_days, layer,
|
|
@@ -996,13 +997,14 @@ function upsertEntryRow(db, entry, bypassRejectionGuard = false) {
|
|
|
996
997
|
dag_level_3_built_at = excluded.dag_level_3_built_at,
|
|
997
998
|
updated_at = datetime('now')
|
|
998
999
|
`).run(entry.id, entry.created, entry.last_retrieved, entry.retrieval_count, entry.strength, entry.half_life_days, entry.layer, JSON.stringify(entry.tags ?? []), entry.emotional_valence, entry.schema_fit, entry.source, entry.outcome_score, entry.outcome_positive ?? 0, entry.outcome_negative ?? 0, JSON.stringify(entry.conflicts_with ?? []), entry.pinned ? 1 : 0, entry.confidence, entry.content, JSON.stringify(entry.parents ?? []), entry.starred ? 1 : 0, entry.trace_outcome ?? null, entry.source_session_id ?? null, entry.valid_from ?? entry.created, entry.superseded_by ?? null, entry.extracted_from ?? null, entry.dag_level ?? 0, entry.dag_parent_id ?? null, entry.kind ?? 'distilled', entry.scope ?? null, entry.owner ?? null, entry.artifact_ref ?? null, entry.tenantId ?? 'default', entry.origin_project ?? null, entry.descendant_count ?? 0, entry.earliest_at ?? null, entry.latest_at ?? null, entry.dag_level_3_built_at ?? null);
|
|
999
|
-
syncFtsRow(db, entry);
|
|
1000
|
+
syncFtsRow(db, entry, isNewRow);
|
|
1000
1001
|
}
|
|
1001
|
-
function syncFtsRow(db, entry) {
|
|
1002
|
+
function syncFtsRow(db, entry, isNewRow = false) {
|
|
1002
1003
|
if (!isFtsAvailable(db))
|
|
1003
1004
|
return;
|
|
1004
1005
|
try {
|
|
1005
|
-
|
|
1006
|
+
if (!isNewRow)
|
|
1007
|
+
db.prepare(`DELETE FROM memories_fts WHERE id = ?`).run(entry.id);
|
|
1006
1008
|
db.prepare(`INSERT INTO memories_fts(id, content, tags) VALUES (?, ?, ?)`).run(entry.id, entry.content, entry.tags.join(' '));
|
|
1007
1009
|
}
|
|
1008
1010
|
catch {
|
|
@@ -1019,13 +1021,8 @@ function deleteFtsRow(db, id) {
|
|
|
1019
1021
|
// Best effort.
|
|
1020
1022
|
}
|
|
1021
1023
|
}
|
|
1022
|
-
/**
|
|
1023
|
-
*
|
|
1024
|
-
* meta) from SQLite, the source of truth. Exported (AT1) for the same
|
|
1025
|
-
* reason as `writeIndexMirror` below: `src/reject-flow.ts` needs to rebuild
|
|
1026
|
-
* the index mirror post-commit after a (possibly multi-row) reject removal,
|
|
1027
|
-
* without duplicating this query.
|
|
1028
|
-
*/
|
|
1024
|
+
/** Derive the current `HippoIndex` from SQLite. Exported for `rebuildIndex`
|
|
1025
|
+
* (the only index.json writer) and the longmemeval benchmark. */
|
|
1029
1026
|
export function buildIndexFromDb(db) {
|
|
1030
1027
|
// SAFETY: rows' shape matches the seven columns named in the SELECT below.
|
|
1031
1028
|
const rows = db.prepare(`SELECT id, created, last_retrieved, strength, layer, tags_json, pinned FROM memories ORDER BY created ASC, id ASC`).all();
|
|
@@ -1076,13 +1073,8 @@ function buildStatsFromDb(db) {
|
|
|
1076
1073
|
})),
|
|
1077
1074
|
};
|
|
1078
1075
|
}
|
|
1079
|
-
/**
|
|
1080
|
-
*
|
|
1081
|
-
* Exported (AT1) so `src/reject-flow.ts` can replicate `deleteEntry`'s exact
|
|
1082
|
-
* post-commit "removeEntryMirrors then rewrite the index once" sequence for
|
|
1083
|
-
* the reject verb's (possibly multi-row) removal, without duplicating
|
|
1084
|
-
* `buildIndexFromDb`'s query.
|
|
1085
|
-
*/
|
|
1076
|
+
/** Write the `index.json` mirror file for an already-derived index. Exported for
|
|
1077
|
+
* `rebuildIndex` (the only index.json writer) and the longmemeval benchmark. */
|
|
1086
1078
|
export function writeIndexMirror(hippoRoot, index) {
|
|
1087
1079
|
mirrorBestEffort('index.json', () => fs.writeFileSync(path.join(hippoRoot, 'index.json'), JSON.stringify(index, null, 2), 'utf8'));
|
|
1088
1080
|
}
|
|
@@ -1115,10 +1107,9 @@ function syncMirrorFiles(hippoRoot, db) {
|
|
|
1115
1107
|
ORDER BY updated_at DESC, id DESC
|
|
1116
1108
|
`).all();
|
|
1117
1109
|
mirrorBestEffort('conflict mirrors', () => writeConflictMirrors(hippoRoot, conflicts.map(rowToMemoryConflict)));
|
|
1118
|
-
writeIndexMirror(hippoRoot, buildIndexFromDb(db));
|
|
1119
1110
|
writeStatsMirror(hippoRoot, buildStatsFromDb(db));
|
|
1120
1111
|
}
|
|
1121
|
-
/** Load the derived index from SQLite. Read-only:
|
|
1112
|
+
/** Load the derived index from SQLite. Read-only: index.json is only ever written by `rebuildIndex`. */
|
|
1122
1113
|
export function loadIndex(hippoRoot) {
|
|
1123
1114
|
initStore(hippoRoot);
|
|
1124
1115
|
const db = openHippoDb(hippoRoot);
|
|
@@ -1136,10 +1127,8 @@ export function loadIndex(hippoRoot) {
|
|
|
1136
1127
|
* land atomically — callers (getContext, cmdRecall) fold a freshly-written
|
|
1137
1128
|
* trace id into `index.last_trace_id` before calling this, relying on BOTH
|
|
1138
1129
|
* meta keys committing together. Wrapped in BEGIN/COMMIT so a crash or a
|
|
1139
|
-
* mid-write failure can never advance one key without the other.
|
|
1140
|
-
*
|
|
1141
|
-
* truth, the mirror is best-effort (matches every other per-call-handle
|
|
1142
|
-
* site's convention).
|
|
1130
|
+
* mid-write failure can never advance one key without the other. index.json
|
|
1131
|
+
* is left untouched; only `rebuildIndex` writes it.
|
|
1143
1132
|
*/
|
|
1144
1133
|
export function saveIndex(hippoRoot, index) {
|
|
1145
1134
|
initStore(hippoRoot);
|
|
@@ -1158,7 +1147,6 @@ export function saveIndex(hippoRoot, index) {
|
|
|
1158
1147
|
catch { /* already rolled back; keep the original error */ }
|
|
1159
1148
|
throw error;
|
|
1160
1149
|
}
|
|
1161
|
-
writeIndexMirror(hippoRoot, buildIndexFromDb(db));
|
|
1162
1150
|
}
|
|
1163
1151
|
finally {
|
|
1164
1152
|
closeHippoDb(db);
|
|
@@ -1222,7 +1210,7 @@ export function writeEntry(hippoRoot, entry, opts) {
|
|
|
1222
1210
|
try {
|
|
1223
1211
|
writeEntryDbOnly(db, stamped, opts);
|
|
1224
1212
|
opts?.afterCommit?.();
|
|
1225
|
-
writeEntryMirrors(hippoRoot,
|
|
1213
|
+
writeEntryMirrors(hippoRoot, stamped);
|
|
1226
1214
|
}
|
|
1227
1215
|
catch (error) {
|
|
1228
1216
|
// AT1 (plan §3): writeEntryDbOnly's own SAVEPOINT has already unwound by
|
|
@@ -1286,15 +1274,9 @@ export function writeEntryDbOnly(db, entry, opts) {
|
|
|
1286
1274
|
throw e;
|
|
1287
1275
|
}
|
|
1288
1276
|
}
|
|
1289
|
-
/**
|
|
1290
|
-
|
|
1291
|
-
* (used by `buildIndexFromDb` to derive the index from the source of truth).
|
|
1292
|
-
* MUST be invoked AFTER the outer transaction commits — a mirror write
|
|
1293
|
-
* during a tx that subsequently rolls back would leave orphan markdown.
|
|
1294
|
-
*/
|
|
1295
|
-
export function writeEntryMirrors(hippoRoot, db, entry) {
|
|
1277
|
+
/** Markdown mirror path, invoked AFTER commit (a rolled-back tx must leave no orphan markdown). */
|
|
1278
|
+
export function writeEntryMirrors(hippoRoot, entry) {
|
|
1296
1279
|
mirrorBestEffort(`${entry.id}.md`, () => writeMarkdownMirror(hippoRoot, entry));
|
|
1297
|
-
writeIndexMirror(hippoRoot, buildIndexFromDb(db));
|
|
1298
1280
|
}
|
|
1299
1281
|
/**
|
|
1300
1282
|
* Read a memory entry by ID.
|
|
@@ -1611,7 +1593,6 @@ export function deleteEntry(hippoRoot, id, opts) {
|
|
|
1611
1593
|
if (!result)
|
|
1612
1594
|
return false;
|
|
1613
1595
|
purgeMirrorBestEffort(hippoRoot, id, false, 'deleteEntry');
|
|
1614
|
-
writeIndexMirror(hippoRoot, buildIndexFromDb(db));
|
|
1615
1596
|
return true;
|
|
1616
1597
|
}
|
|
1617
1598
|
finally {
|
|
@@ -1786,7 +1767,6 @@ export function batchWriteAndDelete(hippoRoot, toWrite, toDeleteIds, opts) {
|
|
|
1786
1767
|
});
|
|
1787
1768
|
for (const id of removedIds)
|
|
1788
1769
|
purgeMirrorBestEffort(hippoRoot, id, false, 'batchWriteAndDelete');
|
|
1789
|
-
writeIndexMirror(hippoRoot, buildIndexFromDb(db));
|
|
1790
1770
|
return removedIds;
|
|
1791
1771
|
}
|
|
1792
1772
|
catch (error) {
|
|
@@ -1978,7 +1958,9 @@ export function rebuildIndex(hippoRoot) {
|
|
|
1978
1958
|
}
|
|
1979
1959
|
}
|
|
1980
1960
|
syncMirrorFiles(hippoRoot, db);
|
|
1981
|
-
|
|
1961
|
+
const index = buildIndexFromDb(db);
|
|
1962
|
+
writeIndexMirror(hippoRoot, index);
|
|
1963
|
+
return index;
|
|
1982
1964
|
}
|
|
1983
1965
|
finally {
|
|
1984
1966
|
closeHippoDb(db);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type DoctorOpts } from './doctor.js';
|
|
2
|
+
import type { JsonObject } from './working-memory.js';
|
|
3
|
+
export interface SupportBundleOpts extends DoctorOpts {
|
|
4
|
+
readonly cwd: string;
|
|
5
|
+
readonly home: string;
|
|
6
|
+
readonly now: Date;
|
|
7
|
+
readonly includeLogs: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare const TAIL_MAX_LINES = 200;
|
|
10
|
+
/** Read-only: builds one redacted support-ticket snapshot. Never reads a memory content column. */
|
|
11
|
+
export declare function buildSupportBundle(opts: SupportBundleOpts): JsonObject;
|
|
12
|
+
//# sourceMappingURL=support-bundle.d.ts.map
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/** `hippo support-bundle`: one redacted JSON snapshot for a support ticket. Read-only; never touches memory content. */
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import { findHippoStoreDir, isGlobalStoreRoot, realpathOrResolve } from './project-identity.js';
|
|
6
|
+
import { getGlobalRoot } from './shared.js';
|
|
7
|
+
import { isInitialized } from './store.js';
|
|
8
|
+
import { openHippoDbReadOnly, closeHippoDb, getSchemaVersion, getMeta, countTableRows } from './db.js';
|
|
9
|
+
import { runDoctor } from './doctor.js';
|
|
10
|
+
import { loadConfig } from './config.js';
|
|
11
|
+
import { redactSecretsStrict } from './secret-detect.js';
|
|
12
|
+
const TAIL_MAX_BYTES = 256 * 1024;
|
|
13
|
+
export const TAIL_MAX_LINES = 200;
|
|
14
|
+
const TAIL_MAX_LINE_CHARS = 2000;
|
|
15
|
+
// Greedy on purpose: complete key blocks are redacted first, so any END left is an orphan and what precedes it may be key body.
|
|
16
|
+
const ORPHAN_KEY_END_RE = /^[\s\S]*-----END [A-Z ]*PRIVATE KEY-----/;
|
|
17
|
+
// The other env vars hippo reads outside the HIPPO_ prefix (src/embedding-provider.ts, connectors/*).
|
|
18
|
+
const OTHER_ENV_NAMES = [
|
|
19
|
+
'ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'VOYAGE_API_KEY', 'COHERE_API_KEY', 'TYPESAFE_API_KEY',
|
|
20
|
+
'GITHUB_TOKEN', 'GITHUB_WEBHOOK_SECRET', 'GITHUB_WEBHOOK_SECRET_PREVIOUS',
|
|
21
|
+
'GITHUB_ALLOW_UNKNOWN_INSTALLATION_FALLBACK', 'SLACK_BOT_TOKEN', 'SLACK_TEAM_ID',
|
|
22
|
+
'SLACK_SIGNING_SECRET', 'SLACK_SIGNING_SECRET_PREVIOUS', 'SLACK_ALLOW_UNKNOWN_TEAM_FALLBACK',
|
|
23
|
+
'XDG_DATA_HOME', 'MCP_SSE_HEARTBEAT_MS', 'MCP_SSE_MAX_AGE_SEC',
|
|
24
|
+
];
|
|
25
|
+
const CONFIG_SECRET_KEY_RE = /key|token|secret|passw|credential|auth|cookie|bearer|signature|private/i;
|
|
26
|
+
function isJsonObject(v) {
|
|
27
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
28
|
+
}
|
|
29
|
+
function isJsonString(v) {
|
|
30
|
+
return typeof v === 'string';
|
|
31
|
+
}
|
|
32
|
+
function buildRuntime() {
|
|
33
|
+
return {
|
|
34
|
+
node: process.versions.node,
|
|
35
|
+
sqlite: process.versions.sqlite ?? null,
|
|
36
|
+
platform: process.platform,
|
|
37
|
+
arch: process.arch,
|
|
38
|
+
osRelease: os.release(),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function listTableNames(db) {
|
|
42
|
+
// SAFETY: each row's shape matches the single `name` column named in the SELECT above.
|
|
43
|
+
const rows = db.prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' AND sql NOT LIKE 'CREATE VIRTUAL TABLE%' ORDER BY name`).all();
|
|
44
|
+
return rows.map((r) => r.name);
|
|
45
|
+
}
|
|
46
|
+
function countTables(db) {
|
|
47
|
+
const tables = {};
|
|
48
|
+
for (const name of listTableNames(db))
|
|
49
|
+
tables[name] = countTableRows(db, name);
|
|
50
|
+
return tables;
|
|
51
|
+
}
|
|
52
|
+
function redactConfigValue(value) {
|
|
53
|
+
if (Array.isArray(value))
|
|
54
|
+
return value.map(redactConfigValue);
|
|
55
|
+
if (isJsonObject(value)) {
|
|
56
|
+
const out = {};
|
|
57
|
+
for (const [key, v] of Object.entries(value)) {
|
|
58
|
+
out[key] = CONFIG_SECRET_KEY_RE.test(key) ? '[REDACTED]' : redactConfigValue(v);
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
function readStoreConfig(storeDir) {
|
|
65
|
+
const configPath = path.join(storeDir, 'config.json');
|
|
66
|
+
let configFile = 'absent';
|
|
67
|
+
if (fs.existsSync(configPath)) {
|
|
68
|
+
try {
|
|
69
|
+
JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
70
|
+
configFile = 'present';
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
configFile = 'invalid';
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// loadConfig always returns the effective config: its own defaults on a missing or broken file.
|
|
77
|
+
const effective = JSON.parse(JSON.stringify(loadConfig(storeDir)));
|
|
78
|
+
return { configFile, config: redactConfigValue(effective) };
|
|
79
|
+
}
|
|
80
|
+
function buildStoreEntry(kind, storeDir) {
|
|
81
|
+
const dbPath = path.join(storeDir, 'hippo.db');
|
|
82
|
+
if (!fs.existsSync(dbPath)) {
|
|
83
|
+
return { kind, path: storeDir, error: 'no hippo.db here (hippo init has not run)' };
|
|
84
|
+
}
|
|
85
|
+
// Stat before opening: a read-only open leaves an empty -wal behind it.
|
|
86
|
+
const files = { 'hippo.db': fs.statSync(dbPath).size };
|
|
87
|
+
const walPath = `${dbPath}-wal`;
|
|
88
|
+
if (fs.existsSync(walPath))
|
|
89
|
+
files['hippo.db-wal'] = fs.statSync(walPath).size;
|
|
90
|
+
let db = null;
|
|
91
|
+
try {
|
|
92
|
+
db = openHippoDbReadOnly(storeDir);
|
|
93
|
+
const schemaVersion = getSchemaVersion(db);
|
|
94
|
+
const minCompatibleBinary = getMeta(db, 'min_compatible_binary', '') || null;
|
|
95
|
+
const tables = countTables(db);
|
|
96
|
+
const { configFile, config } = readStoreConfig(storeDir);
|
|
97
|
+
return { kind, path: storeDir, schemaVersion, minCompatibleBinary, files, tables, configFile, config };
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
return { kind, path: storeDir, error: err instanceof Error ? err.message : String(err) };
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
if (db !== null)
|
|
104
|
+
closeHippoDb(db);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function buildStores(opts) {
|
|
108
|
+
const stores = [];
|
|
109
|
+
const projectDir = findHippoStoreDir(opts.cwd, { homeDir: opts.home });
|
|
110
|
+
if (projectDir !== null)
|
|
111
|
+
stores.push(buildStoreEntry('project', projectDir));
|
|
112
|
+
const globalDir = getGlobalRoot();
|
|
113
|
+
const alreadyListed = projectDir !== null && isGlobalStoreRoot(projectDir);
|
|
114
|
+
if (isInitialized(globalDir) && !alreadyListed)
|
|
115
|
+
stores.push(buildStoreEntry('global', globalDir));
|
|
116
|
+
return stores;
|
|
117
|
+
}
|
|
118
|
+
function listSetEnvNames() {
|
|
119
|
+
const names = new Set();
|
|
120
|
+
for (const key of Object.keys(process.env)) {
|
|
121
|
+
if (key.startsWith('HIPPO_'))
|
|
122
|
+
names.add(key);
|
|
123
|
+
}
|
|
124
|
+
for (const key of OTHER_ENV_NAMES) {
|
|
125
|
+
if (process.env[key] !== undefined)
|
|
126
|
+
names.add(key);
|
|
127
|
+
}
|
|
128
|
+
return [...names].sort();
|
|
129
|
+
}
|
|
130
|
+
function listLogFileNames(dir) {
|
|
131
|
+
if (!fs.existsSync(dir))
|
|
132
|
+
return [];
|
|
133
|
+
return fs.readdirSync(dir, { withFileTypes: true })
|
|
134
|
+
.filter((d) => d.isFile())
|
|
135
|
+
.map((d) => d.name)
|
|
136
|
+
.sort();
|
|
137
|
+
}
|
|
138
|
+
function tailLogFile(file) {
|
|
139
|
+
const size = fs.statSync(file).size;
|
|
140
|
+
const readSize = Math.min(size, TAIL_MAX_BYTES);
|
|
141
|
+
const startedMidFile = size > TAIL_MAX_BYTES;
|
|
142
|
+
const fd = fs.openSync(file, 'r');
|
|
143
|
+
let text;
|
|
144
|
+
try {
|
|
145
|
+
const buf = Buffer.alloc(readSize);
|
|
146
|
+
fs.readSync(fd, buf, 0, readSize, size - readSize);
|
|
147
|
+
text = buf.toString('utf8');
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
fs.closeSync(fd);
|
|
151
|
+
}
|
|
152
|
+
let lines = text.split(/\r?\n/);
|
|
153
|
+
// A tail read starting mid-file cuts its first piece off mid-line.
|
|
154
|
+
if (startedMidFile)
|
|
155
|
+
lines = lines.slice(1);
|
|
156
|
+
if (lines.length > 0 && lines[lines.length - 1] === '')
|
|
157
|
+
lines = lines.slice(0, -1);
|
|
158
|
+
// Redact before splitting: a private key spans lines. An END still left has lost its BEGIN to the window cut.
|
|
159
|
+
const redacted = redactSecretsStrict(lines.join('\n')).replace(ORPHAN_KEY_END_RE, '[REDACTED]');
|
|
160
|
+
return redacted.split('\n').slice(-TAIL_MAX_LINES).map((line) => line.length > TAIL_MAX_LINE_CHARS ? `${line.slice(0, TAIL_MAX_LINE_CHARS)} [truncated]` : line);
|
|
161
|
+
}
|
|
162
|
+
function buildLogsSection(opts) {
|
|
163
|
+
const dir = path.join(opts.home, '.hippo', 'logs');
|
|
164
|
+
const names = listLogFileNames(dir);
|
|
165
|
+
const files = names.map((name) => {
|
|
166
|
+
const stat = fs.statSync(path.join(dir, name));
|
|
167
|
+
return { name, bytes: stat.size, modified: stat.mtime.toISOString() };
|
|
168
|
+
});
|
|
169
|
+
const logs = { dir: '~/.hippo/logs', files };
|
|
170
|
+
if (opts.includeLogs) {
|
|
171
|
+
const tails = {};
|
|
172
|
+
for (const name of names)
|
|
173
|
+
tails[name] = tailLogFile(path.join(dir, name));
|
|
174
|
+
logs['tails'] = tails;
|
|
175
|
+
}
|
|
176
|
+
return logs;
|
|
177
|
+
}
|
|
178
|
+
function escapeRegExp(s) {
|
|
179
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
180
|
+
}
|
|
181
|
+
// The native realpath is the only form that exposes a short-name (8.3) or symlinked alias for what it is.
|
|
182
|
+
function nativeRealPathKey(p) {
|
|
183
|
+
try {
|
|
184
|
+
const real = fs.realpathSync.native(p);
|
|
185
|
+
return process.platform === 'win32' ? real.toLowerCase() : real;
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
// A path that does not exist has no alias to find.
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// Longest first: where one home form nests in another (a test home under the real one), swap the longer.
|
|
193
|
+
function collectHomeForms(home, probes) {
|
|
194
|
+
const forms = new Set();
|
|
195
|
+
const add = (s) => { if (s.length >= 3)
|
|
196
|
+
forms.add(s); };
|
|
197
|
+
for (const base of [home, realpathOrResolve(home), os.homedir(), realpathOrResolve(os.homedir())]) {
|
|
198
|
+
add(base);
|
|
199
|
+
add(path.resolve(base));
|
|
200
|
+
if (process.platform === 'win32') {
|
|
201
|
+
add(base.replace(/\\/g, '/'));
|
|
202
|
+
add(path.resolve(base).replace(/\\/g, '/'));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// The alias (e.g. a short-name temp root) can sit above the probe itself, so walk ancestors and
|
|
206
|
+
// compare native realpaths rather than checking each probe alone.
|
|
207
|
+
const homeKeys = new Set([nativeRealPathKey(home), nativeRealPathKey(os.homedir())].filter((k) => k !== null));
|
|
208
|
+
for (const probe of probes) {
|
|
209
|
+
let dir = path.resolve(probe);
|
|
210
|
+
for (;;) {
|
|
211
|
+
const key = nativeRealPathKey(dir);
|
|
212
|
+
if (key !== null && homeKeys.has(key)) {
|
|
213
|
+
add(dir);
|
|
214
|
+
if (process.platform === 'win32')
|
|
215
|
+
add(dir.replace(/\\/g, '/'));
|
|
216
|
+
}
|
|
217
|
+
const parent = path.dirname(dir);
|
|
218
|
+
if (parent === dir)
|
|
219
|
+
break;
|
|
220
|
+
dir = parent;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const seen = new Set();
|
|
224
|
+
const deduped = [];
|
|
225
|
+
for (const f of [...forms].sort((a, b) => b.length - a.length)) {
|
|
226
|
+
const key = process.platform === 'win32' ? f.toLowerCase() : f;
|
|
227
|
+
if (seen.has(key))
|
|
228
|
+
continue;
|
|
229
|
+
seen.add(key);
|
|
230
|
+
deduped.push(f);
|
|
231
|
+
}
|
|
232
|
+
return deduped;
|
|
233
|
+
}
|
|
234
|
+
function replaceHomeForms(text, forms) {
|
|
235
|
+
if (forms.length === 0)
|
|
236
|
+
return text;
|
|
237
|
+
// Stop only before another name character: "<home> now" and "<home>." swap, a longer user name ("<home>ty") does not.
|
|
238
|
+
const re = new RegExp(`(?:${forms.map(escapeRegExp).join('|')})(?![\\p{L}\\p{N}_])`, process.platform === 'win32' ? 'giu' : 'gu');
|
|
239
|
+
return text.replace(re, '~');
|
|
240
|
+
}
|
|
241
|
+
const URL_RE = /\b[a-z][a-z0-9+.-]*:\/\/[^\s"'<>]+/gi;
|
|
242
|
+
function redactUrlsInString(text) {
|
|
243
|
+
return text.replace(URL_RE, (match) => {
|
|
244
|
+
const scheme = match.slice(0, match.indexOf('://'));
|
|
245
|
+
try {
|
|
246
|
+
const url = new URL(match);
|
|
247
|
+
url.username = '';
|
|
248
|
+
url.password = '';
|
|
249
|
+
url.search = '';
|
|
250
|
+
url.hash = '';
|
|
251
|
+
return url.toString();
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
return `${scheme}://[REDACTED]`;
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
function redactString(text, homeForms) {
|
|
259
|
+
return replaceHomeForms(redactSecretsStrict(redactUrlsInString(text)), homeForms);
|
|
260
|
+
}
|
|
261
|
+
function redactStrings(value, homeForms) {
|
|
262
|
+
if (isJsonString(value))
|
|
263
|
+
return redactString(value, homeForms);
|
|
264
|
+
if (Array.isArray(value))
|
|
265
|
+
return value.map((v) => redactStrings(v, homeForms));
|
|
266
|
+
if (isJsonObject(value)) {
|
|
267
|
+
const out = {};
|
|
268
|
+
for (const [k, v] of Object.entries(value))
|
|
269
|
+
out[k] = redactStrings(v, homeForms);
|
|
270
|
+
return out;
|
|
271
|
+
}
|
|
272
|
+
return value;
|
|
273
|
+
}
|
|
274
|
+
/** Read-only: builds one redacted support-ticket snapshot. Never reads a memory content column. */
|
|
275
|
+
export function buildSupportBundle(opts) {
|
|
276
|
+
const bundle = {
|
|
277
|
+
format: 'hippo-support-bundle/1',
|
|
278
|
+
createdAt: opts.now.toISOString(),
|
|
279
|
+
hippo: opts.version,
|
|
280
|
+
runtime: buildRuntime(),
|
|
281
|
+
doctor: JSON.parse(JSON.stringify(runDoctor(opts))),
|
|
282
|
+
stores: buildStores(opts),
|
|
283
|
+
env: listSetEnvNames(),
|
|
284
|
+
logs: buildLogsSection(opts),
|
|
285
|
+
};
|
|
286
|
+
// Store paths found from the working folder arrive resolved; the global root arrives as HIPPO_HOME was typed, and log lines
|
|
287
|
+
// often quote the temp folder, which usually sits under the home.
|
|
288
|
+
const redacted = redactStrings(bundle, collectHomeForms(opts.home, [getGlobalRoot(), os.tmpdir()]));
|
|
289
|
+
// SAFETY: the value built above is always a JsonObject; redactStrings preserves object shape.
|
|
290
|
+
return redacted;
|
|
291
|
+
}
|
|
292
|
+
//# sourceMappingURL=support-bundle.js.map
|
package/dist/version.d.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* an ESM `import` can resolve cleanly, and a hardcoded constant survives
|
|
17
17
|
* any packager that drops .json files.
|
|
18
18
|
*/
|
|
19
|
-
export declare const PACKAGE_VERSION = "1.
|
|
19
|
+
export declare const PACKAGE_VERSION = "1.48.0";
|
|
20
20
|
/** Compares plain x.y.z versions, positive if a > b; tags throw so the rollback guard never misfires silently. */
|
|
21
21
|
export declare function compareSemver(a: string, b: string): number;
|
|
22
22
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/version.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* an ESM `import` can resolve cleanly, and a hardcoded constant survives
|
|
17
17
|
* any packager that drops .json files.
|
|
18
18
|
*/
|
|
19
|
-
export const PACKAGE_VERSION = '1.
|
|
19
|
+
export const PACKAGE_VERSION = '1.48.0';
|
|
20
20
|
/** Compares plain x.y.z versions, positive if a > b; tags throw so the rollback guard never misfires silently. */
|
|
21
21
|
export function compareSemver(a, b) {
|
|
22
22
|
const parse = (v) => {
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "hippo-memory",
|
|
3
3
|
"name": "Hippo Memory",
|
|
4
4
|
"description": "Biologically-inspired memory for AI agents. Decay by default, retrieval strengthening, sleep consolidation.",
|
|
5
|
-
"version": "1.
|
|
5
|
+
"version": "1.48.0",
|
|
6
6
|
"configSchema": {
|
|
7
7
|
"type": "object",
|
|
8
8
|
"additionalProperties": false,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hippo-memory",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.48.0",
|
|
4
4
|
"description": "Biologically-inspired memory for AI agents. Zero runtime deps, SQLite, MCP server, and an opt-in hosted TypeSafe Jev reranker. Decay, retrieval strengthening, consolidation.",
|
|
5
5
|
"mcpName": "io.github.kitfunso/hippo-memory",
|
|
6
6
|
"type": "module",
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"test": "vitest run",
|
|
35
35
|
"test:watch": "vitest",
|
|
36
36
|
"lint": "oxlint",
|
|
37
|
+
"sbom": "node scripts/sbom.mjs",
|
|
37
38
|
"audit:security": "npm audit --audit-level=high && npm --prefix ui audit --audit-level=high",
|
|
38
39
|
"postinstall": "node scripts/postinstall.cjs",
|
|
39
40
|
"smoke:pack": "node scripts/smoke-pack.mjs",
|