hippo-memory 0.35.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/dist/cli.js CHANGED
@@ -45,12 +45,15 @@ 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, promoteToGlobal, shareMemory, listPeers, autoShare, transferScore, searchBothHybrid, syncGlobalToLocal, } from './shared.js';
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, appendAuditEvent, queryAuditEvents, } from './audit.js';
53
- import { createApiKey, listApiKeys, revokeApiKey } from './auth.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';
54
57
  import { resolveTenantId } from './tenant.js';
55
58
  import { runEval, bootstrapCorpus, compareSummaries } from './eval.js';
56
59
  import { runFeatureEval, formatResult, resultToBaseline, detectRegressions } from './eval-suite.js';
@@ -99,6 +102,35 @@ function requireInit(hippoRoot) {
99
102
  process.exit(1);
100
103
  }
101
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
+ }
102
134
  function parseArgs(argv) {
103
135
  const [, , command = '', ...rest] = argv;
104
136
  const args = [];
@@ -2159,12 +2191,17 @@ function cmdOutcome(hippoRoot, flags) {
2159
2191
  }
2160
2192
  function cmdForget(hippoRoot, id) {
2161
2193
  requireInit(hippoRoot);
2162
- const ok = deleteEntry(hippoRoot, id);
2163
- if (ok) {
2194
+ const ctx = {
2195
+ hippoRoot,
2196
+ tenantId: resolveTenantId({}),
2197
+ actor: 'cli',
2198
+ };
2199
+ try {
2200
+ api.forget(ctx, id);
2164
2201
  updateStats(hippoRoot, { forgotten: 1 });
2165
2202
  console.log(`Forgot ${id}`);
2166
2203
  }
2167
- else {
2204
+ catch {
2168
2205
  console.error(`Memory not found: ${id}`);
2169
2206
  process.exit(1);
2170
2207
  }
@@ -3238,14 +3275,14 @@ function cmdPromote(hippoRoot, id) {
3238
3275
  console.error('Usage: hippo promote <id>');
3239
3276
  process.exit(1);
3240
3277
  }
3278
+ const ctx = {
3279
+ hippoRoot,
3280
+ tenantId: resolveTenantId({}),
3281
+ actor: 'cli',
3282
+ };
3241
3283
  try {
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 });
3248
- console.log(`Promoted ${id} to global store as ${globalEntry.id}`);
3284
+ const result = api.promote(ctx, id);
3285
+ console.log(`Promoted ${id} to global store as ${result.globalId}`);
3249
3286
  console.log(` Global store: ${getGlobalRoot()}`);
3250
3287
  }
3251
3288
  catch (err) {
@@ -3813,21 +3850,18 @@ function cmdAuthCreate(hippoRoot, flags) {
3813
3850
  const root = resolveAuthRoot(hippoRoot, flags);
3814
3851
  const tenantFlag = typeof flags['tenant'] === 'string' ? flags['tenant'] : undefined;
3815
3852
  const labelFlag = typeof flags['label'] === 'string' ? flags['label'] : undefined;
3816
- const tenantId = tenantFlag ?? resolveTenantId({});
3817
3853
  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
- }
3854
+ const ctx = {
3855
+ hippoRoot: root,
3856
+ tenantId: resolveTenantId({}),
3857
+ actor: 'cli',
3858
+ };
3859
+ const result = api.authCreate(ctx, { tenantId: tenantFlag, label: labelFlag });
3826
3860
  if (asJson) {
3827
3861
  console.log(JSON.stringify({
3828
3862
  keyId: result.keyId,
3829
3863
  plaintext: result.plaintext,
3830
- tenantId,
3864
+ tenantId: result.tenantId,
3831
3865
  label: labelFlag ?? null,
3832
3866
  }));
3833
3867
  return;
@@ -3969,14 +4003,8 @@ function cmdAuditList(hippoRoot, flags) {
3969
4003
  console.error(`--limit must be between 1 and 10000 (got ${limit}).`);
3970
4004
  process.exit(1);
3971
4005
  }
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
- }
4006
+ const ctx = { hippoRoot: root, tenantId, actor: 'cli' };
4007
+ const events = api.auditList(ctx, { op, since, limit });
3980
4008
  if (asJson) {
3981
4009
  console.log(JSON.stringify(events));
3982
4010
  return;
@@ -4350,6 +4378,37 @@ async function main() {
4350
4378
  console.error('Memory content too short (minimum 3 characters).');
4351
4379
  process.exit(1);
4352
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
+ }
4353
4412
  await cmdRemember(hippoRoot, text, flags);
4354
4413
  break;
4355
4414
  }
@@ -4499,6 +4558,18 @@ async function main() {
4499
4558
  console.error('Please provide a memory ID.');
4500
4559
  process.exit(1);
4501
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;
4502
4573
  cmdForget(hippoRoot, id);
4503
4574
  break;
4504
4575
  }
@@ -4540,6 +4611,18 @@ async function main() {
4540
4611
  console.error('Please provide a memory ID.');
4541
4612
  process.exit(1);
4542
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;
4543
4626
  cmdPromote(hippoRoot, id);
4544
4627
  break;
4545
4628
  }
@@ -4689,12 +4772,35 @@ async function main() {
4689
4772
  case 'wm':
4690
4773
  cmdWm(hippoRoot, args, flags);
4691
4774
  break;
4692
- case 'mcp':
4693
- // Start MCP server over stdio - dynamically import to keep main CLI lean
4694
- await import('./mcp/server.js');
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();
4695
4782
  // Server runs until stdin closes, so we never reach here
4696
4783
  await new Promise(() => { }); // hang forever
4697
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
+ }
4698
4804
  case 'invalidate': {
4699
4805
  requireInit(hippoRoot);
4700
4806
  const target = args[0];