hippo-memory 1.52.9 → 1.53.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 (76) hide show
  1. package/README.md +47 -13
  2. package/dist/agent-memories/apply.d.ts +47 -0
  3. package/dist/agent-memories/apply.js +253 -0
  4. package/dist/agent-memories/claude-code.d.ts +11 -0
  5. package/dist/agent-memories/claude-code.js +113 -0
  6. package/dist/agent-memories/codex.d.ts +3 -0
  7. package/dist/agent-memories/codex.js +47 -0
  8. package/dist/agent-memories/copilot.d.ts +3 -0
  9. package/dist/agent-memories/copilot.js +125 -0
  10. package/dist/agent-memories/files.d.ts +37 -0
  11. package/dist/agent-memories/files.js +77 -0
  12. package/dist/agent-memories/folder-store.d.ts +17 -0
  13. package/dist/agent-memories/folder-store.js +44 -0
  14. package/dist/agent-memories/gemini.d.ts +3 -0
  15. package/dist/agent-memories/gemini.js +103 -0
  16. package/dist/agent-memories/git.d.ts +8 -0
  17. package/dist/agent-memories/git.js +11 -0
  18. package/dist/agent-memories/keys.d.ts +9 -0
  19. package/dist/agent-memories/keys.js +20 -0
  20. package/dist/agent-memories/legacy.d.ts +17 -0
  21. package/dist/agent-memories/legacy.js +45 -0
  22. package/dist/agent-memories/markdown.d.ts +13 -0
  23. package/dist/agent-memories/markdown.js +123 -0
  24. package/dist/agent-memories/openclaw.d.ts +3 -0
  25. package/dist/agent-memories/openclaw.js +42 -0
  26. package/dist/agent-memories/plan.d.ts +78 -0
  27. package/dist/agent-memories/plan.js +123 -0
  28. package/dist/agent-memories/qwen-code.d.ts +5 -0
  29. package/dist/agent-memories/qwen-code.js +50 -0
  30. package/dist/agent-memories/report.d.ts +52 -0
  31. package/dist/agent-memories/report.js +88 -0
  32. package/dist/agent-memories/source.d.ts +16 -0
  33. package/dist/agent-memories/source.js +32 -0
  34. package/dist/agent-memories/sync.d.ts +33 -0
  35. package/dist/agent-memories/sync.js +336 -0
  36. package/dist/agent-memories/tools.d.ts +33 -0
  37. package/dist/agent-memories/tools.js +19 -0
  38. package/dist/agent-memories/types.d.ts +42 -0
  39. package/dist/agent-memories/types.js +2 -0
  40. package/dist/api.d.ts +2 -2
  41. package/dist/api.js +28 -29
  42. package/dist/audit.d.ts +4 -3
  43. package/dist/audit.js +10 -6
  44. package/dist/capture.d.ts +11 -22
  45. package/dist/capture.js +76 -81
  46. package/dist/cli.d.ts +0 -2
  47. package/dist/cli.js +133 -170
  48. package/dist/compaction-items.d.ts +18 -0
  49. package/dist/compaction-items.js +60 -0
  50. package/dist/compaction-record.d.ts +94 -0
  51. package/dist/compaction-record.js +573 -0
  52. package/dist/config.d.ts +4 -0
  53. package/dist/config.js +13 -0
  54. package/dist/consolidate.d.ts +0 -2
  55. package/dist/consolidate.js +3 -36
  56. package/dist/db.d.ts +5 -1
  57. package/dist/db.js +40 -8
  58. package/dist/dedupe.js +3 -2
  59. package/dist/doctor.js +34 -2
  60. package/dist/dormant.d.ts +5 -3
  61. package/dist/dormant.js +9 -0
  62. package/dist/gated-write.d.ts +9 -0
  63. package/dist/gated-write.js +24 -0
  64. package/dist/hooks.d.ts +4 -2
  65. package/dist/hooks.js +8 -7
  66. package/dist/memory.d.ts +19 -2
  67. package/dist/memory.js +32 -3
  68. package/dist/shared.js +10 -7
  69. package/dist/store.d.ts +14 -2
  70. package/dist/store.js +75 -19
  71. package/dist/version.d.ts +1 -1
  72. package/dist/version.js +1 -1
  73. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  74. package/extensions/openclaw-plugin/package.json +1 -1
  75. package/openclaw.plugin.json +1 -1
  76. package/package.json +1 -1
package/README.md CHANGED
@@ -22,6 +22,8 @@ npm install -g hippo-memory && hippo init
22
22
 
