hippo-memory 0.34.0 → 0.36.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 +17 -0
- package/dist/api.d.ts +163 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +323 -0
- package/dist/api.js.map +1 -0
- package/dist/audit.d.ts +26 -0
- package/dist/audit.d.ts.map +1 -1
- package/dist/audit.js +45 -0
- package/dist/audit.js.map +1 -1
- package/dist/auth.d.ts +28 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +65 -0
- package/dist/auth.js.map +1 -0
- package/dist/cli.js +457 -19
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +54 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +181 -0
- package/dist/client.js.map +1 -0
- package/dist/dashboard.d.ts.map +1 -1
- package/dist/dashboard.js +5 -1
- package/dist/dashboard.js.map +1 -1
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +60 -1
- package/dist/db.js.map +1 -1
- package/dist/mcp/server.d.ts +46 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +81 -29
- package/dist/mcp/server.js.map +1 -1
- package/dist/memory.d.ts +2 -0
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +1 -0
- package/dist/memory.js.map +1 -1
- package/dist/raw-archive.d.ts.map +1 -1
- package/dist/raw-archive.js +19 -0
- package/dist/raw-archive.js.map +1 -1
- package/dist/server-detect.d.ts +26 -0
- package/dist/server-detect.d.ts.map +1 -0
- package/dist/server-detect.js +70 -0
- package/dist/server-detect.js.map +1 -0
- package/dist/server.d.ts +29 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +612 -0
- package/dist/server.js.map +1 -0
- package/dist/shared.d.ts +5 -1
- package/dist/shared.d.ts.map +1 -1
- package/dist/shared.js +8 -8
- package/dist/shared.js.map +1 -1
- package/dist/sso.d.ts +13 -0
- package/dist/sso.d.ts.map +1 -0
- package/dist/sso.js +22 -0
- package/dist/sso.js.map +1 -0
- package/dist/store.d.ts +29 -5
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +98 -22
- package/dist/store.js.map +1 -1
- package/dist/tenant.d.ts +7 -0
- package/dist/tenant.d.ts.map +1 -0
- package/dist/tenant.js +17 -0
- package/dist/tenant.js.map +1 -0
- package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
- package/extensions/openclaw-plugin/package.json +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -45,11 +45,16 @@ import { captureError, extractLessons, deduplicateLesson, runWatched, fetchGitLo
|
|
|
45
45
|
import { extractInvalidationTarget, invalidateMatching } from './invalidation.js';
|
|
46
46
|
import { extractPathTags } from './path-context.js';
|
|
47
47
|
import { detectScope, scopeMatch } from './scope.js';
|
|
48
|
-
import { getGlobalRoot, initGlobal,
|
|
48
|
+
import { getGlobalRoot, initGlobal, shareMemory, listPeers, autoShare, transferScore, searchBothHybrid, syncGlobalToLocal, } from './shared.js';
|
|
49
49
|
import { DAILY_TASK_NAME, buildDailyRunnerCommand, listRegisteredWorkspaces, registerWorkspace, runDailyMaintenance, } from './scheduler.js';
|
|
50
50
|
import { importChatGPT, importClaude, importCursor, importGenericFile, importMarkdown, } from './importers.js';
|
|
51
51
|
import { cmdCapture } from './capture.js';
|
|
52
|
-
import { auditMemories } from './audit.js';
|
|
52
|
+
import { auditMemories, appendAuditEvent, } from './audit.js';
|
|
53
|
+
import { listApiKeys, revokeApiKey } from './auth.js';
|
|
54
|
+
import * as api from './api.js';
|
|
55
|
+
import * as client from './client.js';
|
|
56
|
+
import { detectServer, removePidfile } from './server-detect.js';
|
|
57
|
+
import { resolveTenantId } from './tenant.js';
|
|
53
58
|
import { runEval, bootstrapCorpus, compareSummaries } from './eval.js';
|
|
54
59
|
import { runFeatureEval, formatResult, resultToBaseline, detectRegressions } from './eval-suite.js';
|
|
55
60
|
import { refineStore } from './refine-llm.js';
|
|
@@ -66,12 +71,66 @@ function parseLimitFlag(value) {
|
|
|
66
71
|
const parsed = parseInt(String(value), 10);
|
|
67
72
|
return Number.isFinite(parsed) && parsed >= 1 ? parsed : Infinity;
|
|
68
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* Emit an audit event against `hippoRoot`'s db. Opens its own short-lived
|
|
76
|
+
* connection so callers don't have to thread a db handle. Swallows all errors
|
|
77
|
+
* — audit must never crash a CLI command.
|
|
78
|
+
*/
|
|
79
|
+
function emitCliAudit(hippoRoot, op, targetId, metadata) {
|
|
80
|
+
try {
|
|
81
|
+
const db = openHippoDb(hippoRoot);
|
|
82
|
+
try {
|
|
83
|
+
appendAuditEvent(db, {
|
|
84
|
+
tenantId: resolveTenantId({}),
|
|
85
|
+
actor: 'cli',
|
|
86
|
+
op,
|
|
87
|
+
targetId,
|
|
88
|
+
metadata,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
closeHippoDb(db);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// Audit is best-effort; surface failures only via missing rows.
|
|
97
|
+
}
|
|
98
|
+
}
|
|
69
99
|
function requireInit(hippoRoot) {
|
|
70
100
|
if (!isInitialized(hippoRoot)) {
|
|
71
101
|
console.error('No .hippo directory found. Run `hippo init` first.');
|
|
72
102
|
process.exit(1);
|
|
73
103
|
}
|
|
74
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* Run an HTTP-routed command if a `hippo serve` instance is detected for
|
|
107
|
+
* `hippoRoot`. Returns:
|
|
108
|
+
* - true if the HTTP path ran (success OR a structured server error that
|
|
109
|
+
* was already surfaced to stdout/stderr by `httpFn`),
|
|
110
|
+
* - false if no server was detected, or if the detected pidfile turned out
|
|
111
|
+
* to be stale (connection refused). On stale, the pidfile is removed
|
|
112
|
+
* and the caller should fall back to the direct path.
|
|
113
|
+
*
|
|
114
|
+
* Per the A1 plan footgun #1: stale pidfiles must self-heal, not crash.
|
|
115
|
+
*/
|
|
116
|
+
async function runViaServerIfAvailable(hippoRoot, httpFn) {
|
|
117
|
+
const info = detectServer(hippoRoot);
|
|
118
|
+
if (!info)
|
|
119
|
+
return false;
|
|
120
|
+
const apiKey = process.env['HIPPO_API_KEY'];
|
|
121
|
+
try {
|
|
122
|
+
await httpFn(info, apiKey);
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
if (client.isConnectionRefused(err)) {
|
|
127
|
+
console.error('hippo: stale server pidfile detected, falling back to direct mode');
|
|
128
|
+
removePidfile(hippoRoot);
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
throw err;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
75
134
|
function parseArgs(argv) {
|
|
76
135
|
const [, , command = '', ...rest] = argv;
|
|
77
136
|
const args = [];
|
|
@@ -421,6 +480,9 @@ async function cmdRemember(hippoRoot, text, flags) {
|
|
|
421
480
|
const ownerFlag = typeof flags['owner'] === 'string' ? flags['owner'] : null;
|
|
422
481
|
const artifactRefFlag = typeof flags['artifact-ref'] === 'string' ? flags['artifact-ref'] : null;
|
|
423
482
|
const scopeForEnvelope = typeof flags['scope'] === 'string' ? flags['scope'].trim() || null : null;
|
|
483
|
+
// A5 stub auth: stamp tenant_id from env (HIPPO_TENANT) so recall isolation
|
|
484
|
+
// can filter on this row. Default tenant 'default' for unauthenticated CLI.
|
|
485
|
+
const tenantId = resolveTenantId({});
|
|
424
486
|
const entry = createMemory(text, {
|
|
425
487
|
layer: Layer.Episodic,
|
|
426
488
|
tags: rawTags,
|
|
@@ -432,6 +494,7 @@ async function cmdRemember(hippoRoot, text, flags) {
|
|
|
432
494
|
scope: scopeForEnvelope,
|
|
433
495
|
owner: ownerFlag,
|
|
434
496
|
artifact_ref: artifactRefFlag,
|
|
497
|
+
tenantId,
|
|
435
498
|
});
|
|
436
499
|
// Auto-tag with path context
|
|
437
500
|
const pathTags = extractPathTags(process.cwd());
|
|
@@ -533,6 +596,7 @@ function cmdSupersede(hippoRoot, oldId, newContent, flags) {
|
|
|
533
596
|
old.superseded_by = newEntry.id;
|
|
534
597
|
writeEntry(hippoRoot, old);
|
|
535
598
|
writeEntry(hippoRoot, newEntry);
|
|
599
|
+
emitCliAudit(hippoRoot, 'supersede', oldId, { newId: newEntry.id });
|
|
536
600
|
console.log(`Superseded ${oldId} → ${newEntry.id}`);
|
|
537
601
|
}
|
|
538
602
|
async function cmdRecall(hippoRoot, query, flags) {
|
|
@@ -550,8 +614,11 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
550
614
|
process.exit(1);
|
|
551
615
|
}
|
|
552
616
|
const globalRoot = getGlobalRoot();
|
|
553
|
-
|
|
554
|
-
|
|
617
|
+
// A5 stub auth: resolve the active tenant once and thread it through every
|
|
618
|
+
// recall-time SELECT against `memories`. Cross-tenant rows must never surface.
|
|
619
|
+
const tenantId = resolveTenantId({});
|
|
620
|
+
let localEntries = loadSearchEntries(hippoRoot, query, undefined, tenantId);
|
|
621
|
+
let globalEntries = isInitialized(globalRoot) ? loadSearchEntries(globalRoot, query, undefined, tenantId) : [];
|
|
555
622
|
// Bi-temporal filtering for physics path (hybridSearch handles it internally)
|
|
556
623
|
if (asOf) {
|
|
557
624
|
const filterAsOf = (entries) => {
|
|
@@ -625,7 +692,7 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
625
692
|
else if (hasGlobal) {
|
|
626
693
|
// Use searchBothHybrid for merged results with embedding support
|
|
627
694
|
results = await searchBothHybrid(query, hippoRoot, globalRoot, {
|
|
628
|
-
budget, mmr: mmrEnabled, mmrLambda, localBump, minResults, scope: recallActiveScope,
|
|
695
|
+
budget, mmr: mmrEnabled, mmrLambda, localBump, minResults, scope: recallActiveScope, tenantId,
|
|
629
696
|
});
|
|
630
697
|
}
|
|
631
698
|
else {
|
|
@@ -811,6 +878,20 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
811
878
|
if (limit < results.length) {
|
|
812
879
|
results = results.slice(0, limit);
|
|
813
880
|
}
|
|
881
|
+
// A5 audit: emit one 'recall' event per query, capturing the (truncated)
|
|
882
|
+
// query text and the post-filter result count. Tenant resolved by emitCliAudit.
|
|
883
|
+
// Emit before the early-empty return so zero-result recalls are still logged.
|
|
884
|
+
// recall reads from BOTH local and global stores when both are initialized;
|
|
885
|
+
// log against every participating store so the audit trail in either db
|
|
886
|
+
// shows the read access (no false negatives across --global flows).
|
|
887
|
+
const recallMetadata = {
|
|
888
|
+
query: query.slice(0, 200),
|
|
889
|
+
results: results.length,
|
|
890
|
+
};
|
|
891
|
+
emitCliAudit(hippoRoot, 'recall', undefined, recallMetadata);
|
|
892
|
+
if (isInitialized(globalRoot) && globalRoot !== hippoRoot) {
|
|
893
|
+
emitCliAudit(globalRoot, 'recall', undefined, recallMetadata);
|
|
894
|
+
}
|
|
814
895
|
if (results.length === 0) {
|
|
815
896
|
if (asJson) {
|
|
816
897
|
console.log(JSON.stringify({ query, results: [], total: 0 }));
|
|
@@ -916,8 +997,10 @@ async function cmdExplain(hippoRoot, query, flags) {
|
|
|
916
997
|
process.exit(1);
|
|
917
998
|
}
|
|
918
999
|
const globalRoot = getGlobalRoot();
|
|
919
|
-
|
|
920
|
-
|
|
1000
|
+
// A5: scope explain results to the active tenant.
|
|
1001
|
+
const tenantId = resolveTenantId({});
|
|
1002
|
+
let explainLocalEntries = loadSearchEntries(hippoRoot, query, undefined, tenantId);
|
|
1003
|
+
let explainGlobalEntries = isInitialized(globalRoot) ? loadSearchEntries(globalRoot, query, undefined, tenantId) : [];
|
|
921
1004
|
// Bi-temporal filtering
|
|
922
1005
|
if (explainAsOf) {
|
|
923
1006
|
const filterAsOfExplain = (entries) => {
|
|
@@ -977,7 +1060,7 @@ async function cmdExplain(hippoRoot, query, flags) {
|
|
|
977
1060
|
else if (hasGlobal) {
|
|
978
1061
|
results = await searchBothHybrid(query, hippoRoot, globalRoot, {
|
|
979
1062
|
budget, explain: true, mmr: mmrEnabled, mmrLambda, localBump, scope: explainActiveScope,
|
|
980
|
-
includeSuperseded: explainIncludeSuperseded, asOf: explainAsOf,
|
|
1063
|
+
includeSuperseded: explainIncludeSuperseded, asOf: explainAsOf, tenantId,
|
|
981
1064
|
});
|
|
982
1065
|
modeUsed = 'searchBothHybrid';
|
|
983
1066
|
}
|
|
@@ -2108,12 +2191,17 @@ function cmdOutcome(hippoRoot, flags) {
|
|
|
2108
2191
|
}
|
|
2109
2192
|
function cmdForget(hippoRoot, id) {
|
|
2110
2193
|
requireInit(hippoRoot);
|
|
2111
|
-
const
|
|
2112
|
-
|
|
2194
|
+
const ctx = {
|
|
2195
|
+
hippoRoot,
|
|
2196
|
+
tenantId: resolveTenantId({}),
|
|
2197
|
+
actor: 'cli',
|
|
2198
|
+
};
|
|
2199
|
+
try {
|
|
2200
|
+
api.forget(ctx, id);
|
|
2113
2201
|
updateStats(hippoRoot, { forgotten: 1 });
|
|
2114
2202
|
console.log(`Forgot ${id}`);
|
|
2115
2203
|
}
|
|
2116
|
-
|
|
2204
|
+
catch {
|
|
2117
2205
|
console.error(`Memory not found: ${id}`);
|
|
2118
2206
|
process.exit(1);
|
|
2119
2207
|
}
|
|
@@ -2621,11 +2709,14 @@ async function cmdContext(hippoRoot, args, flags) {
|
|
|
2621
2709
|
}
|
|
2622
2710
|
const globalRoot = getGlobalRoot();
|
|
2623
2711
|
const hasGlobal = isInitialized(globalRoot);
|
|
2712
|
+
// A5: scope context-mode loads to the active tenant. Without this, every
|
|
2713
|
+
// tenant's memories surface through the smart-context injection path.
|
|
2714
|
+
const tenantId = resolveTenantId({});
|
|
2624
2715
|
// When the local store isn't initialized (pinned-only path in a fresh dir),
|
|
2625
2716
|
// skip the local load — loadAllEntries would auto-create .hippo here and
|
|
2626
2717
|
// we don't want to pollute arbitrary cwds.
|
|
2627
|
-
let localEntries = hasLocal ? loadAllEntries(hippoRoot) : [];
|
|
2628
|
-
let globalEntries = hasGlobal ? loadAllEntries(globalRoot) : [];
|
|
2718
|
+
let localEntries = hasLocal ? loadAllEntries(hippoRoot, tenantId) : [];
|
|
2719
|
+
let globalEntries = hasGlobal ? loadAllEntries(globalRoot, tenantId) : [];
|
|
2629
2720
|
// Default context always filters superseded (no --include-superseded / --as-of for context)
|
|
2630
2721
|
localEntries = localEntries.filter(e => !e.superseded_by);
|
|
2631
2722
|
globalEntries = globalEntries.filter(e => !e.superseded_by);
|
|
@@ -2714,7 +2805,7 @@ async function cmdContext(hippoRoot, args, flags) {
|
|
|
2714
2805
|
else {
|
|
2715
2806
|
let results;
|
|
2716
2807
|
if (hasGlobal) {
|
|
2717
|
-
const merged = await searchBothHybrid(query, hippoRoot, globalRoot, { budget, scope: ctxActiveScope });
|
|
2808
|
+
const merged = await searchBothHybrid(query, hippoRoot, globalRoot, { budget, scope: ctxActiveScope, tenantId });
|
|
2718
2809
|
const localIndex = loadIndex(hippoRoot);
|
|
2719
2810
|
results = merged.map((r) => ({
|
|
2720
2811
|
entry: r.entry,
|
|
@@ -2738,6 +2829,20 @@ async function cmdContext(hippoRoot, args, flags) {
|
|
|
2738
2829
|
}
|
|
2739
2830
|
selectedItems = results;
|
|
2740
2831
|
totalTokens = results.reduce((sum, r) => sum + r.tokens, 0);
|
|
2832
|
+
// A5 H4: emit recall audit event for context-mode searches. The recall
|
|
2833
|
+
// handler emits one of these per `hippo recall` invocation; context mode
|
|
2834
|
+
// is the same surface (search → user) and must leave the same audit trail.
|
|
2835
|
+
// Skip pinned-only and '*' fallback (handled in branches above which never
|
|
2836
|
+
// hit the search engines).
|
|
2837
|
+
const ctxRecallMetadata = {
|
|
2838
|
+
query: query.slice(0, 200),
|
|
2839
|
+
results: selectedItems.length,
|
|
2840
|
+
mode: 'context',
|
|
2841
|
+
};
|
|
2842
|
+
if (hasLocal)
|
|
2843
|
+
emitCliAudit(hippoRoot, 'recall', undefined, ctxRecallMetadata);
|
|
2844
|
+
if (hasGlobal)
|
|
2845
|
+
emitCliAudit(globalRoot, 'recall', undefined, ctxRecallMetadata);
|
|
2741
2846
|
}
|
|
2742
2847
|
if (limit < selectedItems.length) {
|
|
2743
2848
|
selectedItems = selectedItems.slice(0, limit);
|
|
@@ -3170,9 +3275,14 @@ function cmdPromote(hippoRoot, id) {
|
|
|
3170
3275
|
console.error('Usage: hippo promote <id>');
|
|
3171
3276
|
process.exit(1);
|
|
3172
3277
|
}
|
|
3278
|
+
const ctx = {
|
|
3279
|
+
hippoRoot,
|
|
3280
|
+
tenantId: resolveTenantId({}),
|
|
3281
|
+
actor: 'cli',
|
|
3282
|
+
};
|
|
3173
3283
|
try {
|
|
3174
|
-
const
|
|
3175
|
-
console.log(`Promoted ${id} to global store as ${
|
|
3284
|
+
const result = api.promote(ctx, id);
|
|
3285
|
+
console.log(`Promoted ${id} to global store as ${result.globalId}`);
|
|
3176
3286
|
console.log(` Global store: ${getGlobalRoot()}`);
|
|
3177
3287
|
}
|
|
3178
3288
|
catch (err) {
|
|
@@ -3725,6 +3835,226 @@ function cmdDag(hippoRoot, flags) {
|
|
|
3725
3835
|
}
|
|
3726
3836
|
}
|
|
3727
3837
|
}
|
|
3838
|
+
// ---------------------------------------------------------------------------
|
|
3839
|
+
// Auth subcommands (A5 stub auth)
|
|
3840
|
+
// ---------------------------------------------------------------------------
|
|
3841
|
+
function resolveAuthRoot(hippoRoot, flags) {
|
|
3842
|
+
if (flags['global']) {
|
|
3843
|
+
initGlobal();
|
|
3844
|
+
return getGlobalRoot();
|
|
3845
|
+
}
|
|
3846
|
+
requireInit(hippoRoot);
|
|
3847
|
+
return hippoRoot;
|
|
3848
|
+
}
|
|
3849
|
+
function cmdAuthCreate(hippoRoot, flags) {
|
|
3850
|
+
const root = resolveAuthRoot(hippoRoot, flags);
|
|
3851
|
+
const tenantFlag = typeof flags['tenant'] === 'string' ? flags['tenant'] : undefined;
|
|
3852
|
+
const labelFlag = typeof flags['label'] === 'string' ? flags['label'] : undefined;
|
|
3853
|
+
const asJson = Boolean(flags['json']);
|
|
3854
|
+
const ctx = {
|
|
3855
|
+
hippoRoot: root,
|
|
3856
|
+
tenantId: resolveTenantId({}),
|
|
3857
|
+
actor: 'cli',
|
|
3858
|
+
};
|
|
3859
|
+
const result = api.authCreate(ctx, { tenantId: tenantFlag, label: labelFlag });
|
|
3860
|
+
if (asJson) {
|
|
3861
|
+
console.log(JSON.stringify({
|
|
3862
|
+
keyId: result.keyId,
|
|
3863
|
+
plaintext: result.plaintext,
|
|
3864
|
+
tenantId: result.tenantId,
|
|
3865
|
+
label: labelFlag ?? null,
|
|
3866
|
+
}));
|
|
3867
|
+
return;
|
|
3868
|
+
}
|
|
3869
|
+
console.log(`key_id: ${result.keyId}`);
|
|
3870
|
+
console.log(`plaintext: ${result.plaintext}`);
|
|
3871
|
+
console.log('');
|
|
3872
|
+
console.log('!! WARNING: this is the ONLY time the plaintext key will be shown. !!');
|
|
3873
|
+
console.log('!! Copy it now. Hippo stores only a scrypt hash and cannot recover it. !!');
|
|
3874
|
+
}
|
|
3875
|
+
function formatKeyRow(item) {
|
|
3876
|
+
const label = item.label ?? '-';
|
|
3877
|
+
const created = item.createdAt;
|
|
3878
|
+
const revoked = item.revokedAt ?? '-';
|
|
3879
|
+
return `${item.keyId} ${item.tenantId} ${label} ${created} ${revoked}`;
|
|
3880
|
+
}
|
|
3881
|
+
function cmdAuthList(hippoRoot, flags) {
|
|
3882
|
+
const root = resolveAuthRoot(hippoRoot, flags);
|
|
3883
|
+
const includeRevoked = Boolean(flags['all']);
|
|
3884
|
+
const asJson = Boolean(flags['json']);
|
|
3885
|
+
const db = openHippoDb(root);
|
|
3886
|
+
let items;
|
|
3887
|
+
try {
|
|
3888
|
+
items = listApiKeys(db, { active: !includeRevoked });
|
|
3889
|
+
}
|
|
3890
|
+
finally {
|
|
3891
|
+
closeHippoDb(db);
|
|
3892
|
+
}
|
|
3893
|
+
if (asJson) {
|
|
3894
|
+
console.log(JSON.stringify(items));
|
|
3895
|
+
return;
|
|
3896
|
+
}
|
|
3897
|
+
if (items.length === 0) {
|
|
3898
|
+
console.log(includeRevoked ? 'No API keys.' : 'No active API keys. (Use --all to include revoked.)');
|
|
3899
|
+
return;
|
|
3900
|
+
}
|
|
3901
|
+
console.log('key_id tenant label created revoked');
|
|
3902
|
+
for (const item of items) {
|
|
3903
|
+
console.log(formatKeyRow(item));
|
|
3904
|
+
}
|
|
3905
|
+
}
|
|
3906
|
+
function cmdAuthRevoke(hippoRoot, keyId, flags) {
|
|
3907
|
+
const root = resolveAuthRoot(hippoRoot, flags);
|
|
3908
|
+
const asJson = Boolean(flags['json']);
|
|
3909
|
+
const db = openHippoDb(root);
|
|
3910
|
+
let exists = false;
|
|
3911
|
+
let alreadyRevoked = false;
|
|
3912
|
+
let revokedAt = null;
|
|
3913
|
+
let keyTenantId = null;
|
|
3914
|
+
try {
|
|
3915
|
+
const row = db.prepare(`SELECT key_id, tenant_id, revoked_at FROM api_keys WHERE key_id = ?`).get(keyId);
|
|
3916
|
+
if (!row) {
|
|
3917
|
+
// Let the finally{} block close the db. M4: avoid manual close before
|
|
3918
|
+
// process.exit() — the finally already handles it on every path.
|
|
3919
|
+
console.error(`Unknown key_id: ${keyId}`);
|
|
3920
|
+
process.exit(1);
|
|
3921
|
+
}
|
|
3922
|
+
exists = true;
|
|
3923
|
+
keyTenantId = row.tenant_id;
|
|
3924
|
+
if (row.revoked_at) {
|
|
3925
|
+
alreadyRevoked = true;
|
|
3926
|
+
revokedAt = row.revoked_at;
|
|
3927
|
+
}
|
|
3928
|
+
else {
|
|
3929
|
+
revokeApiKey(db, keyId);
|
|
3930
|
+
const updated = db.prepare(`SELECT revoked_at FROM api_keys WHERE key_id = ?`).get(keyId);
|
|
3931
|
+
revokedAt = updated?.revoked_at ?? null;
|
|
3932
|
+
}
|
|
3933
|
+
// M1: emit auth_revoke audit event. Skip on no-op revoke (already revoked)
|
|
3934
|
+
// so re-running the command doesn't pad the audit log with duplicates.
|
|
3935
|
+
if (!alreadyRevoked && keyTenantId) {
|
|
3936
|
+
try {
|
|
3937
|
+
appendAuditEvent(db, {
|
|
3938
|
+
tenantId: keyTenantId,
|
|
3939
|
+
actor: 'cli',
|
|
3940
|
+
op: 'auth_revoke',
|
|
3941
|
+
targetId: keyId,
|
|
3942
|
+
});
|
|
3943
|
+
}
|
|
3944
|
+
catch {
|
|
3945
|
+
// Audit must not crash a successful revoke.
|
|
3946
|
+
}
|
|
3947
|
+
}
|
|
3948
|
+
}
|
|
3949
|
+
finally {
|
|
3950
|
+
closeHippoDb(db);
|
|
3951
|
+
}
|
|
3952
|
+
if (!exists)
|
|
3953
|
+
return;
|
|
3954
|
+
if (asJson) {
|
|
3955
|
+
console.log(JSON.stringify({ keyId, revokedAt }));
|
|
3956
|
+
return;
|
|
3957
|
+
}
|
|
3958
|
+
console.log(`Revoked ${keyId} at ${revokedAt}`);
|
|
3959
|
+
}
|
|
3960
|
+
// ---------------------------------------------------------------------------
|
|
3961
|
+
// Audit log subcommands (A5 stub auth — `hippo audit list`)
|
|
3962
|
+
// ---------------------------------------------------------------------------
|
|
3963
|
+
const VALID_AUDIT_OPS = new Set([
|
|
3964
|
+
'remember',
|
|
3965
|
+
'recall',
|
|
3966
|
+
'promote',
|
|
3967
|
+
'supersede',
|
|
3968
|
+
'forget',
|
|
3969
|
+
'archive_raw',
|
|
3970
|
+
'auth_revoke',
|
|
3971
|
+
]);
|
|
3972
|
+
function formatAuditRow(ev) {
|
|
3973
|
+
const target = ev.targetId ?? '-';
|
|
3974
|
+
const meta = JSON.stringify(ev.metadata ?? {});
|
|
3975
|
+
return `${ev.ts} ${ev.actor} ${ev.op} ${target} ${meta}`;
|
|
3976
|
+
}
|
|
3977
|
+
function cmdAuditList(hippoRoot, flags) {
|
|
3978
|
+
const root = resolveAuthRoot(hippoRoot, flags);
|
|
3979
|
+
const asJson = Boolean(flags['json']);
|
|
3980
|
+
const tenantId = resolveTenantId({});
|
|
3981
|
+
const opFlag = typeof flags['op'] === 'string' ? flags['op'] : undefined;
|
|
3982
|
+
if (opFlag && !VALID_AUDIT_OPS.has(opFlag)) {
|
|
3983
|
+
console.error(`Unknown --op value: ${opFlag}. Expected one of: remember | recall | promote | supersede | forget | archive_raw.`);
|
|
3984
|
+
process.exit(1);
|
|
3985
|
+
}
|
|
3986
|
+
const op = opFlag;
|
|
3987
|
+
const since = typeof flags['since'] === 'string' ? flags['since'] : undefined;
|
|
3988
|
+
if (since !== undefined && !Number.isFinite(new Date(since).getTime())) {
|
|
3989
|
+
console.error(`Invalid --since: ${since} (expected an ISO timestamp like 2026-04-22 or 2026-04-22T12:00:00Z).`);
|
|
3990
|
+
process.exit(1);
|
|
3991
|
+
}
|
|
3992
|
+
const limitRaw = flags['limit'];
|
|
3993
|
+
let limit = 100;
|
|
3994
|
+
if (limitRaw !== undefined && typeof limitRaw !== 'boolean') {
|
|
3995
|
+
const parsed = parseInt(String(limitRaw), 10);
|
|
3996
|
+
if (!Number.isFinite(parsed)) {
|
|
3997
|
+
console.error(`Invalid --limit value: ${String(limitRaw)} (expected a positive integer).`);
|
|
3998
|
+
process.exit(1);
|
|
3999
|
+
}
|
|
4000
|
+
limit = parsed;
|
|
4001
|
+
}
|
|
4002
|
+
if (limit < 1 || limit > 10000) {
|
|
4003
|
+
console.error(`--limit must be between 1 and 10000 (got ${limit}).`);
|
|
4004
|
+
process.exit(1);
|
|
4005
|
+
}
|
|
4006
|
+
const ctx = { hippoRoot: root, tenantId, actor: 'cli' };
|
|
4007
|
+
const events = api.auditList(ctx, { op, since, limit });
|
|
4008
|
+
if (asJson) {
|
|
4009
|
+
console.log(JSON.stringify(events));
|
|
4010
|
+
return;
|
|
4011
|
+
}
|
|
4012
|
+
if (events.length === 0) {
|
|
4013
|
+
console.log('No audit events.');
|
|
4014
|
+
return;
|
|
4015
|
+
}
|
|
4016
|
+
console.log('ts actor op target_id metadata');
|
|
4017
|
+
for (const ev of events) {
|
|
4018
|
+
console.log(formatAuditRow(ev));
|
|
4019
|
+
}
|
|
4020
|
+
}
|
|
4021
|
+
function cmdAuditLog(hippoRoot, args, flags) {
|
|
4022
|
+
const sub = args[0];
|
|
4023
|
+
if (sub === 'list') {
|
|
4024
|
+
cmdAuditList(hippoRoot, flags);
|
|
4025
|
+
return;
|
|
4026
|
+
}
|
|
4027
|
+
console.error(`Unknown audit subcommand: ${sub}. Expected: list.`);
|
|
4028
|
+
process.exit(1);
|
|
4029
|
+
}
|
|
4030
|
+
function cmdAuth(hippoRoot, args, flags) {
|
|
4031
|
+
const sub = args[0];
|
|
4032
|
+
if (!sub) {
|
|
4033
|
+
console.error('Usage: hippo auth <create|list|revoke> [options]');
|
|
4034
|
+
process.exit(1);
|
|
4035
|
+
}
|
|
4036
|
+
const subArgs = args.slice(1);
|
|
4037
|
+
switch (sub) {
|
|
4038
|
+
case 'create':
|
|
4039
|
+
cmdAuthCreate(hippoRoot, flags);
|
|
4040
|
+
return;
|
|
4041
|
+
case 'list':
|
|
4042
|
+
cmdAuthList(hippoRoot, flags);
|
|
4043
|
+
return;
|
|
4044
|
+
case 'revoke': {
|
|
4045
|
+
const keyId = subArgs[0];
|
|
4046
|
+
if (!keyId) {
|
|
4047
|
+
console.error('Usage: hippo auth revoke <key_id>');
|
|
4048
|
+
process.exit(1);
|
|
4049
|
+
}
|
|
4050
|
+
cmdAuthRevoke(hippoRoot, keyId, flags);
|
|
4051
|
+
return;
|
|
4052
|
+
}
|
|
4053
|
+
default:
|
|
4054
|
+
console.error(`Unknown auth subcommand: ${sub}. Expected: create | list | revoke.`);
|
|
4055
|
+
process.exit(1);
|
|
4056
|
+
}
|
|
4057
|
+
}
|
|
3728
4058
|
function printUsage() {
|
|
3729
4059
|
console.log(`
|
|
3730
4060
|
Hippo - biologically-inspired memory system for AI agents
|
|
@@ -3961,6 +4291,27 @@ Commands:
|
|
|
3961
4291
|
dashboard Open web dashboard for memory health
|
|
3962
4292
|
--port <n> Port to serve on (default: 3333)
|
|
3963
4293
|
mcp Start MCP server (stdio transport)
|
|
4294
|
+
auth <sub> Manage API keys (A5 stub auth)
|
|
4295
|
+
auth create Mint a new API key (plaintext shown ONCE)
|
|
4296
|
+
--label <s> Optional human label
|
|
4297
|
+
--tenant <id> Override tenant (defaults to HIPPO_TENANT)
|
|
4298
|
+
--json Output as JSON
|
|
4299
|
+
--global Operate on the global store
|
|
4300
|
+
auth list List API keys (active by default)
|
|
4301
|
+
--all Include revoked keys
|
|
4302
|
+
--json Output as JSON
|
|
4303
|
+
--global Operate on the global store
|
|
4304
|
+
auth revoke <key_id> Revoke an API key (subsequent validate fails)
|
|
4305
|
+
--json Output as JSON
|
|
4306
|
+
--global Operate on the global store
|
|
4307
|
+
audit <sub> Query the append-only audit log (A5 stub auth)
|
|
4308
|
+
audit list List audit events for the active tenant
|
|
4309
|
+
--op <op> Filter by op (remember | recall | promote |
|
|
4310
|
+
supersede | forget | archive_raw | auth_revoke)
|
|
4311
|
+
--since <iso> Lower bound on ts (ISO timestamp)
|
|
4312
|
+
--limit <n> Max events (default: 100, max: 10000)
|
|
4313
|
+
--json Output as JSON
|
|
4314
|
+
--global Operate on the global store
|
|
3964
4315
|
|
|
3965
4316
|
Examples:
|
|
3966
4317
|
hippo init
|
|
@@ -4027,6 +4378,37 @@ async function main() {
|
|
|
4027
4378
|
console.error('Memory content too short (minimum 3 characters).');
|
|
4028
4379
|
process.exit(1);
|
|
4029
4380
|
}
|
|
4381
|
+
// Thin-client routing. When a server is up, simple `remember` calls go
|
|
4382
|
+
// over HTTP so the daemon stays single-writer (footgun #2). Rich CLI
|
|
4383
|
+
// flags (--pin, --layer, --extract, --global, salience gates) still
|
|
4384
|
+
// need the direct path; we only intercept the minimal envelope.
|
|
4385
|
+
const richFlag = flags['pin'] || flags['global'] || flags['extract'] || flags['force'] ||
|
|
4386
|
+
flags['observed'] || flags['inferred'] || flags['verified'] ||
|
|
4387
|
+
flags['layer'] !== undefined;
|
|
4388
|
+
if (!richFlag) {
|
|
4389
|
+
const rememberKindRaw = typeof flags['kind'] === 'string' ? flags['kind'].toLowerCase() : undefined;
|
|
4390
|
+
const rememberKindAllowed = ['distilled', 'superseded'];
|
|
4391
|
+
if (rememberKindRaw === undefined || rememberKindAllowed.includes(rememberKindRaw)) {
|
|
4392
|
+
const tagsRaw = flags['tag'];
|
|
4393
|
+
const tags = Array.isArray(tagsRaw)
|
|
4394
|
+
? tagsRaw.map(String)
|
|
4395
|
+
: typeof tagsRaw === 'string' ? [tagsRaw] : undefined;
|
|
4396
|
+
const remembered = await runViaServerIfAvailable(hippoRoot, async (info, apiKey) => {
|
|
4397
|
+
const result = await client.remember(info.url, apiKey, {
|
|
4398
|
+
content: text,
|
|
4399
|
+
kind: rememberKindRaw,
|
|
4400
|
+
scope: typeof flags['scope'] === 'string' ? flags['scope'] : undefined,
|
|
4401
|
+
owner: typeof flags['owner'] === 'string' ? flags['owner'] : undefined,
|
|
4402
|
+
artifactRef: typeof flags['artifact-ref'] === 'string' ? flags['artifact-ref'] : undefined,
|
|
4403
|
+
tags,
|
|
4404
|
+
});
|
|
4405
|
+
console.log(`Remembered [${result.id}] (via ${info.url})`);
|
|
4406
|
+
console.log(` Kind: ${result.kind} | Tenant: ${result.tenantId}`);
|
|
4407
|
+
});
|
|
4408
|
+
if (remembered)
|
|
4409
|
+
break;
|
|
4410
|
+
}
|
|
4411
|
+
}
|
|
4030
4412
|
await cmdRemember(hippoRoot, text, flags);
|
|
4031
4413
|
break;
|
|
4032
4414
|
}
|
|
@@ -4103,7 +4485,16 @@ async function main() {
|
|
|
4103
4485
|
case 'dag':
|
|
4104
4486
|
cmdDag(hippoRoot, flags);
|
|
4105
4487
|
break;
|
|
4488
|
+
case 'auth':
|
|
4489
|
+
cmdAuth(hippoRoot, args, flags);
|
|
4490
|
+
break;
|
|
4106
4491
|
case 'audit': {
|
|
4492
|
+
// `audit list` -> A5 audit-log viewer. Other forms (no sub, --fix) keep
|
|
4493
|
+
// the existing memory-quality auditor for backwards compatibility.
|
|
4494
|
+
if (args[0] === 'list') {
|
|
4495
|
+
cmdAuditLog(hippoRoot, args, flags);
|
|
4496
|
+
break;
|
|
4497
|
+
}
|
|
4107
4498
|
requireInit(hippoRoot);
|
|
4108
4499
|
const entries = loadAllEntries(hippoRoot);
|
|
4109
4500
|
const result = auditMemories(entries);
|
|
@@ -4167,6 +4558,18 @@ async function main() {
|
|
|
4167
4558
|
console.error('Please provide a memory ID.');
|
|
4168
4559
|
process.exit(1);
|
|
4169
4560
|
}
|
|
4561
|
+
const routed = await runViaServerIfAvailable(hippoRoot, async (info, apiKey) => {
|
|
4562
|
+
try {
|
|
4563
|
+
await client.forget(info.url, apiKey, id);
|
|
4564
|
+
console.log(`Forgot ${id}`);
|
|
4565
|
+
}
|
|
4566
|
+
catch (err) {
|
|
4567
|
+
console.error(err.message);
|
|
4568
|
+
process.exit(1);
|
|
4569
|
+
}
|
|
4570
|
+
});
|
|
4571
|
+
if (routed)
|
|
4572
|
+
break;
|
|
4170
4573
|
cmdForget(hippoRoot, id);
|
|
4171
4574
|
break;
|
|
4172
4575
|
}
|
|
@@ -4208,6 +4611,18 @@ async function main() {
|
|
|
4208
4611
|
console.error('Please provide a memory ID.');
|
|
4209
4612
|
process.exit(1);
|
|
4210
4613
|
}
|
|
4614
|
+
const promoted = await runViaServerIfAvailable(hippoRoot, async (info, apiKey) => {
|
|
4615
|
+
try {
|
|
4616
|
+
const result = await client.promote(info.url, apiKey, id);
|
|
4617
|
+
console.log(`Promoted ${id} to global store as ${result.globalId}`);
|
|
4618
|
+
}
|
|
4619
|
+
catch (err) {
|
|
4620
|
+
console.error(`Failed to promote: ${err.message}`);
|
|
4621
|
+
process.exit(1);
|
|
4622
|
+
}
|
|
4623
|
+
});
|
|
4624
|
+
if (promoted)
|
|
4625
|
+
break;
|
|
4211
4626
|
cmdPromote(hippoRoot, id);
|
|
4212
4627
|
break;
|
|
4213
4628
|
}
|
|
@@ -4357,12 +4772,35 @@ async function main() {
|
|
|
4357
4772
|
case 'wm':
|
|
4358
4773
|
cmdWm(hippoRoot, args, flags);
|
|
4359
4774
|
break;
|
|
4360
|
-
case 'mcp':
|
|
4361
|
-
// Start MCP server over stdio
|
|
4362
|
-
|
|
4775
|
+
case 'mcp': {
|
|
4776
|
+
// Start MCP server over stdio. Dynamic import keeps main CLI lean; the
|
|
4777
|
+
// dispatcher itself is transport-agnostic, so we explicitly attach the
|
|
4778
|
+
// stdio loop here. (HTTP/SSE transport is wired in src/server.ts and
|
|
4779
|
+
// imports the same module without triggering stdin handlers.)
|
|
4780
|
+
const mod = await import('./mcp/server.js');
|
|
4781
|
+
mod.startStdioLoop();
|
|
4363
4782
|
// Server runs until stdin closes, so we never reach here
|
|
4364
4783
|
await new Promise(() => { }); // hang forever
|
|
4365
4784
|
break;
|
|
4785
|
+
}
|
|
4786
|
+
case 'serve': {
|
|
4787
|
+
requireInit(hippoRoot);
|
|
4788
|
+
const portRaw = flags['port'] ?? process.env['HIPPO_PORT'] ?? '6789';
|
|
4789
|
+
const port = Number(portRaw);
|
|
4790
|
+
if (!Number.isFinite(port) || port < 0) {
|
|
4791
|
+
console.error(`Invalid --port: ${String(portRaw)}`);
|
|
4792
|
+
process.exit(1);
|
|
4793
|
+
}
|
|
4794
|
+
const host = typeof flags['host'] === 'string' ? flags['host'] : '127.0.0.1';
|
|
4795
|
+
const { serve } = await import('./server.js');
|
|
4796
|
+
const handle = await serve({ hippoRoot, port, host });
|
|
4797
|
+
console.log(`hippo serve listening on ${handle.url} (pid ${process.pid})`);
|
|
4798
|
+
console.log(`pidfile: ${path.join(hippoRoot, 'server.pid')}`);
|
|
4799
|
+
console.log('press Ctrl+C to stop');
|
|
4800
|
+
// SIGINT/SIGTERM handlers wired in server.ts (skipped under VITEST). Hang.
|
|
4801
|
+
await new Promise(() => { });
|
|
4802
|
+
break;
|
|
4803
|
+
}
|
|
4366
4804
|
case 'invalidate': {
|
|
4367
4805
|
requireInit(hippoRoot);
|
|
4368
4806
|
const target = args[0];
|