Local code graph and solution memory for coding agents via CLI and MCP.
Code graph + memory · surgical reads · 100% local · MCP + CLI
Kernel written in Rust
Usage details: main agent vs subagent · MCP configs for each IDE · 中文说明
When an AI agent needs to understand code — or reuse what it already learned about this project — it discovers structure the slow way: grep, glob, and Read, one file at a time. Next session it does the same work again.
cam hands the agent two things it can query in one shell call:
Surgical reads, not a file-by-file search. Memories stay on disk, 100% local.
One binary, two doors: the main agent uses MCP (
cam mcp); subagents usually have no MCP, so they run the same CLI. Configs: docs/mcp.md. Usage: docs/agents.md.
Need Rust first? Install it from rustup.rs, then:
cargo install --git https://github.com/HanochZhu/codeagent_memory --locked
cam --help
Install cam from https://github.com/HanochZhu/codeagent_memory: require a working Rust toolchain (rustup), run `cargo install --git https://github.com/HanochZhu/codeagent_memory --locked`, ensure ~/.cargo/bin (or %USERPROFILE%\.cargo\bin on Windows) is on PATH, then verify with `cam --help`. In this repo run `cam index`. For the main agent, register MCP stdio: command `cam`, args `["mcp"]` (Cursor: .cursor/mcp.json; Claude Code: .mcp.json). Subagents call `cam --json` in the shell. Do not add extra docs.
| Environment | How to use it |
|---|---|
| Cursor | Paste into Agent / Chat; it runs the install in the terminal |
| VS Code + Copilot / Continue / Cline | Same: paste into Chat / Agent |
| Claude Code / Windsurf / Codex | Paste in the conversation and let it run the shell |
| JetBrains (IntelliJ / RustRover / GoLand) | AI Assistant, or run the same cargo install in the built-in terminal |
| Local terminal | Skip the AI; run the command above |
The installer puts cam in ~/.cargo/bin (Windows: %USERPROFILE%\.cargo\bin) but doesn't change your current shell — open a new terminal if cam is not found.
git clone https://github.com/HanochZhu/codeagent_memory.git
cd codeagent_memory
cargo install --path . --locked
No Rust toolchain required. Pull the published image and run the stdio server with the current directory mounted at /work:
docker pull ghcr.io/hanochzhu/codeagent-memory:0.1.3
docker run -i --rm -v "$PWD:/work" -w /work ghcr.io/hanochzhu/codeagent-memory:0.1.3
Or build it from this repository: docker build -t cam https://github.com/HanochZhu/codeagent_memory.git
cd your-project
cam index
cam index creates .cam/ if needed and parses the tree with tree-sitter into .cam/cam.db. Other commands do the same: they auto-init on first use. Leave cam watch running if you want the graph to follow later edits.
cam ls src/
cam read src/main.rs/main
cam ref main --dir in
cam recall "how does hybrid recall fuse BM25 and vectors"
cam add --summary "..." < notes.md
Main agent (the chat session): wire cam mcp once, then call cam_* tools. Subagent (Task / explore / delegated CLI): run the same commands in the shell. Output is compact by default; pass --json when you need structured results.
{ "mcpServers": { "cam": { "command": "cam", "args": ["mcp"] } } }
Paste that into Cursor .cursor/mcp.json, Claude Code .mcp.json, or the host's MCP settings. Per-tool files: docs/mcp.md. Who should call MCP vs CLI: docs/agents.md.
The first recall / add downloads potion-multilingual-128M. If that default model is unavailable, cam falls back to a hash embedder. Set another model2vec model with cam config set embedding_model <repo-or-path> (see Configuration).
cam has two retrieval planes. Each is scored against the closest open system on the same corpus. Do not fold them into one number.
| Plane | Closest analogue | Shared bench |
|---|---|---|
| Memory | agentmemory hybrid search + tokenized grep | coding-agent-life-v1 |
| Long-term memory | BM25 / dense memory retrievers | LongMemEval-S |
| Code graph | codegraph | LongMemCode clap |
| Workflow retrieval | lexical / BM25 file retrieval | Agent Retrieval Bench V2 edit2ripple |
| Token cost | full context dump | DeepSeek multi-turn |
Mem0, Zep, and Letta are general chat memories. They are not on these two corpora, so they are not in the score tables.
| cam | agentmemory | codegraph | |
|---|---|---|---|
| What it stores | code graph + memory tree | session / chat memories | code graph |
| Query | recall / ls / read / ref | smart-search / remember | explore / callers / callees |
| Interface | MCP + CLI | MCP + REST + hooks | MCP + CLI |
| Fusion | vector + BM25 + RRF | BM25 + embed + rerank | FTS5 + graph walk |
| Local / cost | 100% local, retrieval $0 | local server | local |
15 sessions, 15 queries, k=5. Same formula as agentmemory score.ts. P@5 ceiling is 0.240.
| system | Hit rate | R@5 | P@5 | source |
|---|---|---|---|---|
| grep (tokenized substring) | 15 / 15 | 0.967 | 0.227 | this tree, 2026-09-23 |
| cam min-max sum (legacy) | 15 / 15 | 0.933 | 0.213 | historical result, hash embed |
| cam RRF | 15 / 15 | 1.000 | 0.240 | this tree, hash embed |
| agentmemory hybrid | 15 / 15 | 1.000 | 0.240 | published v0.9.26 |
RRF matches the published hybrid ceiling and beats grep on temporal (q-015). Min-max sum still drops the second gold on temporal and multi-session-causal (q-011).
The corpus above is flat: one memory per session, no --supersedes. --lineage re-ingests every conversation turn as a revision that supersedes the previous one and adds two metrics. newest R@k asks whether the top-k carried the newest node of each gold chain; stale latest counts returned hits that claimed latest while a newer revision existed.
| k | R@k | P@k | newest R@k | stale latest |
|---|---|---|---|---|
| 5 | 1.000 | 0.240 | 1.000 | 0 |
| 10 | 1.000 | 0.120 | 1.000 | 0 |
Hash embedder, 2026-09-29. Separating structural hierarchy from revision replacement and collapsing older revisions before top-k raised newest R@5 from 0.567 to 1.000 and newest R@10 from 0.800 to 1.000, while stale latest stayed at zero.
Cleaned LongMemEval-S, 100 non-abstention questions sampled with seed 42. Each raw dated session is stored as one memory; retrieval uses hash embeddings + RRF at k=10, without an LLM reader.
| R@1 | R@5 | R@10 | Hit@1 | Hit@5 | Hit@10 | MRR |
|---|---|---|---|---|---|---|
| 0.260 | 0.512 | 0.652 | 0.420 | 0.670 | 0.810 | 0.527 |
MRR = Mean Reciprocal Rank. Under the 8-worker run, retrieval P50 / P95 was 1.28 / 2.18 s and mean per-question ingestion was 69.6 s. This is a retrieval-only result: it measures whether gold sessions are returned, not final answer quality. The run also exposed repeated tokenizer initialization during cam add as the dominant ingestion cost.
edit2ripple: 58 anchored-change queries over 44 frozen repository snapshots, no LLM. The cam adapter ranks files connected to symbols in the anchored file. All 58 questions are scored; 55 anchor files use supported languages and three Java anchors remain misses.
| system | Recall@5 | Recall@10 | Recall@20 | MRR | BCY@8k |
|---|---|---|---|---|---|
| cam graph | 0.292 | 0.320 | 0.364 | 0.291 | 0.292 |
| lexical | 0.412 | 0.536 | 0.588 | 0.243 | 0.447 |
| BM25 | 0.207 | 0.287 | 0.499 | 0.154 | 0.243 |
BCY = Budgeted Context Yield, using the benchmark's canonical 8K-token file-packing protocol. cam graph has the best first-hit rank, while lexical retrieval has much better broad gold coverage; this points to rank fusion rather than graph-only retrieval. cam query P95 was 11.6 ms; indexing the 44 snapshots took 327.6 s with 8 workers.
clap v4.6.1, 536 scenarios, no LLM. cam one-hop vs codegraph on the same SCIP gold.
| slice | n | cam | codegraph |
|---|---|---|---|
| supported (one-hop) | 478 | 0.769 | 0.769 |
| lookup / file_symbols | 293 / 55 | 0.808 / 0.915 | 0.808 / 0.915 |
| callers / callees | 67 / 18 | 0.397 / 0.811 | 0.379 / 0.811 |
| implementors | 40 | 0.119 | 0.094 |
| raw / weighted | 536 | 0.709 / 0.704 | — / 0.702 |
P95 ≈ 6.5 ms, $/1k = 0.
Same conversation twice: dump every session / every src/*.rs file, or inject cam recall (plus read / ref on code) for the current turn only. Hash embedder, 2026-09-15.
| track | n | full acc. | cam acc. | full tokens | cam tokens | saving |
|---|---|---|---|---|---|---|
| memory (coding-agent-life) | 15 | 1.00 | 1.00 | 23673 | 13255 | 44% |
code (this repo after cam index) | 6 | 1.00 | 0.50 | 169838 | 6432 | 96% |
Mean prompt tokens / turn: memory 1521 → 838; code 28250 → 1004. Code misses were retrieval gaps (callers of the fusion helper, INITIAL_STABILITY_DAYS, the cam add update rule), not the model ignoring snippets.
Corpus locations for every script below live in one file, eval/datasets.toml. coding-agent-life-v1 is not redistributed here and defaults to an agentmemory checkout next to this repository; point the entry elsewhere, or override a single run with CAM_EVAL_CODING_LIFE.
python3 eval/coding_life/run.py --adapter grep
python3 eval/coding_life/run.py --hash-embed # RRF
python3 eval/longmemeval/run.py --sample 100 --seed 42 --workers 8
git clone --depth 1 https://github.com/eyuansu62/agent-retrieval-bench \
eval/agent_retrieval/vendor/agent-retrieval-bench
python3 -m pip install -e eval/agent_retrieval/vendor/agent-retrieval-bench
arb download-benchmark --version v2_edit2ripple \
--local-dir eval/agent_retrieval/data --force
python3 eval/agent_retrieval/run.py --workers 8
python3 eval/llm_multiturn/run.py --track both # needs DEEPSEEK_API_KEY
python3 eval/longmemcode/run.py --corpus clap
python3 eval/charts/generate.py
| Code graph in SQLite | tree-sitter parse into .cam/cam.db — list, read, and hop callers/callees without opening whole files |
| Live sync | cam watch incrementally updates the graph when source files change |
| Virtual paths | src/main.rs is a file; src/main.rs/main is a symbol in that file |
| Hybrid recall | Vector + BM25 fused with RRF (k=60) |
| Revision expansion | Recall pulls in the newest replacement of whatever matched and hides older revisions by default; --no-expand disables expansion and --include-superseded keeps history |
| Ebbinghaus retention | R = exp(-t / S) moves the recall score by at most 10%; stale and forgotten entries are flagged, never deleted |
| Memory tree | --parent creates branching structural hierarchy; --supersedes creates a linear replacement revision |
| MCP + CLI | Main agent: cam_* tools. Subagent: cam --json …. Same binary |
| 100% local | No API keys. SQLite + an optional on-disk embedding model under ~/.cam/models/ |
| 9 languages | Rust, Python, TypeScript, JavaScript, Go, Java, C, C++, C# |
ls / read / ref walk a virtual filesystem. read returns an outline or a symbol slice; --full is the whole file.recall requires an English query, embeds it, runs English-normalized BM25, drops entries far below each path's best, and fuses the two ranked lists with RRF (k=60). The result is scaled by 0.9 + 0.1 × R. The newest replacement of each top match is pulled in and older revisions are folded before top-k.add stores an English summary + body. --parent adds hierarchy; --supersedes replaces an older revision while preserving it for history.Design notes (Chinese): DESIGN.md.
recall first. If nothing hits, walk the code graph. After you learn something worth keeping, add.
Main agent (MCP): cam_recall → cam_find / cam_ls / cam_read / cam_ref → cam_add. Do not shell out unless MCP is down. Full text: docs/agents.md.
Subagent (CLI):
cam --json index
cam --json sync
cam --json watch
cam --json ls src/
cam --json read src/memory/recall.rs/fuse_rrf
cam --json ref fuse_rrf --dir in
cam --json recall "how to fuse BM25 and vector recall"
cam --json add --summary "..." --body "..."
cam --json mem tree
cam --json mem show <id>
cam --json status
cam mcp
Global flags: --project <dir>, --json (compact JSON), --pretty (pretty JSON, implies --json). Without --project, resolution is CAM_PROJECT, then walk up for .cam / .git. With --json, failures print {"error":{"code":…,"message":…}} to stdout and exit non-zero. Start the MCP server with cam mcp (optional --project).
cam index # Parse the project with tree-sitter into SQLite
cam sync # Incrementally update the graph for changed files
cam watch [--debounce-ms N] # Watch source files; --json streams one report per sync
cam ls [virt_path] # List directories / files / symbols
cam read <virt_path> [--full] # File outline or symbol body
cam ref <symbol> --dir in|out [--file SUBSTR] [--kind KIND] [--scope DIR] # One-hop callers (in) or callees (out); --callers / --callees aliases
cam find <query> [--kind KIND] [--file SUBSTR] [--scope DIR] [--limit N] # Symbols whose name contains <query>
cam recall "<query>" [--limit N] [--no-expand] [--include-superseded] [--snippets] # Hybrid recall: vector + BM25 fused with RRF
cam add --summary "..." [--parent ID] [--supersedes ID] [--body TEXT | --file PATH] # Store a memory (body: --body, --file, or stdin)
cam mem tree # Print the memory tree
cam mem show <id> # Show one memory
cam status # Resolved project, database counts, and config
cam config get | config set stale_days N | config set embedding_model <model> # Read or update ~/.cam/config.toml
cam mcp # MCP stdio server for the main agent
| Command | What it does |
|---|---|
cam index | Parse with tree-sitter into SQLite (.cam/cam.db); creates .cam/ if needed |
cam sync | Incrementally update the graph from content hashes |
cam watch | Debounced file watcher that runs sync on source changes |
cam ls [path] | List directories / files / symbols |
cam read <path> | File outline or symbol body; --full for the whole file |
cam ref <symbol> --dir in|out | One-hop callers / callees. A name shared by several definitions returns status: ambiguous with ranked candidates; re-run with a candidate id, or narrow with --file / --kind / --scope |
cam find <query> | Case-insensitive substring search over symbol names (not body text). Exact names first, then prefixes; each hit has a virtual path and id for read / ref |
cam recall "<one English sentence>" | Vector + BM25 fused with RRF (k=60); --no-expand skips revision expansion; --include-superseded keeps history; --snippets clips long bodies below the top hit (body_truncated) |
cam add --summary "..." [--parent ID] [--supersedes ID] | Store an English memory; parent is structural and supersedes replaces an older revision |
cam mem tree / cam mem show <id> | Browse the memory tree |
cam status | Resolved project root, db path, node/edge/memory counts, config |
cam config get / cam config set stale_days N / cam config set embedding_model <model> | Read or update ~/.cam/config.toml |
cam mcp | Stdio MCP server (main agent). See docs/mcp.md |
Virtual paths: src/main.rs is a file; src/main.rs/main is a symbol in that file. Node ids (src/main.rs:function:main:12) from an ambiguous answer are accepted wherever a symbol is.
Multi-repo folders. .gitignore and .camignore (same syntax) are honored at any depth, even when the root itself is not a git repo. Submodules and linked worktrees (directories whose .git is a file) are skipped; nested standalone clones are indexed, and cam ls lists them with kind project so you can pass that path as --scope.
stale_days in ~/.cam/config.toml (default 30) are marked staleR = exp(-t / S): the sort score is relevance × (0.9 + 0.1 × R); R < 0.3 is marked needs_updateneeds_update = false) refreshes C0 and multiplies S by 1.7. Expanded revisions and filtered older revisions are not refreshedsupersedes chain is latest; older revisions are hidden by default and available with --include-superseded--parent for branching hierarchy and --supersedes for a complete replacement revision~/.cam/config.toml:
stale_days = 30
# embedding_model = "minishlab/potion-base-8M" # Hugging Face model2vec repo
# embedding_model = "/path/to/model2vec" # local tokenizer.json + config.json + model.safetensors
# embedding_model = "hash" # no download; lexical hash only
embedding_model defaults to minishlab/potion-multilingual-128M. default clears a custom value. CAM_EMBED_MODEL overrides the file for one process. The first recall after a model change re-embeds stored memories with the new model before scoring, so vector recall keeps working. Downloaded models are cached under ~/.cam/models/.
cam is a Rust binary. cargo install builds it for the machine you are on:
| Platform | Install |
|---|---|
| Windows | cargo install --git … --locked |
| macOS | cargo install --git … --locked |
| Linux | cargo install --git … --locked |
See Get Started for the full command.
Main agent: register cam mcp (stdio). Subagent: same CLI, no extra wiring.
| Host | MCP config | Notes |
|---|---|---|
| Cursor | .cursor/mcp.json or ~/.cursor/mcp.json, or the codeagent-memory plugin | Task / explore subagents use CLI |
| Claude Code | .mcp.json / ~/.claude.json / claude mcp add | Limited-tool subagents use CLI |
| Codex | ~/.codex/config.toml → [mcp_servers.cam] | codex exec uses CLI |
| Windsurf | ~/.codeium/windsurf/mcp_config.json | Cascade = MCP |
| GitHub Copilot | .vscode/mcp.json / ~/.copilot/mcp-config.json | Chat = MCP; CLI scripts = CLI |
| Continue | ~/.continue/config.yaml | |
| Cline | ~/.cline/mcp.json | |
| JetBrains | AI Assistant → MCP | Use an absolute cam.exe if PATH is empty |
| Gemini CLI / Antigravity / OpenCode / Zed / Droid | see docs/mcp.md |
Paste the one-sentence install into the agent, then add the MCP block. Copy-paste files: docs/mcp.md. Agent prompts: docs/agents.md.
The Cursor plugin (.cursor-plugin/plugin.json) registers the same cam mcp server and a skill. Install the cam binary first and keep it on PATH; the plugin does not ship the binary.
| Language | Extension | What is indexed |
|---|---|---|
| Rust | .rs | functions, structs, enums, traits, calls |
| Python | .py | functions, classes, calls |
| TypeScript | .ts, .tsx | functions, methods, classes, calls |
| JavaScript | .js, .jsx | functions, methods, classes, calls |
| Go | .go | functions, methods, structs, calls |
| Java | .java | functions, methods, classes, interfaces, calls |
| C | .c | functions, structs, calls |
| C++ | .cc, .cpp, .cxx, .h, .hpp | functions, methods, classes, structs, calls |
| C# | .cs | functions, methods, classes, interfaces, calls |
MIT
Made for AI coding agents — Claude Code, Cursor, Codex, Windsurf, Copilot, and JetBrains
Source-derived launch command. Check the maintainer’s required arguments and credentials before running:
docker run -i --rm ghcr.io/hanochzhu/codeagent-memory:0.1.5Merge this template into ~/Library/Application Support/Claude/claude_desktop_config.json. Keep existing servers. Add any arguments, credentials, and permissions required by the maintainer; this template has not been install-tested.
{
"mcpServers": {
"io-github-hanochzhu-codeagent-memory": {
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"ghcr.io/hanochzhu/codeagent-memory:0.1.5"
]
}
}
}Restart Claude Desktop completely for changes to take effect. Confirm the server appears connected in the client’s tool list, then try a read-only example from its documentation.
Claude Desktop setup referenceghcr.io/hanochzhu/codeagent-memory:0.1.5dockercodeagent-memory works with any MCP-compatible client. Copy the config snippet from the Configuration section above and add it to the file shown for your client, then restart the application.
~/Library/Application Support/Claude/claude_desktop_config.jsonRestart Claude Desktop completely for changes to take effect.~/.cursor/mcp.jsonRestart Cursor for changes to take effect..vscode/mcp.jsonReload VS Code window for changes to take effect.~/.codeium/windsurf/mcp_config.jsonRestart Windsurf for changes to take effect..mcp.jsonSave at the project root, then start Claude Code in that project and review the MCP server approval prompt. Keep real credentials out of shared files.