hippo-memory 1.23.0 → 1.24.1

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.
Files changed (148) hide show
  1. package/README.md +16 -5
  2. package/bin/hippo.js +2 -2
  3. package/dist/ablation.d.ts +93 -0
  4. package/dist/ablation.d.ts.map +1 -0
  5. package/dist/ablation.js +138 -0
  6. package/dist/ablation.js.map +1 -0
  7. package/dist/ambient.d.ts.map +1 -1
  8. package/dist/ambient.js +2 -1
  9. package/dist/ambient.js.map +1 -1
  10. package/dist/api.d.ts +37 -0
  11. package/dist/api.d.ts.map +1 -1
  12. package/dist/api.js +88 -11
  13. package/dist/api.js.map +1 -1
  14. package/dist/benchmarks/e1.3/scenarios.json +2587 -0
  15. package/dist/cli.d.ts +5 -0
  16. package/dist/cli.d.ts.map +1 -1
  17. package/dist/cli.js +133 -20
  18. package/dist/cli.js.map +1 -1
  19. package/dist/config.d.ts +8 -0
  20. package/dist/config.d.ts.map +1 -1
  21. package/dist/config.js +2 -0
  22. package/dist/config.js.map +1 -1
  23. package/dist/connectors/github/backfill.js +4 -4
  24. package/dist/connectors/github/cli-impl.js +6 -6
  25. package/dist/connectors/github/dlq.js +14 -14
  26. package/dist/connectors/slack/backfill.js +1 -1
  27. package/dist/connectors/slack/dlq.js +10 -10
  28. package/dist/connectors/slack/workspaces.js +4 -4
  29. package/dist/consolidate.d.ts.map +1 -1
  30. package/dist/consolidate.js +27 -5
  31. package/dist/consolidate.js.map +1 -1
  32. package/dist/dag.js +6 -6
  33. package/dist/dashboard.js +7 -7
  34. package/dist/db.d.ts.map +1 -1
  35. package/dist/db.js +55 -4
  36. package/dist/db.js.map +1 -1
  37. package/dist/hooks.js +24 -24
  38. package/dist/importers.d.ts +28 -0
  39. package/dist/importers.d.ts.map +1 -1
  40. package/dist/importers.js +412 -1
  41. package/dist/importers.js.map +1 -1
  42. package/dist/index.d.ts +1 -1
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +1 -1
  45. package/dist/index.js.map +1 -1
  46. package/dist/mcp/server.d.ts.map +1 -1
  47. package/dist/mcp/server.js +44 -7
  48. package/dist/mcp/server.js.map +1 -1
  49. package/dist/memory.d.ts +9 -0
  50. package/dist/memory.d.ts.map +1 -1
  51. package/dist/memory.js +29 -6
  52. package/dist/memory.js.map +1 -1
  53. package/dist/physics-state.d.ts.map +1 -1
  54. package/dist/physics-state.js +4 -3
  55. package/dist/physics-state.js.map +1 -1
  56. package/dist/physics.d.ts.map +1 -1
  57. package/dist/physics.js +8 -1
  58. package/dist/physics.js.map +1 -1
  59. package/dist/predictions.js +67 -67
  60. package/dist/project-identity.d.ts +84 -0
  61. package/dist/project-identity.d.ts.map +1 -0
  62. package/dist/project-identity.js +184 -0
  63. package/dist/project-identity.js.map +1 -0
  64. package/dist/refine-llm.js +13 -13
  65. package/dist/replay.d.ts.map +1 -1
  66. package/dist/replay.js +4 -1
  67. package/dist/replay.js.map +1 -1
  68. package/dist/search.d.ts +14 -0
  69. package/dist/search.d.ts.map +1 -1
  70. package/dist/search.js +36 -7
  71. package/dist/search.js.map +1 -1
  72. package/dist/secret-detect.d.ts +33 -0
  73. package/dist/secret-detect.d.ts.map +1 -0
  74. package/dist/secret-detect.js +70 -0
  75. package/dist/secret-detect.js.map +1 -0
  76. package/dist/server.d.ts.map +1 -1
  77. package/dist/server.js +11 -0
  78. package/dist/server.js.map +1 -1
  79. package/dist/shared.d.ts +9 -1
  80. package/dist/shared.d.ts.map +1 -1
  81. package/dist/shared.js +67 -16
  82. package/dist/shared.js.map +1 -1
  83. package/dist/src/ablation.js +138 -0
  84. package/dist/src/ablation.js.map +1 -0
  85. package/dist/src/ambient.js +2 -1
  86. package/dist/src/ambient.js.map +1 -1
  87. package/dist/src/api.js +88 -11
  88. package/dist/src/api.js.map +1 -1
  89. package/dist/src/cli.js +133 -20
  90. package/dist/src/cli.js.map +1 -1
  91. package/dist/src/config.js +2 -0
  92. package/dist/src/config.js.map +1 -1
  93. package/dist/src/connectors/github/backfill.js +4 -4
  94. package/dist/src/connectors/github/cli-impl.js +6 -6
  95. package/dist/src/connectors/github/dlq.js +14 -14
  96. package/dist/src/connectors/slack/backfill.js +1 -1
  97. package/dist/src/connectors/slack/dlq.js +10 -10
  98. package/dist/src/connectors/slack/workspaces.js +4 -4
  99. package/dist/src/consolidate.js +27 -5
  100. package/dist/src/consolidate.js.map +1 -1
  101. package/dist/src/dag.js +6 -6
  102. package/dist/src/dashboard.js +7 -7
  103. package/dist/src/db.js +55 -4
  104. package/dist/src/db.js.map +1 -1
  105. package/dist/src/hooks.js +24 -24
  106. package/dist/src/importers.js +412 -1
  107. package/dist/src/importers.js.map +1 -1
  108. package/dist/src/index.js +1 -1
  109. package/dist/src/index.js.map +1 -1
  110. package/dist/src/mcp/server.js +44 -7
  111. package/dist/src/mcp/server.js.map +1 -1
  112. package/dist/src/memory.js +29 -6
  113. package/dist/src/memory.js.map +1 -1
  114. package/dist/src/physics-state.js +4 -3
  115. package/dist/src/physics-state.js.map +1 -1
  116. package/dist/src/physics.js +8 -1
  117. package/dist/src/physics.js.map +1 -1
  118. package/dist/src/predictions.js +67 -67
  119. package/dist/src/project-identity.js +184 -0
  120. package/dist/src/project-identity.js.map +1 -0
  121. package/dist/src/refine-llm.js +13 -13
  122. package/dist/src/replay.js +4 -1
  123. package/dist/src/replay.js.map +1 -1
  124. package/dist/src/search.js +36 -7
  125. package/dist/src/search.js.map +1 -1
  126. package/dist/src/secret-detect.js +70 -0
  127. package/dist/src/secret-detect.js.map +1 -0
  128. package/dist/src/server.js +11 -0
  129. package/dist/src/server.js.map +1 -1
  130. package/dist/src/shared.js +67 -16
  131. package/dist/src/shared.js.map +1 -1
  132. package/dist/src/store.js +66 -11
  133. package/dist/src/store.js.map +1 -1
  134. package/dist/src/version.js +1 -1
  135. package/dist/src/working-memory.js +19 -19
  136. package/dist/store.d.ts +15 -0
  137. package/dist/store.d.ts.map +1 -1
  138. package/dist/store.js +66 -11
  139. package/dist/store.js.map +1 -1
  140. package/dist/version.d.ts +1 -1
  141. package/dist/version.js +1 -1
  142. package/dist/working-memory.js +19 -19
  143. package/dist-ui/index.html +12 -12
  144. package/extensions/openclaw-plugin/index.ts +650 -650
  145. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  146. package/extensions/openclaw-plugin/package.json +1 -1
  147. package/openclaw.plugin.json +1 -1
  148. package/package.json +3 -1
