hippo-memory 0.34.0 → 0.35.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 +8 -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 +342 -10
- package/dist/cli.js.map +1 -1
- 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.js +9 -5
- 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/shared.d.ts +2 -0
- package/dist/shared.d.ts.map +1 -1
- package/dist/shared.js +6 -6
- 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 +14 -3
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +83 -18
- 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
|
@@ -49,7 +49,9 @@ import { getGlobalRoot, initGlobal, promoteToGlobal, shareMemory, listPeers, aut
|
|
|
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, queryAuditEvents, } from './audit.js';
|
|
53
|
+
import { createApiKey, listApiKeys, revokeApiKey } from './auth.js';
|
|
54
|
+
import { resolveTenantId } from './tenant.js';
|
|
53
55
|
import { runEval, bootstrapCorpus, compareSummaries } from './eval.js';
|
|
54
56
|
import { runFeatureEval, formatResult, resultToBaseline, detectRegressions } from './eval-suite.js';
|
|
55
57
|
import { refineStore } from './refine-llm.js';
|
|
@@ -66,6 +68,31 @@ function parseLimitFlag(value) {
|
|
|
66
68
|
const parsed = parseInt(String(value), 10);
|
|
67
69
|
return Number.isFinite(parsed) && parsed >= 1 ? parsed : Infinity;
|
|
68
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* Emit an audit event against `hippoRoot`'s db. Opens its own short-lived
|
|
73
|
+
* connection so callers don't have to thread a db handle. Swallows all errors
|
|
74
|
+
* — audit must never crash a CLI command.
|
|
75
|
+
*/
|
|
76
|
+
function emitCliAudit(hippoRoot, op, targetId, metadata) {
|
|
77
|
+
try {
|
|
78
|
+
const db = openHippoDb(hippoRoot);
|
|
79
|
+
try {
|
|
80
|
+
appendAuditEvent(db, {
|
|
81
|
+
tenantId: resolveTenantId({}),
|
|
82
|
+
actor: 'cli',
|
|
83
|
+
op,
|
|
84
|
+
targetId,
|
|
85
|
+
metadata,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
closeHippoDb(db);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// Audit is best-effort; surface failures only via missing rows.
|
|
94
|
+
}
|
|
95
|
+
}
|
|
69
96
|
function requireInit(hippoRoot) {
|
|
70
97
|
if (!isInitialized(hippoRoot)) {
|
|
71
98
|
console.error('No .hippo directory found. Run `hippo init` first.');
|
|
@@ -421,6 +448,9 @@ async function cmdRemember(hippoRoot, text, flags) {
|
|
|
421
448
|
const ownerFlag = typeof flags['owner'] === 'string' ? flags['owner'] : null;
|
|
422
449
|
const artifactRefFlag = typeof flags['artifact-ref'] === 'string' ? flags['artifact-ref'] : null;
|
|
423
450
|
const scopeForEnvelope = typeof flags['scope'] === 'string' ? flags['scope'].trim() || null : null;
|
|
451
|
+
// A5 stub auth: stamp tenant_id from env (HIPPO_TENANT) so recall isolation
|
|
452
|
+
// can filter on this row. Default tenant 'default' for unauthenticated CLI.
|
|
453
|
+
const tenantId = resolveTenantId({});
|
|
424
454
|
const entry = createMemory(text, {
|
|
425
455
|
layer: Layer.Episodic,
|
|
426
456
|
tags: rawTags,
|
|
@@ -432,6 +462,7 @@ async function cmdRemember(hippoRoot, text, flags) {
|
|
|
432
462
|
scope: scopeForEnvelope,
|
|
433
463
|
owner: ownerFlag,
|
|
434
464
|
artifact_ref: artifactRefFlag,
|
|
465
|
+
tenantId,
|
|
435
466
|
});
|
|
436
467
|
// Auto-tag with path context
|
|
437
468
|
const pathTags = extractPathTags(process.cwd());
|
|
@@ -533,6 +564,7 @@ function cmdSupersede(hippoRoot, oldId, newContent, flags) {
|
|
|
533
564
|
old.superseded_by = newEntry.id;
|
|
534
565
|
writeEntry(hippoRoot, old);
|
|
535
566
|
writeEntry(hippoRoot, newEntry);
|
|
567
|
+
emitCliAudit(hippoRoot, 'supersede', oldId, { newId: newEntry.id });
|
|
536
568
|
console.log(`Superseded ${oldId} → ${newEntry.id}`);
|
|
537
569
|
}
|
|
538
570
|
async function cmdRecall(hippoRoot, query, flags) {
|
|
@@ -550,8 +582,11 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
550
582
|
process.exit(1);
|
|
551
583
|
}
|
|
552
584
|
const globalRoot = getGlobalRoot();
|
|
553
|
-
|
|
554
|
-
|
|
585
|
+
// A5 stub auth: resolve the active tenant once and thread it through every
|
|
586
|
+
// recall-time SELECT against `memories`. Cross-tenant rows must never surface.
|
|
587
|
+
const tenantId = resolveTenantId({});
|
|
588
|
+
let localEntries = loadSearchEntries(hippoRoot, query, undefined, tenantId);
|
|
589
|
+
let globalEntries = isInitialized(globalRoot) ? loadSearchEntries(globalRoot, query, undefined, tenantId) : [];
|
|
555
590
|
// Bi-temporal filtering for physics path (hybridSearch handles it internally)
|
|
556
591
|
if (asOf) {
|
|
557
592
|
const filterAsOf = (entries) => {
|
|
@@ -625,7 +660,7 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
625
660
|
else if (hasGlobal) {
|
|
626
661
|
// Use searchBothHybrid for merged results with embedding support
|
|
627
662
|
results = await searchBothHybrid(query, hippoRoot, globalRoot, {
|
|
628
|
-
budget, mmr: mmrEnabled, mmrLambda, localBump, minResults, scope: recallActiveScope,
|
|
663
|
+
budget, mmr: mmrEnabled, mmrLambda, localBump, minResults, scope: recallActiveScope, tenantId,
|
|
629
664
|
});
|
|
630
665
|
}
|
|
631
666
|
else {
|
|
@@ -811,6 +846,20 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
811
846
|
if (limit < results.length) {
|
|
812
847
|
results = results.slice(0, limit);
|
|
813
848
|
}
|
|
849
|
+
// A5 audit: emit one 'recall' event per query, capturing the (truncated)
|
|
850
|
+
// query text and the post-filter result count. Tenant resolved by emitCliAudit.
|
|
851
|
+
// Emit before the early-empty return so zero-result recalls are still logged.
|
|
852
|
+
// recall reads from BOTH local and global stores when both are initialized;
|
|
853
|
+
// log against every participating store so the audit trail in either db
|
|
854
|
+
// shows the read access (no false negatives across --global flows).
|
|
855
|
+
const recallMetadata = {
|
|
856
|
+
query: query.slice(0, 200),
|
|
857
|
+
results: results.length,
|
|
858
|
+
};
|
|
859
|
+
emitCliAudit(hippoRoot, 'recall', undefined, recallMetadata);
|
|
860
|
+
if (isInitialized(globalRoot) && globalRoot !== hippoRoot) {
|
|
861
|
+
emitCliAudit(globalRoot, 'recall', undefined, recallMetadata);
|
|
862
|
+
}
|
|
814
863
|
if (results.length === 0) {
|
|
815
864
|
if (asJson) {
|
|
816
865
|
console.log(JSON.stringify({ query, results: [], total: 0 }));
|
|
@@ -916,8 +965,10 @@ async function cmdExplain(hippoRoot, query, flags) {
|
|
|
916
965
|
process.exit(1);
|
|
917
966
|
}
|
|
918
967
|
const globalRoot = getGlobalRoot();
|
|
919
|
-
|
|
920
|
-
|
|
968
|
+
// A5: scope explain results to the active tenant.
|
|
969
|
+
const tenantId = resolveTenantId({});
|
|
970
|
+
let explainLocalEntries = loadSearchEntries(hippoRoot, query, undefined, tenantId);
|
|
971
|
+
let explainGlobalEntries = isInitialized(globalRoot) ? loadSearchEntries(globalRoot, query, undefined, tenantId) : [];
|
|
921
972
|
// Bi-temporal filtering
|
|
922
973
|
if (explainAsOf) {
|
|
923
974
|
const filterAsOfExplain = (entries) => {
|
|
@@ -977,7 +1028,7 @@ async function cmdExplain(hippoRoot, query, flags) {
|
|
|
977
1028
|
else if (hasGlobal) {
|
|
978
1029
|
results = await searchBothHybrid(query, hippoRoot, globalRoot, {
|
|
979
1030
|
budget, explain: true, mmr: mmrEnabled, mmrLambda, localBump, scope: explainActiveScope,
|
|
980
|
-
includeSuperseded: explainIncludeSuperseded, asOf: explainAsOf,
|
|
1031
|
+
includeSuperseded: explainIncludeSuperseded, asOf: explainAsOf, tenantId,
|
|
981
1032
|
});
|
|
982
1033
|
modeUsed = 'searchBothHybrid';
|
|
983
1034
|
}
|
|
@@ -2621,11 +2672,14 @@ async function cmdContext(hippoRoot, args, flags) {
|
|
|
2621
2672
|
}
|
|
2622
2673
|
const globalRoot = getGlobalRoot();
|
|
2623
2674
|
const hasGlobal = isInitialized(globalRoot);
|
|
2675
|
+
// A5: scope context-mode loads to the active tenant. Without this, every
|
|
2676
|
+
// tenant's memories surface through the smart-context injection path.
|
|
2677
|
+
const tenantId = resolveTenantId({});
|
|
2624
2678
|
// When the local store isn't initialized (pinned-only path in a fresh dir),
|
|
2625
2679
|
// skip the local load — loadAllEntries would auto-create .hippo here and
|
|
2626
2680
|
// we don't want to pollute arbitrary cwds.
|
|
2627
|
-
let localEntries = hasLocal ? loadAllEntries(hippoRoot) : [];
|
|
2628
|
-
let globalEntries = hasGlobal ? loadAllEntries(globalRoot) : [];
|
|
2681
|
+
let localEntries = hasLocal ? loadAllEntries(hippoRoot, tenantId) : [];
|
|
2682
|
+
let globalEntries = hasGlobal ? loadAllEntries(globalRoot, tenantId) : [];
|
|
2629
2683
|
// Default context always filters superseded (no --include-superseded / --as-of for context)
|
|
2630
2684
|
localEntries = localEntries.filter(e => !e.superseded_by);
|
|
2631
2685
|
globalEntries = globalEntries.filter(e => !e.superseded_by);
|
|
@@ -2714,7 +2768,7 @@ async function cmdContext(hippoRoot, args, flags) {
|
|
|
2714
2768
|
else {
|
|
2715
2769
|
let results;
|
|
2716
2770
|
if (hasGlobal) {
|
|
2717
|
-
const merged = await searchBothHybrid(query, hippoRoot, globalRoot, { budget, scope: ctxActiveScope });
|
|
2771
|
+
const merged = await searchBothHybrid(query, hippoRoot, globalRoot, { budget, scope: ctxActiveScope, tenantId });
|
|
2718
2772
|
const localIndex = loadIndex(hippoRoot);
|
|
2719
2773
|
results = merged.map((r) => ({
|
|
2720
2774
|
entry: r.entry,
|
|
@@ -2738,6 +2792,20 @@ async function cmdContext(hippoRoot, args, flags) {
|
|
|
2738
2792
|
}
|
|
2739
2793
|
selectedItems = results;
|
|
2740
2794
|
totalTokens = results.reduce((sum, r) => sum + r.tokens, 0);
|
|
2795
|
+
// A5 H4: emit recall audit event for context-mode searches. The recall
|
|
2796
|
+
// handler emits one of these per `hippo recall` invocation; context mode
|
|
2797
|
+
// is the same surface (search → user) and must leave the same audit trail.
|
|
2798
|
+
// Skip pinned-only and '*' fallback (handled in branches above which never
|
|
2799
|
+
// hit the search engines).
|
|
2800
|
+
const ctxRecallMetadata = {
|
|
2801
|
+
query: query.slice(0, 200),
|
|
2802
|
+
results: selectedItems.length,
|
|
2803
|
+
mode: 'context',
|
|
2804
|
+
};
|
|
2805
|
+
if (hasLocal)
|
|
2806
|
+
emitCliAudit(hippoRoot, 'recall', undefined, ctxRecallMetadata);
|
|
2807
|
+
if (hasGlobal)
|
|
2808
|
+
emitCliAudit(globalRoot, 'recall', undefined, ctxRecallMetadata);
|
|
2741
2809
|
}
|
|
2742
2810
|
if (limit < selectedItems.length) {
|
|
2743
2811
|
selectedItems = selectedItems.slice(0, limit);
|
|
@@ -3172,6 +3240,11 @@ function cmdPromote(hippoRoot, id) {
|
|
|
3172
3240
|
}
|
|
3173
3241
|
try {
|
|
3174
3242
|
const globalEntry = promoteToGlobal(hippoRoot, id);
|
|
3243
|
+
// Emit audit on the global store (where the promoted memory now lives).
|
|
3244
|
+
// The writeEntry inside promoteToGlobal already fires a 'remember' on the
|
|
3245
|
+
// global db; we add a separate 'promote' event so the audit trail keeps
|
|
3246
|
+
// the user-facing intent distinct from the underlying upsert.
|
|
3247
|
+
emitCliAudit(getGlobalRoot(), 'promote', globalEntry.id, { sourceId: id });
|
|
3175
3248
|
console.log(`Promoted ${id} to global store as ${globalEntry.id}`);
|
|
3176
3249
|
console.log(` Global store: ${getGlobalRoot()}`);
|
|
3177
3250
|
}
|
|
@@ -3725,6 +3798,235 @@ function cmdDag(hippoRoot, flags) {
|
|
|
3725
3798
|
}
|
|
3726
3799
|
}
|
|
3727
3800
|
}
|
|
3801
|
+
// ---------------------------------------------------------------------------
|
|
3802
|
+
// Auth subcommands (A5 stub auth)
|
|
3803
|
+
// ---------------------------------------------------------------------------
|
|
3804
|
+
function resolveAuthRoot(hippoRoot, flags) {
|
|
3805
|
+
if (flags['global']) {
|
|
3806
|
+
initGlobal();
|
|
3807
|
+
return getGlobalRoot();
|
|
3808
|
+
}
|
|
3809
|
+
requireInit(hippoRoot);
|
|
3810
|
+
return hippoRoot;
|
|
3811
|
+
}
|
|
3812
|
+
function cmdAuthCreate(hippoRoot, flags) {
|
|
3813
|
+
const root = resolveAuthRoot(hippoRoot, flags);
|
|
3814
|
+
const tenantFlag = typeof flags['tenant'] === 'string' ? flags['tenant'] : undefined;
|
|
3815
|
+
const labelFlag = typeof flags['label'] === 'string' ? flags['label'] : undefined;
|
|
3816
|
+
const tenantId = tenantFlag ?? resolveTenantId({});
|
|
3817
|
+
const asJson = Boolean(flags['json']);
|
|
3818
|
+
const db = openHippoDb(root);
|
|
3819
|
+
let result;
|
|
3820
|
+
try {
|
|
3821
|
+
result = createApiKey(db, { tenantId, label: labelFlag });
|
|
3822
|
+
}
|
|
3823
|
+
finally {
|
|
3824
|
+
closeHippoDb(db);
|
|
3825
|
+
}
|
|
3826
|
+
if (asJson) {
|
|
3827
|
+
console.log(JSON.stringify({
|
|
3828
|
+
keyId: result.keyId,
|
|
3829
|
+
plaintext: result.plaintext,
|
|
3830
|
+
tenantId,
|
|
3831
|
+
label: labelFlag ?? null,
|
|
3832
|
+
}));
|
|
3833
|
+
return;
|
|
3834
|
+
}
|
|
3835
|
+
console.log(`key_id: ${result.keyId}`);
|
|
3836
|
+
console.log(`plaintext: ${result.plaintext}`);
|
|
3837
|
+
console.log('');
|
|
3838
|
+
console.log('!! WARNING: this is the ONLY time the plaintext key will be shown. !!');
|
|
3839
|
+
console.log('!! Copy it now. Hippo stores only a scrypt hash and cannot recover it. !!');
|
|
3840
|
+
}
|
|
3841
|
+
function formatKeyRow(item) {
|
|
3842
|
+
const label = item.label ?? '-';
|
|
3843
|
+
const created = item.createdAt;
|
|
3844
|
+
const revoked = item.revokedAt ?? '-';
|
|
3845
|
+
return `${item.keyId} ${item.tenantId} ${label} ${created} ${revoked}`;
|
|
3846
|
+
}
|
|
3847
|
+
function cmdAuthList(hippoRoot, flags) {
|
|
3848
|
+
const root = resolveAuthRoot(hippoRoot, flags);
|
|
3849
|
+
const includeRevoked = Boolean(flags['all']);
|
|
3850
|
+
const asJson = Boolean(flags['json']);
|
|
3851
|
+
const db = openHippoDb(root);
|
|
3852
|
+
let items;
|
|
3853
|
+
try {
|
|
3854
|
+
items = listApiKeys(db, { active: !includeRevoked });
|
|
3855
|
+
}
|
|
3856
|
+
finally {
|
|
3857
|
+
closeHippoDb(db);
|
|
3858
|
+
}
|
|
3859
|
+
if (asJson) {
|
|
3860
|
+
console.log(JSON.stringify(items));
|
|
3861
|
+
return;
|
|
3862
|
+
}
|
|
3863
|
+
if (items.length === 0) {
|
|
3864
|
+
console.log(includeRevoked ? 'No API keys.' : 'No active API keys. (Use --all to include revoked.)');
|
|
3865
|
+
return;
|
|
3866
|
+
}
|
|
3867
|
+
console.log('key_id tenant label created revoked');
|
|
3868
|
+
for (const item of items) {
|
|
3869
|
+
console.log(formatKeyRow(item));
|
|
3870
|
+
}
|
|
3871
|
+
}
|
|
3872
|
+
function cmdAuthRevoke(hippoRoot, keyId, flags) {
|
|
3873
|
+
const root = resolveAuthRoot(hippoRoot, flags);
|
|
3874
|
+
const asJson = Boolean(flags['json']);
|
|
3875
|
+
const db = openHippoDb(root);
|
|
3876
|
+
let exists = false;
|
|
3877
|
+
let alreadyRevoked = false;
|
|
3878
|
+
let revokedAt = null;
|
|
3879
|
+
let keyTenantId = null;
|
|
3880
|
+
try {
|
|
3881
|
+
const row = db.prepare(`SELECT key_id, tenant_id, revoked_at FROM api_keys WHERE key_id = ?`).get(keyId);
|
|
3882
|
+
if (!row) {
|
|
3883
|
+
// Let the finally{} block close the db. M4: avoid manual close before
|
|
3884
|
+
// process.exit() — the finally already handles it on every path.
|
|
3885
|
+
console.error(`Unknown key_id: ${keyId}`);
|
|
3886
|
+
process.exit(1);
|
|
3887
|
+
}
|
|
3888
|
+
exists = true;
|
|
3889
|
+
keyTenantId = row.tenant_id;
|
|
3890
|
+
if (row.revoked_at) {
|
|
3891
|
+
alreadyRevoked = true;
|
|
3892
|
+
revokedAt = row.revoked_at;
|
|
3893
|
+
}
|
|
3894
|
+
else {
|
|
3895
|
+
revokeApiKey(db, keyId);
|
|
3896
|
+
const updated = db.prepare(`SELECT revoked_at FROM api_keys WHERE key_id = ?`).get(keyId);
|
|
3897
|
+
revokedAt = updated?.revoked_at ?? null;
|
|
3898
|
+
}
|
|
3899
|
+
// M1: emit auth_revoke audit event. Skip on no-op revoke (already revoked)
|
|
3900
|
+
// so re-running the command doesn't pad the audit log with duplicates.
|
|
3901
|
+
if (!alreadyRevoked && keyTenantId) {
|
|
3902
|
+
try {
|
|
3903
|
+
appendAuditEvent(db, {
|
|
3904
|
+
tenantId: keyTenantId,
|
|
3905
|
+
actor: 'cli',
|
|
3906
|
+
op: 'auth_revoke',
|
|
3907
|
+
targetId: keyId,
|
|
3908
|
+
});
|
|
3909
|
+
}
|
|
3910
|
+
catch {
|
|
3911
|
+
// Audit must not crash a successful revoke.
|
|
3912
|
+
}
|
|
3913
|
+
}
|
|
3914
|
+
}
|
|
3915
|
+
finally {
|
|
3916
|
+
closeHippoDb(db);
|
|
3917
|
+
}
|
|
3918
|
+
if (!exists)
|
|
3919
|
+
return;
|
|
3920
|
+
if (asJson) {
|
|
3921
|
+
console.log(JSON.stringify({ keyId, revokedAt }));
|
|
3922
|
+
return;
|
|
3923
|
+
}
|
|
3924
|
+
console.log(`Revoked ${keyId} at ${revokedAt}`);
|
|
3925
|
+
}
|
|
3926
|
+
// ---------------------------------------------------------------------------
|
|
3927
|
+
// Audit log subcommands (A5 stub auth — `hippo audit list`)
|
|
3928
|
+
// ---------------------------------------------------------------------------
|
|
3929
|
+
const VALID_AUDIT_OPS = new Set([
|
|
3930
|
+
'remember',
|
|
3931
|
+
'recall',
|
|
3932
|
+
'promote',
|
|
3933
|
+
'supersede',
|
|
3934
|
+
'forget',
|
|
3935
|
+
'archive_raw',
|
|
3936
|
+
'auth_revoke',
|
|
3937
|
+
]);
|
|
3938
|
+
function formatAuditRow(ev) {
|
|
3939
|
+
const target = ev.targetId ?? '-';
|
|
3940
|
+
const meta = JSON.stringify(ev.metadata ?? {});
|
|
3941
|
+
return `${ev.ts} ${ev.actor} ${ev.op} ${target} ${meta}`;
|
|
3942
|
+
}
|
|
3943
|
+
function cmdAuditList(hippoRoot, flags) {
|
|
3944
|
+
const root = resolveAuthRoot(hippoRoot, flags);
|
|
3945
|
+
const asJson = Boolean(flags['json']);
|
|
3946
|
+
const tenantId = resolveTenantId({});
|
|
3947
|
+
const opFlag = typeof flags['op'] === 'string' ? flags['op'] : undefined;
|
|
3948
|
+
if (opFlag && !VALID_AUDIT_OPS.has(opFlag)) {
|
|
3949
|
+
console.error(`Unknown --op value: ${opFlag}. Expected one of: remember | recall | promote | supersede | forget | archive_raw.`);
|
|
3950
|
+
process.exit(1);
|
|
3951
|
+
}
|
|
3952
|
+
const op = opFlag;
|
|
3953
|
+
const since = typeof flags['since'] === 'string' ? flags['since'] : undefined;
|
|
3954
|
+
if (since !== undefined && !Number.isFinite(new Date(since).getTime())) {
|
|
3955
|
+
console.error(`Invalid --since: ${since} (expected an ISO timestamp like 2026-04-22 or 2026-04-22T12:00:00Z).`);
|
|
3956
|
+
process.exit(1);
|
|
3957
|
+
}
|
|
3958
|
+
const limitRaw = flags['limit'];
|
|
3959
|
+
let limit = 100;
|
|
3960
|
+
if (limitRaw !== undefined && typeof limitRaw !== 'boolean') {
|
|
3961
|
+
const parsed = parseInt(String(limitRaw), 10);
|
|
3962
|
+
if (!Number.isFinite(parsed)) {
|
|
3963
|
+
console.error(`Invalid --limit value: ${String(limitRaw)} (expected a positive integer).`);
|
|
3964
|
+
process.exit(1);
|
|
3965
|
+
}
|
|
3966
|
+
limit = parsed;
|
|
3967
|
+
}
|
|
3968
|
+
if (limit < 1 || limit > 10000) {
|
|
3969
|
+
console.error(`--limit must be between 1 and 10000 (got ${limit}).`);
|
|
3970
|
+
process.exit(1);
|
|
3971
|
+
}
|
|
3972
|
+
const db = openHippoDb(root);
|
|
3973
|
+
let events;
|
|
3974
|
+
try {
|
|
3975
|
+
events = queryAuditEvents(db, { tenantId, op, since, limit });
|
|
3976
|
+
}
|
|
3977
|
+
finally {
|
|
3978
|
+
closeHippoDb(db);
|
|
3979
|
+
}
|
|
3980
|
+
if (asJson) {
|
|
3981
|
+
console.log(JSON.stringify(events));
|
|
3982
|
+
return;
|
|
3983
|
+
}
|
|
3984
|
+
if (events.length === 0) {
|
|
3985
|
+
console.log('No audit events.');
|
|
3986
|
+
return;
|
|
3987
|
+
}
|
|
3988
|
+
console.log('ts actor op target_id metadata');
|
|
3989
|
+
for (const ev of events) {
|
|
3990
|
+
console.log(formatAuditRow(ev));
|
|
3991
|
+
}
|
|
3992
|
+
}
|
|
3993
|
+
function cmdAuditLog(hippoRoot, args, flags) {
|
|
3994
|
+
const sub = args[0];
|
|
3995
|
+
if (sub === 'list') {
|
|
3996
|
+
cmdAuditList(hippoRoot, flags);
|
|
3997
|
+
return;
|
|
3998
|
+
}
|
|
3999
|
+
console.error(`Unknown audit subcommand: ${sub}. Expected: list.`);
|
|
4000
|
+
process.exit(1);
|
|
4001
|
+
}
|
|
4002
|
+
function cmdAuth(hippoRoot, args, flags) {
|
|
4003
|
+
const sub = args[0];
|
|
4004
|
+
if (!sub) {
|
|
4005
|
+
console.error('Usage: hippo auth <create|list|revoke> [options]');
|
|
4006
|
+
process.exit(1);
|
|
4007
|
+
}
|
|
4008
|
+
const subArgs = args.slice(1);
|
|
4009
|
+
switch (sub) {
|
|
4010
|
+
case 'create':
|
|
4011
|
+
cmdAuthCreate(hippoRoot, flags);
|
|
4012
|
+
return;
|
|
4013
|
+
case 'list':
|
|
4014
|
+
cmdAuthList(hippoRoot, flags);
|
|
4015
|
+
return;
|
|
4016
|
+
case 'revoke': {
|
|
4017
|
+
const keyId = subArgs[0];
|
|
4018
|
+
if (!keyId) {
|
|
4019
|
+
console.error('Usage: hippo auth revoke <key_id>');
|
|
4020
|
+
process.exit(1);
|
|
4021
|
+
}
|
|
4022
|
+
cmdAuthRevoke(hippoRoot, keyId, flags);
|
|
4023
|
+
return;
|
|
4024
|
+
}
|
|
4025
|
+
default:
|
|
4026
|
+
console.error(`Unknown auth subcommand: ${sub}. Expected: create | list | revoke.`);
|
|
4027
|
+
process.exit(1);
|
|
4028
|
+
}
|
|
4029
|
+
}
|
|
3728
4030
|
function printUsage() {
|
|
3729
4031
|
console.log(`
|
|
3730
4032
|
Hippo - biologically-inspired memory system for AI agents
|
|
@@ -3961,6 +4263,27 @@ Commands:
|
|
|
3961
4263
|
dashboard Open web dashboard for memory health
|
|
3962
4264
|
--port <n> Port to serve on (default: 3333)
|
|
3963
4265
|
mcp Start MCP server (stdio transport)
|
|
4266
|
+
auth <sub> Manage API keys (A5 stub auth)
|
|
4267
|
+
auth create Mint a new API key (plaintext shown ONCE)
|
|
4268
|
+
--label <s> Optional human label
|
|
4269
|
+
--tenant <id> Override tenant (defaults to HIPPO_TENANT)
|
|
4270
|
+
--json Output as JSON
|
|
4271
|
+
--global Operate on the global store
|
|
4272
|
+
auth list List API keys (active by default)
|
|
4273
|
+
--all Include revoked keys
|
|
4274
|
+
--json Output as JSON
|
|
4275
|
+
--global Operate on the global store
|
|
4276
|
+
auth revoke <key_id> Revoke an API key (subsequent validate fails)
|
|
4277
|
+
--json Output as JSON
|
|
4278
|
+
--global Operate on the global store
|
|
4279
|
+
audit <sub> Query the append-only audit log (A5 stub auth)
|
|
4280
|
+
audit list List audit events for the active tenant
|
|
4281
|
+
--op <op> Filter by op (remember | recall | promote |
|
|
4282
|
+
supersede | forget | archive_raw | auth_revoke)
|
|
4283
|
+
--since <iso> Lower bound on ts (ISO timestamp)
|
|
4284
|
+
--limit <n> Max events (default: 100, max: 10000)
|
|
4285
|
+
--json Output as JSON
|
|
4286
|
+
--global Operate on the global store
|
|
3964
4287
|
|
|
3965
4288
|
Examples:
|
|
3966
4289
|
hippo init
|
|
@@ -4103,7 +4426,16 @@ async function main() {
|
|
|
4103
4426
|
case 'dag':
|
|
4104
4427
|
cmdDag(hippoRoot, flags);
|
|
4105
4428
|
break;
|
|
4429
|
+
case 'auth':
|
|
4430
|
+
cmdAuth(hippoRoot, args, flags);
|
|
4431
|
+
break;
|
|
4106
4432
|
case 'audit': {
|
|
4433
|
+
// `audit list` -> A5 audit-log viewer. Other forms (no sub, --fix) keep
|
|
4434
|
+
// the existing memory-quality auditor for backwards compatibility.
|
|
4435
|
+
if (args[0] === 'list') {
|
|
4436
|
+
cmdAuditLog(hippoRoot, args, flags);
|
|
4437
|
+
break;
|
|
4438
|
+
}
|
|
4107
4439
|
requireInit(hippoRoot);
|
|
4108
4440
|
const entries = loadAllEntries(hippoRoot);
|
|
4109
4441
|
const result = auditMemories(entries);
|