23
23
  Setting up every git repo under a folder in one go is a second step. The [Quick start](#quick-start) says what it changes, then gives the command.
24
24
 
25
+ Package installation alone does not enable automatic preservation on every agent. Complete the documented setup and required host trust; capture and compaction coverage depend on the integration. See [automatic-save coverage](#does-installation-automatically-save-before-compaction).
26
+
25
27
  Having an AI agent install it? Point it at [llms-install.md](llms-install.md): it installs, wires hippo into the agents it finds, and verifies with `hippo doctor`.
26
28
 
27
29
  ```
@@ -83,7 +85,7 @@ npm install -g hippo-memory
83
85
  hippo init
84
86
  ```
85
87
 
86
- **Optional: many repos at once.** Read what it changes first. `hippo init --scan <folder>` looks for git repos in the folder and up to three levels below it, skipping dot-folders and `node_modules`. Each repo gets a `.hippo/` store, seeded with lessons from the last 365 days of its commits, and is added to the daily run's list. For the agents it finds, it installs the same user-level hooks as `hippo init`: 7 Claude Code hook entries in `~/.claude/settings.json` and the OpenCode plugin. It also sets up the daily 6:15am run, a crontab line on Linux and macOS or a scheduled task on Windows. It adds no block to any repo's `CLAUDE.md` or `AGENTS.md`. `--no-hooks`, `--no-schedule` and `--no-learn` leave out the hooks, the daily run and the history import.
88
+ **Optional: many repos at once.** Read what it changes first. `hippo init --scan <folder>` looks for git repos in the folder and up to three levels below it, skipping dot-folders and `node_modules`. Each repo gets a `.hippo/` store, seeded with lessons from the last 365 days of its commits and with its [agent memories](#agent-memories), and is added to the daily run's list. For the agents it finds, it installs the same user-level hooks as `hippo init`: 7 Claude Code hook entries in `~/.claude/settings.json` and the OpenCode plugin. It also sets up the daily 6:15am run, a crontab line on Linux and macOS or a scheduled task on Windows. It adds no block to any repo's `CLAUDE.md` or `AGENTS.md`. `--no-hooks`, `--no-schedule` and `--no-learn` leave out the hooks, the daily run and the history import.
87
89
 
88
90
  ```bash
89
91
  hippo init --scan ~
@@ -92,7 +94,7 @@ hippo init --scan ~
92
94
  After setup, `hippo sleep` runs when a Claude Code or OpenCode session ends, and in the daily 6:15am job for every project. Codex runs it at session end only if you installed its wrapper. It does five things:
93
95
 
94
96
  1. **Learns** from today's git commits
95
- 2. **Imports** new entries from the project's Claude Code auto memory
97
+ 2. **Imports** what your coding agents remember, about this project and about you ([Agent memories](#agent-memories))
96
98
  3. **Consolidates** memories (decay, merge, prune)
97
99
  4. **Deduplicates** identical memories, keeping the stronger copy
98
100
  5. **Shares** high-value lessons to a global store so they surface in every project
@@ -118,7 +120,7 @@ Run `hippo init` inside one project. This is everything it writes, in the projec
118
120
  - **OpenCode,** when the project has `.opencode/` or `opencode.json`: a plugin at `~/.config/opencode/plugins/hippo.ts`.
119
121
  - **Codex,** when the project has `AGENTS.md` or `.codex` and Codex is installed (`$CODEX_HOME`, else `~/.codex`, exists): 2 hook entries in Codex's `hooks.json`, one on UserPromptSubmit that sends your pinned memories plus the five most recent ones with every prompt and one on SessionStart after a compaction. **Codex runs them only after you trust them once in `/hooks`.** Init also prints `hippo hook install codex`, the opt-in that wraps the Codex launcher to capture sessions; `hippo hook uninstall codex` removes hippo's hooks and the wrapper.
120
122
  - **A daily run at 6:15am,** one per machine: a crontab line on Linux and macOS, a scheduled task named `hippo-daily-runner` on Windows. It runs `hippo learn --git --days 1` and then `hippo sleep` in every project listed in `~/.hippo/workspaces.json`, and init adds this project to that list.
121
- - **Claude Code auto memory.** On the first run, the notes with YAML front matter in this project's own folder under `~/.claude/projects/` are imported into its store. A file that looks like it holds a secret is skipped.
123
+ - **Agent memories.** On every run, the notes your coding agents keep about this project go into its store, and the ones about you go into the global store. [Agent memories](#agent-memories) lists what is read.
122
124
 
123
125
  ```bash
124
126
  cd my-project
@@ -133,7 +135,28 @@ hippo init
133
135
  # Scheduled machine-level daily runner (6:15am) via crontab
134
136
  ```
135
137
 
136
- To leave parts out: `--no-hooks` skips the instruction files and hooks, `--no-schedule` the daily run, and `--no-learn` the git history and auto memory import. `HIPPO_SKIP_AUTO_INTEGRATIONS=1` skips the same files and hooks that `--no-hooks` does.
138
+ To leave parts out: `--no-hooks` skips the instruction files and hooks, `--no-schedule` the daily run, and `--no-learn` the git history and agent memory import. `HIPPO_SKIP_AUTO_INTEGRATIONS=1` skips the same files and hooks that `--no-hooks` does.
139
+
140
+ ### Agent memories
141
+
142
+ Most coding agents now keep their own notes between sessions. Hippo reads them, whatever the tool, so what one agent learned reaches the others. It reads files only and never writes to another tool's folders.
143
+
144
+ When: `hippo init` (every run), `init --scan`, `init --global`, `hippo setup`, every `hippo sleep` and the daily run. At session end a folder with its own store gets it through sleep; a folder without one sends its project's notes to the global store, marked with the project's name. After a Claude Code compaction, the session's own notes folder is read as well.
145
+
146
+ What is read, per tool (each tool's own environment variables and settings decide where its home is):
147
+
148
+ - **Claude Code:** the project's auto memory notes under `~/.claude/projects/<project>/memory/` (front matter required, `MEMORY.md` skipped), and the `autoMemoryDirectory` folder from your user settings.
149
+ - **Codex:** the User Profile, preferences and tips in `~/.codex/memories/memory_summary.md`.
150
+ - **Gemini CLI:** the "Gemini Added Memories" section of `~/.gemini/GEMINI.md`, and the project's auto memory folder when that feature is on.
151
+ - **GitHub Copilot Chat in VS Code:** the memory tool's user memories and the repository memories of this project's workspace.
152
+ - **OpenClaw:** the workspace's `MEMORY.md`.
153
+ - **Qwen Code:** the project's auto memory folder and your user memories.
154
+
155
+ Each imported memory follows its note. It stays while the note exists, is replaced when the note changes, and is set aside as dormant when the note is deleted (`hippo dormant` lists it and can restore it). A note shorter than 10 characters, one that looks like it holds a secret (an API key, a password, an auth header or a token), and one whose text you rejected with `hippo reject` are skipped. Email addresses are stored masked, and notes are cut at 1,500 characters.
156
+
157
+ Not read: Windsurf (the file format is not documented, and Cascade reached end of life on 1 July 2026); Cursor, Copilot CLI and GitHub's Copilot Memory (the memories live on the vendor's servers); Kiro (the local store is not documented); Cline and Roo memory banks (files in the repository, which `hippo import --markdown` covers); Amp, Aider, Continue, OpenCode and pi (no memory feature found).
158
+
159
+ `hippo import --agents` runs the import by hand; in a folder without a store it does what session end does there. With `--dry-run` it shows each tool's home, the folders found and what would change, and writes nothing. To choose tools, set `"agentMemories": { "tools": ["claude-code", "codex"] }` in `.hippo/config.json` (`[]` turns the import off), or `HIPPO_AGENT_MEMORY_TOOLS=claude-code,codex` in the environment (`none` turns it off), which wins over config.
137
160
 
138
161
  ---
139
162
 
@@ -508,8 +531,9 @@ detector flags is deleted, never kept dormant, and a dormant memory nobody resto
508
531
  `dormant.retentionDays` (default 180, `0` keeps them forever) is deleted for good. Rejecting
509
532
  a value (`hippo reject`) removes its dormant copies too. To delete faded memories straight
510
533
  away as before, set `{"dormant":{"enabled":false}}` in `.hippo/config.json`. Sleep never
511
- removes pinned memories or raw receipts (Slack, GitHub, vault imports) either way, and
512
- duplicate removal and junk cleanup still delete.
534
+ removes pinned memories, raw receipts (Slack, GitHub, vault imports) or the memories a
535
+ Claude Code compaction saved either way, and duplicate removal and junk cleanup still
536
+ delete other memories. `hippo forget` still deletes a compaction memory.
513
537
 
514
538
  **See what memory costs in tokens.** Every block of memory text hippo hands an agent (the
515
539
  per-prompt hook, the block `hippo compact-resume` restores after compaction, `hippo context`,
@@ -734,7 +758,7 @@ On `heartbeat`, `block`, `review` and `complete`, a given `--run` is checked aga
734
758
  | OpenCode | `.opencode/` or `opencode.json` | `AGENTS.md` + TS plugin at `~/.config/opencode/plugins/hippo.ts` (subscribes to `session.idle` + `session.created`) |
735
759
  | Pi | `.pi` or `.pi/agent` | `AGENTS.md`; copy the [Pi extension](https://github.com/kitfunso/hippo-memory/tree/master/extensions/pi-extension) for session hooks |
736
760
 
737
- Init patches an instruction file only if it already exists. It also sets up a daily run and imports the project's Claude Code auto memory; [What hippo init changes](#what-hippo-init-changes) lists everything.
761
+ Init patches an instruction file only if it already exists. It also sets up a daily run and imports your coding agents' own memories; [What hippo init changes](#what-hippo-init-changes) lists everything.
738
762
 
739
763
  ### Manual install
740
764
 
@@ -760,11 +784,13 @@ For Claude Code, it also adds 7 hook entries to `~/.claude/settings.json`:
760
784
  - a `SessionEnd` hook that runs `hippo sleep` and then `hippo capture` when the session exits. Capture matches the last 20 user and 10 assistant messages of the transcript against word patterns for decisions, rules, errors and preferences. It uses no model and does not read earlier turns, so record lessons with `hippo remember` as you go.
761
785
  - a `SessionStart` hook that prints the previous session's consolidation output
762
786
  - a `UserPromptSubmit` hook that runs `hippo context --pinned-only --include-recent 5 --format additional-context` every turn. It re-injects pinned memories (`hippo remember <text> --pin`) plus the 5 newest memories in the store, so fresh same-session lessons appear on the next prompt before you pin them. It does not read your prompt unless you set `{"pinnedInject":{"promptRecall":true}}`, which swaps the 5 newest for memories that match the prompt. The block is rendered without live strength percentages, so it stays byte-identical while its memories do not change, and it is sent only when it changed since the session's last prompt: an unchanged block is skipped, resent every 10 skips (`pinnedInject.refreshTurns`, `0` never resends) and resent after compaction. `{"pinnedInject":{"skipUnchanged":false}}` sends it every turn as before. Opt out entirely with `{"pinnedInject":{"enabled":false}}` in `.hippo/config.json`.
763
- - a `PreCompact` hook that runs `hippo pre-compact` before the transcript gets summarized. It saves a working-state snapshot (task/summary/next step) so mid-session compaction can't drop it; the `SessionEnd` hook still owns extracting durable memories.
787
+ - a `PreCompact` hook that runs `hippo pre-compact` before the transcript gets summarized. It records the compaction in the store, saves a working-state snapshot (task/summary/next step) so mid-session compaction can't drop it, and asks the summariser to end its summary with a "Memories for hippo" list: the lessons, decisions and corrections from the session that should outlive it. The `SessionEnd` hook still owns extracting durable memories from the transcript.
764
788
  - a second `SessionStart` hook (matcher `compact`) that runs `hippo compact-resume`, printing that snapshot back into context right after compaction, if it is under 15 minutes old.
765
- - a `PostCompact` hook that runs `hippo post-compact`, which tells you what was saved ("Hippo saved your task snapshot before compacting."). It prints nothing when nothing was saved.
789
+ - a `PostCompact` hook that runs `hippo post-compact`. It keeps the summary in the store with secrets scrubbed, and saves each item of that list as a memory that sleep never deletes: at most 10 per compaction, skipping an item an earlier compaction already saved and any item that looks like a secret. An item over 500 characters stays in the compaction's record only. It then prints one line, such as "Hippo saved 3 memories from this compaction and restored your task snapshot." If the store is busy, the summary waits in the store's `compactions-spool/` folder and `hippo sleep` finishes the save; `hippo doctor` names any compaction left unfinished for over 10 minutes. The session that compacted does not have those memories injected back into its own prompts, since it just read them in the summary; `hippo recall` still finds them. The hook prints nothing when there is no store to save to.
766
790
  - a `PostToolUseFailure` hook that runs `hippo capture-error`, which stores a failed tool call as an error memory. It skips interrupts, declined permissions and searches that found nothing, and stores a repeated failure once. It also logs every failure, stored or not, for `hippo failures`: the session, the tool and hashes of the error, never its text. A hash is not anonymous, since anyone who guesses an error's text can check it against the hash. The log keeps 90 days.
767
791
 
792
+ Only Claude Code saves memories at a compaction: hippo installs no `PreCompact` or `PostCompact` hook for Codex, Cursor, OpenCode, OpenClaw or Pi.
793
+
768
794
  To remove: `hippo hook uninstall claude-code`
769
795
 
770
796
  For Codex, it adds two hooks to `$CODEX_HOME/hooks.json` (else `~/.codex/hooks.json`) and keeps every hook already there:
@@ -885,7 +911,7 @@ The rows from where the data lives to the graph were checked against each tool's
885
911
  | Feature | Hippo | [MemPalace](https://github.com/milla-jovovich/mempalace) | [Mem0](https://github.com/mem0ai/mem0) | [Basic Memory](https://github.com/basicmachines-co/basic-memory) | [gbrain](https://hermesatlas.com/projects/garrytan/gbrain) | [Zep](https://www.getzep.com/) | [Letta](https://github.com/letta-ai/letta-code) | [Cognee](https://www.cognee.ai/) | [Memoria](https://github.com/matrixorigin/Memoria) | [EverMind](https://evermind.ai/) |
886
912
  |---------|-------|-----------|------|-------------|--------|-----|-------|--------|---------|----------|
887
913
  | Where your data lives | Your machine or your server (SQLite) | Your machine (ChromaDB by default) | Where you run it, or Mem0's cloud | Your machine (Markdown files); cloud optional | Your machine (PGLite) or your Postgres | Zep's cloud (your own cloud on Enterprise) | Your machine; cloud backup with /login | Your machine by default; Cognee Cloud optional | Memoria Cloud, or self-hosted (Docker or embedded) | Your machine by default; EverOS Cloud optional |
888
- | Managed multi-user service | No (self-hosted, with tenants and API keys; the commercial edition adds hosted SaaS) | ? | Yes (hosted platform) | Yes (Teams) | ? (self-hosted server with OAuth) | Yes (Zep Cloud) | ? (cloud backup with /login) | Yes (Cognee Cloud) | ? (Memoria Cloud) | Yes (EverOS Cloud) |
914
+ | Managed multi-user service | No (self-hosted, with tenants and API keys; hosted SaaS is planned for the commercial edition) | ? | Yes (hosted platform) | Yes (Teams) | ? (self-hosted server with OAuth) | Yes (Zep Cloud) | ? (cloud backup with /login) | Yes (Cognee Cloud) | ? (Memoria Cloud) | Yes (EverOS Cloud) |
889
915
  | Needs an account or model key | No | No (core path) | Yes (model key; account for the platform) | No (account for the cloud) | No (keyless mode) | Yes (Zep account; Graphiti needs a model key) | Yes (your own model keys) | No (local) | ? (account for Memoria Cloud) | Yes (an LLM key, OpenRouter) |
890
916
  | License | MIT | MIT | Apache-2.0 | AGPL-3.0 | MIT | Proprietary cloud (Graphiti: Apache-2.0) | Apache-2.0 | Apache-2.0 | Apache-2.0 | Apache-2.0 (EverOS) + cloud |
891
917
  | Runtime | Node.js 22.16+, no runtime deps | Python 3.9+ (ChromaDB by default) | Python or Node.js (server: Postgres + pgvector) | Python (SQLite by default) | Bun (PGLite or Postgres + pgvector) | Managed service (Graphiti: Python + a graph database) | Node.js (npm) | Python (graph and vector stores, or Postgres) | A CLI binary + a MatrixOne database | Python (SQLite + LanceDB) |
@@ -1030,7 +1056,7 @@ node run.mjs --adapter all
1030
1056
 
1031
1057
  ### How do I give Claude Code memory between sessions?
1032
1058
 
1033
- Run `npm install -g hippo-memory`, then `hippo init` in the project. If the project has a `CLAUDE.md`, init adds a short block telling Claude to run `hippo context --auto` when a session starts. It also adds 7 hook entries to Claude Code's settings that keep your pinned memories plus the five most recent ones in context, save a task snapshot before compaction, store failed tool calls as lessons, and run `hippo sleep` when the session ends, and it sets up a daily 6:15am run. [What hippo init changes](#what-hippo-init-changes) lists everything. The [Claude Code plugin](https://github.com/kitfunso/hippo-memory/tree/master/extensions/claude-code-plugin) is the alternative to these hooks; use one, not both. To set up every git repo up to three folders below your home directory at once, know what the scan changes first: each repo gets its own store, seeded from a year of its git history, the same hooks go in when one of those repos uses Claude Code, and the daily run is set up, but no block goes into any repo's `CLAUDE.md`. The command is `hippo init --scan ~`; run `hippo init` in the projects where you want the block.
1059
+ Run `npm install -g hippo-memory`, then `hippo init` in the project. If the project has a `CLAUDE.md`, init adds a short block telling Claude to run `hippo context --auto` when a session starts. It also adds 7 hook entries to Claude Code's settings that keep your pinned memories plus the five most recent ones in context, save a task snapshot and the memories a compaction summary lists, store failed tool calls as lessons, and run `hippo sleep` when the session ends, and it sets up a daily 6:15am run. [What hippo init changes](#what-hippo-init-changes) lists everything. The [Claude Code plugin](https://github.com/kitfunso/hippo-memory/tree/master/extensions/claude-code-plugin) is the alternative to these hooks; use one, not both. To set up every git repo up to three folders below your home directory at once, know what the scan changes first: each repo gets its own store, seeded from a year of its git history, the same hooks go in when one of those repos uses Claude Code, and the daily run is set up, but no block goes into any repo's `CLAUDE.md`. The command is `hippo init --scan ~`; run `hippo init` in the projects where you want the block.
1034
1060
 
1035
1061
  ### How do I give Cursor memory between sessions?
1036
1062
 
@@ -1044,6 +1070,14 @@ Run `npm install -g hippo-memory`, then `hippo init` in the project. If the proj
1044
1070
 
1045
1071
  `hippo init` detects Claude Code, Codex, Cursor, OpenClaw, OpenCode and Pi. It installs hooks for Claude Code and OpenCode, adds 2 hooks to Codex's `hooks.json` when Codex is installed (Codex runs them once you trust them in `/hooks`), and adds instructions to an existing `AGENTS.md` for Codex, Cursor, OpenClaw and Pi. It only patches instruction files that already exist. Any MCP client can use the [MCP server](#mcp-server), and other tools can call the CLI or the HTTP API that `hippo serve` starts.
1046
1072
 
1073
+ ### Does installation automatically save before compaction?
1074
+
1075
+ Package installation alone does not enable automatic preservation on every agent. Complete the documented setup and required host trust; capture and compaction coverage depend on the integration.
1076
+
1077
+ With the Claude Code hooks or native plugin configured, PreCompact saves a derivable working-state snapshot and a compaction record; PostCompact extracts the lessons listed in the compaction summary. A snapshot, a compaction record and a useful memory are different outputs, and post-compaction extraction is not a guarantee that every earlier lesson was saved before loss. Hippo's Codex hooks currently provide prompt delivery and post-compaction re-injection, with one-time trust in `/hooks`; they install no PreCompact save hook. Codex session-end capture requires the opt-in wrapper. MCP tool access and instruction files alone do not provide automatic lifecycle capture.
1078
+
1079
+ Follow the [integration recipes](https://github.com/kitfunso/hippo-memory/tree/master/integrations) for the exact agent and mode. Automatic pre-loss preservation across all supported agents is planned under AZ4-AZ6 in the [roadmap](https://github.com/kitfunso/hippo-memory/blob/master/ROADMAP.md#current-execution-index); it is not a universal install-only capability today.
1080
+
1047
1081
  ### Can I use hippo as an MCP memory server?
1048
1082
 
1049
1083
  Yes. `hippo mcp` runs the server over stdio, and `npx -y hippo-memory mcp` runs it without a global install. Add it to the MCP config of Claude Desktop, Cursor, Windsurf (now Devin Desktop), Cline or any other client (example [above](#mcp-server)); in Claude Code, run `claude mcp add hippo-memory -- hippo mcp`. The agent gets tools such as `hippo_recall`, `hippo_remember` and `hippo_outcome`.
@@ -1101,9 +1135,9 @@ The interesting problems:
1101
1135
 
1102
1136
  ## Open source and commercial
1103
1137
 
1104
- Hippo is open core, and the line is drawn by who pays. Everything an individual developer or a self-hosted team needs is in this repository under MIT: the CLI, the MCP server, hooks, connectors, the dashboard, tenants, API keys, roles, per-key scope grants, the audit log and zero-touch memory. Code published here stays MIT and stays here.
1138
+ Hippo is open core. This repository provides the MIT core for individual developers and self-hosted teams: the CLI, the MCP server, supported hooks and adapters, connectors, the dashboard, tenants, API keys, admin/member roles, per-key scope grants and the audit log. Automatic capture and delivery depend on the configured integration; the all-agent low-touch acceptance work remains planned. Code published here stays MIT and stays here.
1105
1139
 
1106
- A commercial edition for larger companies ships as a separate package under a commercial licence from KITFUNSO LTD. It adds SSO (OIDC and SAML sign-in), SCIM, an org admin view, the pilot report and telemetry join, SIEM export of the audit log, offline licence keys, hosted SaaS, and support with an SLA. Pull requests for those features belong there, not here; see [CONTRIBUTING.md](CONTRIBUTING.md).
1140
+ The commercial edition is planned; its private repository is a scaffold, not a released enterprise product. It is intended for larger companies as a separate package under a commercial licence from KITFUNSO LTD. Planned capabilities include SSO (OIDC and SAML sign-in), SCIM, organisation/team/project policy, an org admin view, the pilot report and telemetry join, SIEM export of the audit log, offline licence keys, hosted SaaS, and support with an SLA. Pull requests implementing those commercial features belong there; see [CONTRIBUTING.md](CONTRIBUTING.md). Release availability and independently verified benefit are separate milestones in the [roadmap](https://github.com/kitfunso/hippo-memory/blob/master/ROADMAP.md#current-execution-index).
1107
1141
 
1108
1142
  ## License
1109
1143
 
@@ -0,0 +1,47 @@
1
+ import type { DatabaseSyncLike } from '../db.js';
2
+ import { type MemoryEntry } from '../memory.js';
3
+ import { type Tally } from './report.js';
4
+ import type { AgentMemoryTool } from './tools.js';
5
+ import type { Container } from './types.js';
6
+ export declare const SYNC_ACTOR = "agent-memories";
7
+ export interface StoreSession {
8
+ readonly db: DatabaseSyncLike;
9
+ readonly hippoRoot: string;
10
+ readonly tenantId: string;
11
+ readonly baseHalfLifeDays: number;
12
+ /** Stamped on written rows; undefined lets the store stamp its own project. */
13
+ readonly originProject: string | undefined;
14
+ /** Whether the text is already stored live by another path, as seen where the new row goes. */
15
+ readonly isDuplicate: (text: string) => boolean;
16
+ readonly dryRun: boolean;
17
+ }
18
+ export interface ContainerWork {
19
+ readonly tool: AgentMemoryTool;
20
+ readonly container: Container;
21
+ /** `containerPrefix(tool, containerId)`: every row of the container has a source starting with it. */
22
+ readonly prefix: string;
23
+ /** Legacy rows that take an item's key as they are, by key (plan design 10, first round). */
24
+ readonly adopt: ReadonlyMap<string, readonly MemoryEntry[]>;
25
+ /** Legacy rows a new row of the key supersedes, by key (second round). */
26
+ readonly replace: ReadonlyMap<string, readonly MemoryEntry[]>;
27
+ }
28
+ export interface ContainerOutcome {
29
+ readonly tally: Tally;
30
+ /** Rows to mirror after commit, as they now stand. */
31
+ readonly mirror: readonly MemoryEntry[];
32
+ /** Rows set aside, whose mirrors are purged after commit. */
33
+ readonly purge: readonly string[];
34
+ }
35
+ /** Throws SQLITE_BUSY when another writer holds the store past its busy timeout; nothing is written then. */
36
+ export declare function syncContainer(s: StoreSession, work: ContainerWork): ContainerOutcome;
37
+ export type SetAsideWhy = 'note-gone' | 'note-changed' | 'handover';
38
+ export type SetAsideResult = {
39
+ readonly kind: 'untagged';
40
+ readonly entry: MemoryEntry;
41
+ } | {
42
+ readonly kind: 'dormant';
43
+ readonly id: string;
44
+ };
45
+ /** Design 6's set-aside on the caller's transaction: a pinned row only loses the tag, any other goes dormant, restorable. */
46
+ export declare function setAsideRow(db: DatabaseSyncLike, tag: string, row: MemoryEntry, why: SetAsideWhy): SetAsideResult;
47
+ //# sourceMappingURL=apply.d.ts.map
@@ -0,0 +1,253 @@
1
+ // One container's sync in one transaction on the caller's handle: lookup, plan, then every write (plan designs 6 to 8).
2
+ import { appendAuditEvent } from '../audit.js';
3
+ import { deleteDormantRow, dormantSnapshotsBySourcePrefix, insertDormantRow, readDormantSnapshot } from '../dormant.js';
4
+ import { gatedWrite } from '../gated-write.js';
5
+ import { Layer, calculateStrength, createMemory } from '../memory.js';
6
+ import { findRejectedValue, rejectionDigest } from '../rejection.js';
7
+ import { redactSecretsStrict } from '../secret-detect.js';
8
+ import { deleteEntryRowInTx, markSummaryDirtyInTx, selectLiveEntriesBySourcePrefix, setEntryTagsInTx, stampOriginProject } from '../store.js';
9
+ import { itemHash } from './keys.js';
10
+ import { planContainer } from './plan.js';
11
+ import { emptyTally } from './report.js';
12
+ import { MIN_ITEM_CHARS, itemSource, splitSource, storedText } from './source.js';
13
+ export const SYNC_ACTOR = 'agent-memories';
14
+ /** Throws SQLITE_BUSY when another writer holds the store past its busy timeout; nothing is written then. */
15
+ export function syncContainer(s, work) {
16
+ // A dry run takes no lock up front and rolls every write back.
17
+ s.db.exec(s.dryRun ? 'BEGIN' : 'BEGIN IMMEDIATE');
18
+ try {
19
+ const out = new ContainerRun(s, work).run();
20
+ s.db.exec(s.dryRun ? 'ROLLBACK' : 'COMMIT');
21
+ return out;
22
+ }
23
+ catch (err) {
24
+ try {
25
+ s.db.exec('ROLLBACK');
26
+ }
27
+ catch { /* already rolled back; keep the original error */ }
28
+ throw err;
29
+ }
30
+ }
31
+ /** Design 6's set-aside on the caller's transaction: a pinned row only loses the tag, any other goes dormant, restorable. */
32
+ export function setAsideRow(db, tag, row, why) {
33
+ const untagged = { ...row, tags: row.tags.filter((t) => t !== tag) };
34
+ const audit = (metadata) => appendAuditEvent(db, { tenantId: row.tenantId, actor: SYNC_ACTOR, op: 'agent_memory_set_aside', targetId: row.id, metadata });
35
+ if (row.pinned) {
36
+ setEntryTagsInTx(db, untagged);
37
+ audit({ why, untagged: true });
38
+ return { kind: 'untagged', entry: untagged };
39
+ }
40
+ const now = new Date();
41
+ // Sleep's dormant move skips kept rows, so the steps are written out here without its filter.
42
+ insertDormantRow(db, { entry: untagged, strength: calculateStrength(row, now), reason: 'source-deleted', dormantAt: now.toISOString() });
43
+ deleteEntryRowInTx(db, row, SYNC_ACTOR);
44
+ audit({ why });
45
+ return { kind: 'dormant', id: row.id };
46
+ }
47
+ class ContainerRun {
48
+ s;
49
+ w;
50
+ tally = emptyTally();
51
+ mirror = [];
52
+ purge = [];
53
+ rows = new Map();
54
+ items = new Map();
55
+ tag;
56
+ constructor(s, w) {
57
+ this.s = s;
58
+ this.w = w;
59
+ this.tag = w.tool.tag;
60
+ for (const item of w.container.items)
61
+ this.items.set(item.key, item);
62
+ }
63
+ run() {
64
+ this.adoptLegacy();
65
+ const live = selectLiveEntriesBySourcePrefix(this.s.db, this.s.tenantId, this.w.prefix);
66
+ for (const row of [...live, ...[...this.w.replace.values()].flat()])
67
+ this.rows.set(row.id, row);
68
+ const refusals = this.refusals();
69
+ const liveRows = live.map((row) => this.liveRow(row));
70
+ const plan = planContainer({
71
+ textKeyed: this.w.container.textKeyed,
72
+ items: this.w.container.items.map((item) => ({ key: item.key, hash: itemHash(item.text), refused: refusals.has(item.key) })),
73
+ skipped: this.w.container.skipped,
74
+ live: liveRows,
75
+ dormant: this.dormantRows(new Set(liveRows.map((r) => r.key)), refusals),
76
+ legacy: new Map([...this.w.replace].map(([key, rows]) => [key, rows.map((r) => r.id)])),
77
+ isDuplicate: (item) => this.s.isDuplicate(storedText(this.item(item.key).text)),
78
+ });
79
+ for (const reason of refusals.values())
80
+ this.tally[reason]++;
81
+ this.tally.unread += this.w.container.skipped.length;
82
+ this.apply(plan);
83
+ return { tally: this.tally, mirror: this.mirror, purge: this.purge };
84
+ }
85
+ apply(plan) {
86
+ this.tally.unchanged += plan.unchanged;
87
+ this.tally.duplicate += plan.duplicates;
88
+ for (const id of plan.retag)
89
+ this.retag(this.row(id));
90
+ for (const { id, by } of plan.collapse)
91
+ if (this.supersede(this.row(id), by))
92
+ this.tally.collapsed++;
93
+ for (const write of plan.writes)
94
+ this.write(write);
95
+ for (const { key, dormantId } of plan.restores)
96
+ this.restore(key, dormantId);
97
+ for (const id of plan.setAside)
98
+ this.setAside(this.row(id));
99
+ }
100
+ /** Same text as a current note: the legacy row takes the note's key, keeping its id, recall count and outcomes. */
101
+ adoptLegacy() {
102
+ for (const [key, rows] of this.w.adopt) {
103
+ const item = this.items.get(key);
104
+ if (item === undefined)
105
+ continue;
106
+ const source = itemSource(this.w.prefix, key, item.text);
107
+ for (const row of rows) {
108
+ const moved = this.s.db.prepare(`UPDATE memories SET source = ? WHERE id = ? AND tenant_id = ? AND source = ? AND superseded_by IS NULL`).run(source, row.id, row.tenantId, row.source);
109
+ if (Number(moved.changes ?? 0) === 0)
110
+ continue;
111
+ this.tally.adopted++;
112
+ this.mirror.push({ ...row, source });
113
+ }
114
+ }
115
+ }
116
+ refusals() {
117
+ const out = new Map();
118
+ for (const item of this.w.container.items) {
119
+ const reason = this.refusal(item);
120
+ if (reason !== null)
121
+ out.set(item.key, reason);
122
+ }
123
+ return out;
124
+ }
125
+ // The rejection lookup reads the capped text, as the write would store it, so a rejected note writes no audit row.
126
+ refusal(item) {
127
+ if (item.text.trim().length < MIN_ITEM_CHARS)
128
+ return 'short';
129
+ // Imported rows reach prompts, so the bar is text leaving the machine: Bearer headers and JWTs count too.
130
+ if (redactSecretsStrict(item.text) !== item.text)
131
+ return 'secret';
132
+ if (findRejectedValue(this.s.db, this.s.tenantId, rejectionDigest(storedText(item.text))) !== null)
133
+ return 'rejected';
134
+ return null;
135
+ }
136
+ liveRow(row) {
137
+ return { id: row.id, ...splitSource(row.source, this.w.prefix), tagged: row.tags.includes(this.tag), created: row.created };
138
+ }
139
+ /** Read only when a present key has no live row, which after the first import is rare. */
140
+ dormantRows(liveKeys, refusals) {
141
+ const needed = this.w.container.items.some((item) => !refusals.has(item.key) && !liveKeys.has(item.key));
142
+ if (!needed)
143
+ return [];
144
+ return dormantSnapshotsBySourcePrefix(this.s.db, this.s.tenantId, this.w.prefix)
145
+ .filter((snap) => !snap.entry.superseded_by)
146
+ .map((snap) => ({ id: snap.entry.id, ...splitSource(snap.entry.source, this.w.prefix), dormantAt: snap.dormantAt }));
147
+ }
148
+ retag(row) {
149
+ const tagged = { ...row, tags: [...row.tags, this.tag] };
150
+ setEntryTagsInTx(this.s.db, tagged);
151
+ this.tally.retagged++;
152
+ this.mirror.push(tagged);
153
+ }
154
+ /** api.supersede's steps on this transaction; false when another writer superseded the row first. */
155
+ supersede(old, newId) {
156
+ const result = this.s.db.prepare(`UPDATE memories SET superseded_by = ? WHERE id = ? AND tenant_id = ? AND superseded_by IS NULL`)
157
+ .run(newId, old.id, old.tenantId);
158
+ if (Number(result.changes ?? 0) === 0)
159
+ return false;
160
+ if (old.dag_parent_id)
161
+ markSummaryDirtyInTx(this.s.db, old.dag_parent_id, old.tenantId, SYNC_ACTOR);
162
+ appendAuditEvent(this.s.db, { tenantId: old.tenantId, actor: SYNC_ACTOR, op: 'supersede', targetId: old.id, metadata: { newId } });
163
+ this.mirror.push({ ...old, superseded_by: newId });
164
+ return true;
165
+ }
166
+ write(planned) {
167
+ const entry = this.newRow(this.item(planned.key));
168
+ if (!this.gated(entry))
169
+ return;
170
+ this.tally[planned.supersedes.length > 0 ? 'replaced' : 'imported']++;
171
+ for (const id of planned.supersedes)
172
+ this.supersede(this.row(id), entry.id);
173
+ }
174
+ newRow(item) {
175
+ const base = createMemory(storedText(item.text), {
176
+ layer: Layer.Episodic,
177
+ tags: [this.tag],
178
+ source: itemSource(this.w.prefix, item.key, item.text),
179
+ confidence: 'observed',
180
+ kind: 'distilled',
181
+ tenantId: this.s.tenantId,
182
+ baseHalfLifeDays: this.s.baseHalfLifeDays,
183
+ });
184
+ // The note's own time when it is earlier than now; the loader reads any form but toISOString as drift.
185
+ const created = Number.isFinite(item.updatedAt)
186
+ ? new Date(Math.min(item.updatedAt, Date.parse(base.created))).toISOString()
187
+ : base.created;
188
+ const origin = this.s.originProject === undefined ? {} : { origin_project: this.s.originProject };
189
+ return stampOriginProject(this.s.hippoRoot, { ...base, created, valid_from: created, ...origin });
190
+ }
191
+ gated(entry) {
192
+ const result = gatedWrite(this.s.db, this.s.hippoRoot, entry, { actor: SYNC_ACTOR, worthCheck: false });
193
+ if (result === 'written') {
194
+ this.mirror.push(entry);
195
+ return true;
196
+ }
197
+ this.tally[result === 'skipped:rejected' ? 'rejected' : 'secret']++;
198
+ return false;
199
+ }
200
+ /** A deleted note came back unchanged: its old row returns with its id and history, under the sync's own audit op. */
201
+ restore(key, dormantId) {
202
+ const snap = readDormantSnapshot(this.s.db, this.s.tenantId, dormantId);
203
+ const taken = this.s.db.prepare(`SELECT 1 FROM memories WHERE id = ?`).get(dormantId) !== undefined;
204
+ if (snap === null || taken) {
205
+ this.write({ key, hash: '', supersedes: [] });
206
+ return;
207
+ }
208
+ const now = new Date();
209
+ const tags = Array.isArray(snap.entry.tags) ? snap.entry.tags.filter((t) => t !== this.tag) : [];
210
+ const revived = {
211
+ ...createMemory('dormant snapshot defaults', { baseHalfLifeDays: this.s.baseHalfLifeDays }),
212
+ ...snap.entry,
213
+ tags: [...tags, this.tag],
214
+ last_retrieved: now.toISOString(),
215
+ };
216
+ if (!this.gated(stampOriginProject(this.s.hippoRoot, { ...revived, strength: calculateStrength(revived, now) })))
217
+ return;
218
+ deleteDormantRow(this.s.db, this.s.tenantId, dormantId);
219
+ appendAuditEvent(this.s.db, {
220
+ tenantId: this.s.tenantId,
221
+ actor: SYNC_ACTOR,
222
+ op: 'agent_memory_restore',
223
+ targetId: dormantId,
224
+ metadata: { reason: snap.reason, dormantAt: snap.dormantAt },
225
+ });
226
+ this.tally.restored++;
227
+ }
228
+ setAside(row) {
229
+ const why = this.items.has(splitSource(row.source, this.w.prefix).key) ? 'note-changed' : 'note-gone';
230
+ const result = setAsideRow(this.s.db, this.tag, row, why);
231
+ if (result.kind === 'untagged') {
232
+ this.tally.untagged++;
233
+ this.mirror.push(result.entry);
234
+ }
235
+ else {
236
+ this.tally.setAside++;
237
+ this.purge.push(result.id);
238
+ }
239
+ }
240
+ row(id) {
241
+ const row = this.rows.get(id);
242
+ if (row === undefined)
243
+ throw new Error(`agent memory sync: planned row ${id} was not in the lookup`);
244
+ return row;
245
+ }
246
+ item(key) {
247
+ const item = this.items.get(key);
248
+ if (item === undefined)
249
+ throw new Error(`agent memory sync: planned key ${key} was not read`);
250
+ return item;
251
+ }
252
+ }
253
+ //# sourceMappingURL=apply.js.map
@@ -0,0 +1,11 @@
1
+ import type { Adapter, AdapterContext, Listing } from './types.js';
2
+ /** Claude Code's auto memory folder names for a project: its checkout, which subfolders share, or the folder itself outside a repository. */
3
+ export declare function claudeMemoryFolderNames(projectRoot: string, platform: NodeJS.Platform): Set<string>;
4
+ /** Claude Code's rule: a linked worktree shares its main checkout's folder, or the git folder's when that sits outside a checkout (a bare repository, or --separate-git-dir); any other checkout, a submodule included, keeps its own. */
5
+ export declare function claudeCheckoutRoot(top: string, gitDir: string, common: string): string;
6
+ /** Claude Code's folder name for a path: non-alphanumerics made '-', and a name over 200 characters cut to 200 plus a base-36 hash of the whole path. */
7
+ export declare function claudeFolderName(root: string): string;
8
+ export declare const claudeCodeAdapter: Adapter;
9
+ /** Post-compact's read: the session's own notes folder and nothing else, so no git call runs inside the hook's time limit. */
10
+ export declare function claudeTranscriptListing(ctx: AdapterContext, transcriptPath: string): Listing;
11
+ //# sourceMappingURL=claude-code.d.ts.map
@@ -0,0 +1,113 @@
1
+ // Claude Code's auto memory: frontmatter `.md` notes in a per-project folder, plus the `autoMemoryDirectory` user folder.
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { realpathOrResolve } from '../project-identity.js';
5
+ import { isStringValue } from '../capture.js';
6
+ import { isJsonObject } from '../hooks.js';
7
+ import { expandHome, frontmatterField, itemTime, readTextFile, splitFrontmatter } from './files.js';
8
+ import { markdownNotes, readFolderStore, uniqueFolders } from './folder-store.js';
9
+ import { gitLayout } from './git.js';
10
+ // Keeps a pinned name from carrying a separator or `..` out of the projects folder.
11
+ const PROJECT_DIR_NAME = /^[A-Za-z0-9_-]{1,64}$/;
12
+ /** Claude Code's auto memory folder names for a project: its checkout, which subfolders share, or the folder itself outside a repository. */
13
+ export function claudeMemoryFolderNames(projectRoot, platform) {
14
+ const roots = [projectRoot, realpathOrResolve(projectRoot)];
15
+ const layout = gitLayout(projectRoot);
16
+ if (layout)
17
+ roots.push(claudeCheckoutRoot(layout.top, layout.gitDir, layout.common));
18
+ return new Set(roots.map((root) => (platform === 'win32' ? claudeFolderName(root).toLowerCase() : claudeFolderName(root))));
19
+ }
20
+ /** Claude Code's rule: a linked worktree shares its main checkout's folder, or the git folder's when that sits outside a checkout (a bare repository, or --separate-git-dir); any other checkout, a submodule included, keeps its own. */
21
+ export function claudeCheckoutRoot(top, gitDir, common) {
22
+ if (gitDir === common)
23
+ return top;
24
+ if (path.basename(common) === '.git')
25
+ return path.dirname(common);
26
+ return fs.existsSync(path.join(common, '.git')) ? top : common;
27
+ }
28
+ /** Claude Code's folder name for a path: non-alphanumerics made '-', and a name over 200 characters cut to 200 plus a base-36 hash of the whole path. */
29
+ export function claudeFolderName(root) {
30
+ const full = path.resolve(root);
31
+ const name = full.replace(/[^a-zA-Z0-9]/g, '-');
32
+ if (name.length <= 200)
33
+ return name;
34
+ let hash = 0;
35
+ for (let i = 0; i < full.length; i++)
36
+ hash = ((hash << 5) - hash + full.charCodeAt(i)) | 0;
37
+ return `${name.slice(0, 200)}-${Math.abs(hash).toString(36)}`;
38
+ }
39
+ export const claudeCodeAdapter = {
40
+ tool: 'claude-code',
41
+ list(ctx, scope) {
42
+ const config = ctx.env.CLAUDE_CONFIG_DIR || path.join(ctx.home, '.claude');
43
+ const warnings = [];
44
+ const folders = scope === 'project' ? projectFolders(ctx, config) : userFolders(ctx, config, warnings);
45
+ return { tool: 'claude-code', home: config, containers: readFolders(folders, scope, ctx.platform), warnings };
46
+ },
47
+ };
48
+ /** Post-compact's read: the session's own notes folder and nothing else, so no git call runs inside the hook's time limit. */
49
+ export function claudeTranscriptListing(ctx, transcriptPath) {
50
+ const config = ctx.env.CLAUDE_CONFIG_DIR || path.join(ctx.home, '.claude');
51
+ const folder = path.join(path.dirname(transcriptPath), 'memory');
52
+ return { tool: 'claude-code', home: config, containers: readFolders([folder], 'project', ctx.platform), warnings: [] };
53
+ }
54
+ function projectFolders(ctx, config) {
55
+ const projects = path.join(config, 'projects');
56
+ const names = ctx.projectRoot === undefined ? [] : [...claudeMemoryFolderNames(ctx.projectRoot, ctx.platform)];
57
+ const pinned = ctx.env.CLAUDE_CODE_PROJECT_DIR_NAME;
58
+ // Claude reads the pinned name only alongside a pinned config folder.
59
+ if (ctx.env.CLAUDE_CONFIG_DIR && pinned !== undefined && PROJECT_DIR_NAME.test(pinned))
60
+ names.push(pinned);
61
+ const folders = names.map((name) => path.join(projects, name, 'memory'));
62
+ if (ctx.transcriptPath)
63
+ folders.push(path.join(path.dirname(ctx.transcriptPath), 'memory'));
64
+ return folders;
65
+ }
66
+ function userFolders(ctx, config, warnings) {
67
+ const dir = autoMemoryDirectory(path.join(config, 'settings.json'), ctx.home, warnings);
68
+ return dir === null ? [] : [dir];
69
+ }
70
+ // User settings only: a cloned repository's own settings must not point the import at another project's notes.
71
+ function autoMemoryDirectory(settings, home, warnings) {
72
+ if (!fs.existsSync(settings))
73
+ return null;
74
+ const file = readTextFile(settings);
75
+ if (!file.ok) {
76
+ warnings.push(file.reason);
77
+ return null;
78
+ }
79
+ let json;
80
+ try {
81
+ // SAFETY: JSON.parse yields JSON; the object and string checks below decide what is used.
82
+ json = JSON.parse(file.text);
83
+ }
84
+ catch (err) {
85
+ warnings.push(`${settings}: ${err instanceof Error ? err.message : String(err)}`);
86
+ return null;
87
+ }
88
+ const value = isJsonObject(json) ? json.autoMemoryDirectory : undefined;
89
+ if (!isStringValue(value) || value === '')
90
+ return null;
91
+ const dir = expandHome(value, home);
92
+ if (path.isAbsolute(dir))
93
+ return dir;
94
+ warnings.push(`${settings}: autoMemoryDirectory "${value}" is not absolute or under ~/, so it is ignored`);
95
+ return null;
96
+ }
97
+ // Claude counts a note only when it carries frontmatter; `modified` there beats the file time.
98
+ const NOTES = {
99
+ recursive: false,
100
+ include: markdownNotes,
101
+ item(text, mtimeMs) {
102
+ const { yaml, body } = splitFrontmatter(text);
103
+ if (yaml === null)
104
+ return null;
105
+ return { text: body.trim(), updatedAt: itemTime(frontmatterField(yaml, 'modified'), mtimeMs) };
106
+ },
107
+ };
108
+ function readFolders(folders, scope, platform) {
109
+ return uniqueFolders(folders, platform)
110
+ .map((dir) => readFolderStore(dir, scope, NOTES))
111
+ .filter((c) => c !== null);
112
+ }
113
+ //# sourceMappingURL=claude-code.js.map
@@ -0,0 +1,3 @@
1
+ import type { Adapter } from './types.js';
2
+ export declare const codexAdapter: Adapter;
3
+ //# sourceMappingURL=codex.d.ts.map
@@ -0,0 +1,47 @@
1
+ // Codex CLI's memory_summary.md, the one memory file Codex puts in its prompt, read as a single text-keyed file.
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { codexHomeDir } from '../hooks.js';
5
+ import { readTextFile } from './files.js';
6
+ import { textItemKeys } from './keys.js';
7
+ import { splitMarkdownItems } from './markdown.js';
8
+ // "What's in Memory" only indexes MEMORY.md, so it is left out.
9
+ const KEPT_HEADINGS = new Set(['user profile', 'user preferences', 'general tips']);
10
+ const PROFILE_HEADING = /^##[ \t]+User Profile[ \t]*$/im;
11
+ export const codexAdapter = {
12
+ tool: 'codex',
13
+ list(ctx, scope) {
14
+ const home = codexHomeDir(ctx.home, ctx.env);
15
+ const containers = [];
16
+ if (scope === 'user') {
17
+ const container = readSummary(path.join(home, 'memories', 'memory_summary.md'));
18
+ if (container !== null)
19
+ containers.push(container);
20
+ }
21
+ return { tool: 'codex', home, containers, warnings: [] };
22
+ },
23
+ };
24
+ function readSummary(file) {
25
+ if (!fs.existsSync(file))
26
+ return null;
27
+ const read = readTextFile(file);
28
+ if (!read.ok)
29
+ return unreadable(file, read.reason);
30
+ if (!isCodexSummary(read.text)) {
31
+ return unreadable(file, `${file}: not a Codex memory summary (needs "v1" first and a "## User Profile" heading)`);
32
+ }
33
+ const kept = splitMarkdownItems(read.text)
34
+ .filter((item) => KEPT_HEADINGS.has(item.heading.trim().toLowerCase()))
35
+ .map((item) => ({ headingSlug: item.headingSlug, text: `${item.heading}: ${item.text}` }));
36
+ const keys = textItemKeys(kept);
37
+ const items = kept.map(({ text }, i) => ({ key: keys[i], text, updatedAt: read.mtimeMs }));
38
+ return { scope: 'user', path: file, readable: true, items, skipped: [], warnings: [], textKeyed: true };
39
+ }
40
+ function isCodexSummary(text) {
41
+ const first = text.split(/\r?\n/).find((line) => line.trim() !== '');
42
+ return first?.trim() === 'v1' && PROFILE_HEADING.test(text);
43
+ }
44
+ function unreadable(file, warning) {
45
+ return { scope: 'user', path: file, readable: false, items: [], skipped: [], warnings: [warning], textKeyed: true };
46
+ }
47
+ //# sourceMappingURL=codex.js.map