package/dist/cli.d.ts CHANGED
@@ -29,6 +29,11 @@ import { MemoryEntry } from './memory.js';
29
29
  import * as api from './api.js';
30
30
  /** Test-only: reset the module-level recall-history Map. Call from beforeEach. */
31
31
  export declare function __resetSessionRecallHistoryCli(): void;
32
+ /**
33
+ * Scan for Claude Code MEMORY.md files and import new entries into hippo.
34
+ * Looks in ~/.claude/projects/<project>/memory/ for .md files with YAML frontmatter.
35
+ */
36
+ export declare function learnFromMemoryMd(hippoRoot: string, homeDir?: string): number;
32
37
  /**
33
38
  * Render an api.sleep result as console output, byte-identical to the
34
39
  * pre-extraction inline implementation in cmdSleepCore.
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAwBH,OAAO,EASL,WAAW,EAEZ,MAAM,aAAa,CAAC;AAmGrB,OAAO,KAAK,GAAG,MAAM,UAAU,CAAC;AA4ChC,kFAAkF;AAClF,wBAAgB,8BAA8B,IAAI,IAAI,CAErD;AAi4ED;;;GAGG;AACH,oHAAoH;AACpH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,GAAG,CAAC,WAAW,GAAG,IAAI,CAkD/D;AA2tFD,oHAAoH;AACpH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,KAAK,CAAC;IAAE,KAAK,EAAE,WAAW,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,CAAC,EACtF,WAAW,EAAE,MAAM,EACnB,OAAO,GAAE,MAAkB,GAC1B,IAAI,CA6BN"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAyBH,OAAO,EASL,WAAW,EAEZ,MAAM,aAAa,CAAC;AAsGrB,OAAO,KAAK,GAAG,MAAM,UAAU,CAAC;AA4ChC,kFAAkF;AAClF,wBAAgB,8BAA8B,IAAI,IAAI,CAErD;AAguED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,GAAE,MAAqB,GAAG,MAAM,CAyE3F;AAyGD;;;GAGG;AACH,oHAAoH;AACpH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,GAAG,CAAC,WAAW,GAAG,IAAI,CAkD/D;AA8vFD,oHAAoH;AACpH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,KAAK,CAAC;IAAE,KAAK,EAAE,WAAW,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,CAAC,EACtF,WAAW,EAAE,MAAM,EACnB,OAAO,GAAE,MAAkB,GAC1B,IAAI,CA6BN"}
package/dist/cli.js CHANGED
@@ -25,6 +25,7 @@
25
25
  * hippo decide "<decision>" [--context "<why>"] [--supersedes <id>]
26
26
  * hippo wm <push|read|clear|flush>
27
27
  */
28
+ import { evalNow, isRecallBoostAblated } from './ablation.js';
28
29
  import * as path from 'path';
29
30
  import * as fs from 'fs';
30
31
  import * as os from 'os';
@@ -32,6 +33,8 @@ import { fileURLToPath } from 'node:url';
32
33
  import { execFileSync, execSync, spawn } from 'child_process';
33
34
  import { installJsonHooks, uninstallJsonHooks, resolveJsonHookPaths, detectInstalledTools, defaultSleepLogPath, ensureCodexWrapperInstalled, installCodexWrapper, uninstallCodexWrapper, resolveCodexSessionTranscript, resolveCodexWrapperPaths, installOpencodePlugin, uninstallOpencodePlugin, resolveOpencodePluginPath, } from './hooks.js';
34
35
  import { createMemory, calculateStrength, calculateRewardFactor, deriveHalfLife, resolveConfidence, computeSchemaFit, Layer, } from './memory.js';
36
+ import { resolveProjectIdentity } from './project-identity.js';
37
+ import { detectSecret } from './secret-detect.js';
35
38
  import { getHippoRoot, isInitialized, initStore, writeEntry, readEntry, deleteEntry, loadAllEntries, loadSearchEntries, loadIndex, saveIndex, loadStats, updateStats, saveActiveTaskSnapshot, loadActiveTaskSnapshot, clearActiveTaskSnapshot, appendSessionEvent, listSessionEvents, listMemoryConflicts, resolveConflict, saveSessionHandoff, loadLatestHandoff, loadHandoffById, RECALL_DEFAULT_DENY_SCOPES, } from './store.js';
36
39
  import { markRetrieved, hybridSearch, physicsSearch, explainMatch, textOverlap } from './search.js';
37
40
  import { renderTraceContent, parseSteps } from './trace.js';
@@ -50,7 +53,7 @@ import { extractPathTags } from './path-context.js';
50
53
  import { detectScope } from './scope.js';
51
54
  import { getGlobalRoot, initGlobal, shareMemory, listPeers, autoShare, transferScore, searchBothHybrid, syncGlobalToLocal, } from './shared.js';
52
55
  import { DAILY_TASK_NAME, buildDailyRunnerCommand, listRegisteredWorkspaces, registerWorkspace, runDailyMaintenance, } from './scheduler.js';
53
- import { importChatGPT, importClaude, importCursor, importGenericFile, importMarkdown, } from './importers.js';
56
+ import { importChatGPT, importClaude, importCursor, importGenericFile, importMarkdown, importVault, } from './importers.js';
54
57
  import { cmdCapture } from './capture.js';
55
58
  import { auditMemories, appendAuditEvent, } from './audit.js';
56
59
  import { listApiKeys, revokeApiKey } from './auth.js';
@@ -1482,9 +1485,15 @@ async function cmdRecall(hippoRoot, query, flags) {
1482
1485
  // Update retrieval metadata and persist
1483
1486
  const updated = markRetrieved(results.map((r) => r.entry));
1484
1487
  const localIndex = loadIndex(hippoRoot);
1485
- for (const u of updated) {
1486
- const targetRoot = localIndex.entries[u.id] ? hippoRoot : (isInitialized(globalRoot) ? globalRoot : hippoRoot);
1487
- writeEntry(targetRoot, u);
1488
+ // EVAL-ONLY ablation (see ablation.ts): under the recall flag, markRetrieved
1489
+ // returns unmutated entries (ids preserved for outcome attribution below)
1490
+ // and persistence is skipped - writeEntry on identical rows still refreshes
1491
+ // updated_at, rewrites mirrors, and marks DAG parents dirty.
1492
+ if (!isRecallBoostAblated()) {
1493
+ for (const u of updated) {
1494
+ const targetRoot = localIndex.entries[u.id] ? hippoRoot : (isInitialized(globalRoot) ? globalRoot : hippoRoot);
1495
+ writeEntry(targetRoot, u);
1496
+ }
1488
1497
  }
1489
1498
  // Track last retrieval IDs for outcome command
1490
1499
  localIndex.last_retrieval_ids = updated.map((u) => u.id);
@@ -2091,7 +2100,7 @@ function cmdTrace(hippoRoot, id, flags) {
2091
2100
  console.error(`Memory not found: ${id}`);
2092
2101
  process.exit(1);
2093
2102
  }
2094
- const now = new Date();
2103
+ const now = evalNow();
2095
2104
  const strength = calculateStrength(entry, now);
2096
2105
  const halfLife = deriveHalfLife(7, entry);
2097
2106
  const rewardFactor = calculateRewardFactor(entry);
@@ -2216,8 +2225,8 @@ async function cmdRefine(hippoRoot, flags) {
2216
2225
  * Scan for Claude Code MEMORY.md files and import new entries into hippo.
2217
2226
  * Looks in ~/.claude/projects/<project>/memory/ for .md files with YAML frontmatter.
2218
2227
  */
2219
- function learnFromMemoryMd(hippoRoot) {
2220
- const home = os.homedir();
2228
+ export function learnFromMemoryMd(hippoRoot, homeDir = os.homedir()) {
2229
+ const home = homeDir;
2221
2230
  const memoryDirs = [];
2222
2231
  // Claude Code project memories
2223
2232
  const claudeProjectsDir = path.join(home, '.claude', 'projects');
@@ -2235,6 +2244,7 @@ function learnFromMemoryMd(hippoRoot) {
2235
2244
  return 0;
2236
2245
  const existing = loadAllEntries(hippoRoot);
2237
2246
  let imported = 0;
2247
+ let skippedSecret = 0;
2238
2248
  for (const memDir of memoryDirs) {
2239
2249
  try {
2240
2250
  const files = fs.readdirSync(memDir).filter(f => f.endsWith('.md') && f !== 'MEMORY.md');
@@ -2249,6 +2259,15 @@ function learnFromMemoryMd(hippoRoot) {
2249
2259
  continue;
2250
2260
  // Truncate to reasonable size
2251
2261
  const content = body.length > 1500 ? body.slice(0, 1500) + ' [truncated]' : body;
2262
+ // Never ingest secret-bearing memory files. Some Claude Code memory
2263
+ // files exist purely to hold a live credential (e.g. an API-key
2264
+ // reference). The scope-isolation secret veto (v1.24.0) gated
2265
+ // share/promote/sync/ambient but missed this import path, so a live
2266
+ // key could land in the store here. Veto it at ingest. (v1.24.1)
2267
+ if (detectSecret({ content, tags: ['claude-code-memory'] }).flagged) {
2268
+ skippedSecret++;
2269
+ continue;
2270
+ }
2252
2271
  // Dedup: check if substantially similar content already exists
2253
2272
  const isDup = existing.some(e => {
2254
2273
  const overlap = textOverlap(content.slice(0, 200), e.content.slice(0, 200));
@@ -2269,6 +2288,9 @@ function learnFromMemoryMd(hippoRoot) {
2269
2288
  }
2270
2289
  catch { /* skip broken dirs */ }
2271
2290
  }
2291
+ if (skippedSecret > 0) {
2292
+ console.log(`Skipped ${skippedSecret} secret-bearing memory file${skippedSecret === 1 ? '' : 's'} (not ingested).`);
2293
+ }
2272
2294
  return imported;
2273
2295
  }
2274
2296
  function cmdDedup(hippoRoot, flags) {
@@ -2705,7 +2727,7 @@ function cmdStatus(hippoRoot) {
2705
2727
  requireInit(hippoRoot);
2706
2728
  const entries = loadAllEntries(hippoRoot);
2707
2729
  const stats = loadStats(hippoRoot);
2708
- const now = new Date();
2730
+ const now = evalNow();
2709
2731
  const byLayer = {
2710
2732
  [Layer.Buffer]: 0,
2711
2733
  [Layer.Episodic]: 0,
@@ -2926,7 +2948,7 @@ function cmdInspect(hippoRoot, id) {
2926
2948
  console.error(`Memory not found: ${id}`);
2927
2949
  process.exit(1);
2928
2950
  }
2929
- const now = new Date();
2951
+ const now = evalNow();
2930
2952
  const currentStrength = calculateStrength(entry, now);
2931
2953
  const lastRetrieved = new Date(entry.last_retrieved);
2932
2954
  const created = new Date(entry.created);
@@ -4911,6 +4933,9 @@ async function cmdContext(hippoRoot, args, flags) {
4911
4933
  tenantId: resolveTenantId({}),
4912
4934
  actor: api.adminActor('cli'),
4913
4935
  };
4936
+ // v39 memory scope isolation: --cross-project re-includes other-project
4937
+ // memories (rendered under a demarcated section below).
4938
+ const crossProject = flags['cross-project'] === true;
4914
4939
  const opts = {
4915
4940
  q: query,
4916
4941
  budget,
@@ -4918,6 +4943,7 @@ async function cmdContext(hippoRoot, args, flags) {
4918
4943
  pinnedOnly,
4919
4944
  scope: ctxActiveScope ?? undefined,
4920
4945
  includeRecent: parseCountFlag(flags['include-recent']),
4946
+ crossProject,
4921
4947
  };
4922
4948
  const result = await api.getContext(ctx, opts);
4923
4949
  // Early exit when there's nothing to render (matches pre-extraction behavior).
@@ -4930,8 +4956,13 @@ async function cmdContext(hippoRoot, args, flags) {
4930
4956
  // Format + framing are CLI rendering concerns; api.getContext doesn't see them.
4931
4957
  const format = String(flags['format'] ?? 'markdown');
4932
4958
  const framing = String(flags['framing'] ?? 'observe');
4933
- // Adapter: ContextResultEntry -> the print-helper input shape.
4934
- const renderItems = result.entries.map((r) => ({
4959
+ // Adapter: ContextResultEntry -> the print-helper input shape. v39:
4960
+ // cross-project inclusions (only present under --cross-project or with
4961
+ // isolation disabled via crossProject) render in their own demarcated
4962
+ // section so they can never masquerade as project memory.
4963
+ const mainEntries = result.entries.filter((r) => r.category !== 'cross-project');
4964
+ const crossEntries = result.entries.filter((r) => r.category === 'cross-project');
4965
+ const renderItems = mainEntries.map((r) => ({
4935
4966
  entry: r.entry,
4936
4967
  score: r.score,
4937
4968
  tokens: r.tokens,
@@ -4946,6 +4977,8 @@ async function cmdContext(hippoRoot, args, flags) {
4946
4977
  confidence: r.entry.confidence,
4947
4978
  content: r.entry.content,
4948
4979
  global: r.isGlobal ?? false,
4980
+ origin: r.origin ?? null,
4981
+ category: r.category ?? null,
4949
4982
  }));
4950
4983
  console.log(JSON.stringify({
4951
4984
  query: query || '*',
@@ -4973,6 +5006,7 @@ async function cmdContext(hippoRoot, args, flags) {
4973
5006
  if (renderItems.length > 0) {
4974
5007
  printContextMarkdown(renderItems, result.tokens, framing);
4975
5008
  }
5009
+ printCrossProjectSection(crossEntries);
4976
5010
  }
4977
5011
  finally {
4978
5012
  console.log = realLog;
@@ -5002,6 +5036,7 @@ async function cmdContext(hippoRoot, args, flags) {
5002
5036
  if (renderItems.length > 0) {
5003
5037
  printContextMarkdown(renderItems, result.tokens, framing);
5004
5038
  }
5039
+ printCrossProjectSection(crossEntries);
5005
5040
  // Ambient state summary (CLI-side: api.getContext doesn't load all entries
5006
5041
  // post-selection, so we re-load here for the landscape summary).
5007
5042
  const ambientConfig = loadConfig(hippoRoot);
@@ -5009,11 +5044,17 @@ async function cmdContext(hippoRoot, args, flags) {
5009
5044
  const tenantId = ctx.tenantId;
5010
5045
  const globalRoot = getGlobalRoot();
5011
5046
  const hasGlobalForAmbient = isInitialized(globalRoot);
5047
+ // v39: the landscape summary must reflect the same admission policy as
5048
+ // the injected entries - otherwise excluded rows leak back in as
5049
+ // aggregate signal (codex P2-13).
5050
+ const isolationOff = ambientConfig.contextProjectIsolation === false;
5051
+ const ambientCurrentName = resolveProjectIdentity(process.cwd()).name;
5052
+ const ambientAdmit = (e) => api.ambientAdmitEntry(e, ambientCurrentName, crossProject || isolationOff);
5012
5053
  const localForAmbient = isInitialized(hippoRoot)
5013
- ? loadAllEntries(hippoRoot, tenantId).filter(e => !e.superseded_by)
5054
+ ? loadAllEntries(hippoRoot, tenantId).filter(e => !e.superseded_by).filter(ambientAdmit)
5014
5055
  : [];
5015
5056
  const globalForAmbient = hasGlobalForAmbient
5016
- ? loadAllEntries(globalRoot, tenantId).filter(e => !e.superseded_by)
5057
+ ? loadAllEntries(globalRoot, tenantId).filter(e => !e.superseded_by).filter(ambientAdmit)
5017
5058
  : [];
5018
5059
  const allForAmbient = [...localForAmbient, ...globalForAmbient];
5019
5060
  if (allForAmbient.length > 0) {
@@ -5023,9 +5064,24 @@ async function cmdContext(hippoRoot, args, flags) {
5023
5064
  }
5024
5065
  }
5025
5066
  }
5067
+ /**
5068
+ * v39: render cross-project inclusions under an explicit header so agents
5069
+ * (and humans) can tell borrowed context from project memory. Only ever
5070
+ * non-empty when the caller passed --cross-project or disabled isolation.
5071
+ */
5072
+ function printCrossProjectSection(items) {
5073
+ if (items.length === 0)
5074
+ return;
5075
+ console.log(`\n## Other-project memory (explicitly requested, ${items.length} entries)\n`);
5076
+ for (const item of items) {
5077
+ const originLabel = item.origin === null || item.origin === '' ? 'unknown-origin' : item.origin;
5078
+ const tagStr = item.entry.tags.length > 0 ? ` [${item.entry.tags.join(', ')}]` : '';
5079
+ console.log(`- **[${originLabel}]** ${item.entry.content}${tagStr}`);
5080
+ }
5081
+ }
5026
5082
  /** @internal — exported for snapshot tests (tests/cli-context-render-snapshot.test.ts). NOT a stable public API. */
5027
5083
  export function printContextMarkdown(items, totalTokens, framing = 'observe') {
5028
- const now = new Date();
5084
+ const now = evalNow();
5029
5085
  console.log(`## Project Memory (${items.length} entries, ${totalTokens} tokens)\n`);
5030
5086
  for (const item of items) {
5031
5087
  const e = item.entry;
@@ -5320,6 +5376,54 @@ function cmdImport(hippoRoot, args, flags) {
5320
5376
  extraTags,
5321
5377
  hippoRoot,
5322
5378
  };
5379
+ // K1 vault import: a FOLDER importer that mirrors the connector pattern
5380
+ // (kind='raw' + tag provenance + archiveRaw deletions), so it dispatches
5381
+ // separately from the single-file `importer` function-pointer slot below.
5382
+ // It writes through api.remember/archiveRaw which are tenant-scoped, so we
5383
+ // resolve the tenant and pass it through. --global is not supported for
5384
+ // vault import (the connector raw-archive path is tenant-local).
5385
+ if (flags['vault']) {
5386
+ const folderPath = String(flags['vault']);
5387
+ if (!fs.existsSync(folderPath) || !fs.statSync(folderPath).isDirectory()) {
5388
+ console.error(`Vault folder not found (or not a directory): ${folderPath}`);
5389
+ process.exit(1);
5390
+ }
5391
+ if (useGlobal) {
5392
+ console.error('hippo import --vault does not support --global (raw rows are tenant-local).');
5393
+ process.exit(1);
5394
+ }
5395
+ if (typeof flags['name'] !== 'string' || !flags['name'].trim()) {
5396
+ // --name is the vault identity key for the destructive source-deletion sync;
5397
+ // inferring it from the folder basename let same-basename vaults collide and
5398
+ // clobber each other (codex R10 P2). A valueless `--name` parses as boolean
5399
+ // true, and String(true) === "true" would silently import under vault:true:*
5400
+ // - reject a non-string so it fails fast instead (codex R11 P2).
5401
+ console.error('hippo import --vault requires --name <vault> (a non-empty identity key for source-deletion sync).');
5402
+ process.exit(1);
5403
+ }
5404
+ if (flags['scope'] !== undefined && (typeof flags['scope'] !== 'string' || !flags['scope'].trim())) {
5405
+ // Same valueless-flag trap: a bare `--scope` must not become scope "true".
5406
+ // Example uses the source-prefixed private form, since a bare `private` scope
5407
+ // is NOT treated as private by recall and importVault rejects it (R13 P2).
5408
+ console.error('hippo import --vault: --scope requires a value (e.g. --scope vault:private:notes).');
5409
+ process.exit(1);
5410
+ }
5411
+ const tenantId = resolveTenantId({});
5412
+ const vaultOptions = {
5413
+ ...importOptions,
5414
+ tenantId,
5415
+ name: flags['name'] ? String(flags['name']) : undefined,
5416
+ scope: flags['scope'] ? String(flags['scope']) : undefined,
5417
+ };
5418
+ const vaultResult = importVault(folderPath, vaultOptions);
5419
+ console.log(`\nImport Vault: ${folderPath}${dryRun ? ' (dry run - no writes)' : ''}`);
5420
+ console.log(` Notes found: ${vaultResult.total}`);
5421
+ console.log(` ${dryRun ? 'Would import: ' : 'Imported: '}${vaultResult.imported}`);
5422
+ console.log(` Skipped (unchanged): ${vaultResult.skipped}`);
5423
+ console.log(` ${dryRun ? 'Would archive: ' : 'Archived (removed): '}${vaultResult.archived ?? 0}`);
5424
+ console.log(` Store: ${hippoRoot}`);
5425
+ return;
5426
+ }
5323
5427
  // Determine which importer to use based on flag
5324
5428
  let filePath;
5325
5429
  let importer;
@@ -5356,7 +5460,7 @@ function cmdImport(hippoRoot, args, flags) {
5356
5460
  importerName = 'File';
5357
5461
  }
5358
5462
  if (!filePath || !importer) {
5359
- console.error('Usage: hippo import <--chatgpt|--claude|--cursor|--file|--markdown> <path>');
5463
+ console.error('Usage: hippo import <--chatgpt|--claude|--cursor|--file|--markdown|--vault> <path>');
5360
5464
  process.exit(1);
5361
5465
  }
5362
5466
  if (!fs.existsSync(filePath)) {
@@ -5412,15 +5516,17 @@ function cmdPromote(hippoRoot, id) {
5412
5516
  // ---------------------------------------------------------------------------
5413
5517
  // Sync command
5414
5518
  // ---------------------------------------------------------------------------
5415
- function cmdSync(hippoRoot) {
5519
+ function cmdSync(hippoRoot, flags = {}) {
5416
5520
  requireInit(hippoRoot);
5417
5521
  const globalRoot = getGlobalRoot();
5418
5522
  if (!isInitialized(globalRoot)) {
5419
5523
  console.log('No global store found. Run `hippo init --global` first.');
5420
5524
  return;
5421
5525
  }
5422
- const count = syncGlobalToLocal(hippoRoot, globalRoot);
5423
- console.log(`Synced ${count} global memories into local project.`);
5526
+ // v39: other-project rows are skipped by default; secrets always are.
5527
+ const includeCrossProject = flags['cross-project'] === true;
5528
+ const count = syncGlobalToLocal(hippoRoot, globalRoot, { includeCrossProject });
5529
+ console.log(`Synced ${count} global memories into local project.${includeCrossProject ? '' : ' (other-project rows skipped; use --cross-project to include them)'}`);
5424
5530
  }
5425
5531
  // ---------------------------------------------------------------------------
5426
5532
  // Hook install/uninstall
@@ -5436,7 +5542,11 @@ const HOOKS = {
5436
5542
  content: `
5437
5543
  ## Project Memory (Hippo)
5438
5544
 
5439
- Before starting work, load relevant context:
5545
+ Pinned rules and recent writes auto-inject at every prompt via the installed
5546
+ UserPromptSubmit hook; never re-run that part manually. At the START of a
5547
+ task (not per prompt), additionally load task-specific context: git-aware
5548
+ recall over the full store that per-prompt injection does not cover. Also
5549
+ run it if the hook is not installed:
5440
5550
  \`\`\`bash
5441
5551
  hippo context --auto --budget 1500
5442
5552
  \`\`\`
@@ -7076,6 +7186,9 @@ Commands:
7076
7186
  --cursor <path> Import from .cursorrules or .cursor/rules
7077
7187
  --file <path> Import from any markdown or text file
7078
7188
  --markdown <path> Import from structured MEMORY.md / AGENTS.md
7189
+ --vault <path> Import a markdown-vault FOLDER as kind='raw' notes
7190
+ (Obsidian/Foam/Dendron). Requires --name <vault>.
7191
+ [--scope <scope>]
7079
7192
  --dry-run Preview without writing
7080
7193
  --global Write to global store ($HIPPO_HOME or ~/.hippo/)
7081
7194
  --tag <tag> Add extra tag (repeatable)
@@ -7670,7 +7783,7 @@ async function main() {
7670
7783
  break;
7671
7784
  }
7672
7785
  case 'sync':
7673
- cmdSync(hippoRoot);
7786
+ cmdSync(hippoRoot, flags);
7674
7787
  break;
7675
7788
  case 'share': {
7676
7789
  const shareId = args[0];