foldcrumbs 0.3.0__tar.gz
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.
- foldcrumbs-0.3.0/LICENSE +26 -0
- foldcrumbs-0.3.0/PKG-INFO +211 -0
- foldcrumbs-0.3.0/README.md +190 -0
- foldcrumbs-0.3.0/foldcrumbs/__init__.py +9 -0
- foldcrumbs-0.3.0/foldcrumbs/__main__.py +6 -0
- foldcrumbs-0.3.0/foldcrumbs/audit.py +117 -0
- foldcrumbs-0.3.0/foldcrumbs/cli.py +358 -0
- foldcrumbs-0.3.0/foldcrumbs/config.py +226 -0
- foldcrumbs-0.3.0/foldcrumbs/distill.py +376 -0
- foldcrumbs-0.3.0/foldcrumbs/hooks/__init__.py +0 -0
- foldcrumbs-0.3.0/foldcrumbs/hooks/_common.py +185 -0
- foldcrumbs-0.3.0/foldcrumbs/hooks/_state.py +57 -0
- foldcrumbs-0.3.0/foldcrumbs/hooks/_worker.py +57 -0
- foldcrumbs-0.3.0/foldcrumbs/hooks/context_monitor.py +74 -0
- foldcrumbs-0.3.0/foldcrumbs/hooks/post_compact.py +62 -0
- foldcrumbs-0.3.0/foldcrumbs/hooks/session_end.py +43 -0
- foldcrumbs-0.3.0/foldcrumbs/hooks/session_start.py +82 -0
- foldcrumbs-0.3.0/foldcrumbs/install.py +361 -0
- foldcrumbs-0.3.0/foldcrumbs/llm.py +200 -0
- foldcrumbs-0.3.0/foldcrumbs/mcp_server.py +222 -0
- foldcrumbs-0.3.0/foldcrumbs/profile.py +75 -0
- foldcrumbs-0.3.0/foldcrumbs/redact.py +44 -0
- foldcrumbs-0.3.0/foldcrumbs/schema.py +254 -0
- foldcrumbs-0.3.0/foldcrumbs/store.py +265 -0
- foldcrumbs-0.3.0/foldcrumbs.egg-info/PKG-INFO +211 -0
- foldcrumbs-0.3.0/foldcrumbs.egg-info/SOURCES.txt +32 -0
- foldcrumbs-0.3.0/foldcrumbs.egg-info/dependency_links.txt +1 -0
- foldcrumbs-0.3.0/foldcrumbs.egg-info/entry_points.txt +3 -0
- foldcrumbs-0.3.0/foldcrumbs.egg-info/requires.txt +4 -0
- foldcrumbs-0.3.0/foldcrumbs.egg-info/top_level.txt +1 -0
- foldcrumbs-0.3.0/pyproject.toml +38 -0
- foldcrumbs-0.3.0/setup.cfg +4 -0
- foldcrumbs-0.3.0/tests/test_foldcrumbs.py +549 -0
- foldcrumbs-0.3.0/tests/test_mcp.py +97 -0
foldcrumbs-0.3.0/LICENSE
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vincenzo Ingrosso
|
|
4
|
+
|
|
5
|
+
Portions of this software (typed-memory schema and trust/decay logic,
|
|
6
|
+
extraction prompts and parser, lifecycle-hook plumbing, and context-block
|
|
7
|
+
rendering) are derived from MEMANTO (https://github.com/moorcheh-ai/memanto),
|
|
8
|
+
Copyright (c) Moorcheh / Edge AI Innovations, licensed under the MIT License.
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: foldcrumbs
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Persistent cross-session memory for coding agents — file-based, no vector DB, no external service.
|
|
5
|
+
Author: Vincenzo Ingrosso
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/vcnngr/foldcrumbs
|
|
8
|
+
Keywords: claude-code,codex,opencode,agent-memory,llm,hooks,mlx
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: ruff>=0.5; extra == "dev"
|
|
19
|
+
Requires-Dist: mypy>=1.10; extra == "dev"
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# foldcrumbs
|
|
23
|
+
|
|
24
|
+
[](https://github.com/vcnngr/foldcrumbs/actions/workflows/test.yml)
|
|
25
|
+
[](LICENSE)
|
|
26
|
+
|
|
27
|
+
Persistent cross-session memory for coding agents — **no Docker, no vector DB, no external service**.
|
|
28
|
+
|
|
29
|
+
`/clear` and compaction wipe Claude Code's knowledge every session. foldcrumbs keeps a small
|
|
30
|
+
folder of typed memory files so the agent reopens already knowing your decisions, conventions
|
|
31
|
+
and codebase facts. It also fights context rot: around 45% context it checkpoints memory in the
|
|
32
|
+
background and nudges you to `/compact` or `/clear` — nothing is lost.
|
|
33
|
+
|
|
34
|
+
## How it works
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
STORE markdown files + MEMORY.md index in
|
|
38
|
+
~/.claude/projects/<project>/memory/
|
|
39
|
+
RECALL Claude Code's own Grep/Read (no LLM, no vector DB)
|
|
40
|
+
+ SessionStart injects the index
|
|
41
|
+
DISTILL async, local LLM only (MLX/Ollama/OpenRouter via env)
|
|
42
|
+
at ~45% context and at session end → gated, dedup'd
|
|
43
|
+
ANTI-ROT PostToolUse monitor → checkpoint + reminder (no forced compaction)
|
|
44
|
+
PostCompact → re-inject index after compaction
|
|
45
|
+
HANDOFF each checkpoint also writes a live working-state snapshot, re-injected
|
|
46
|
+
at SessionStart → resume the exact task after a /clear
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The retrieval engine is the agent itself: it greps the folder when relevant. The LLM is used
|
|
50
|
+
**only** for async distillation — so recall is instant and never depends on a model being up.
|
|
51
|
+
|
|
52
|
+
Pure Python stdlib: hook scripts never fail on a missing import.
|
|
53
|
+
|
|
54
|
+
The `MEMORY.md` index is written in a **deterministic order** (by immutable
|
|
55
|
+
creation time, newest first within each type), so a trust bump, re-touch or
|
|
56
|
+
re-distillation never reshuffles existing entries. Only adding or removing a
|
|
57
|
+
memory changes the file. This keeps the SessionStart-injected prefix identical
|
|
58
|
+
across sessions — so it rides the agent's own prompt cache instead of busting it
|
|
59
|
+
— and keeps the file diff-clean for sync tools like Syncthing.
|
|
60
|
+
|
|
61
|
+
## What's different from memanto
|
|
62
|
+
|
|
63
|
+
foldcrumbs started from ideas in [memanto](https://github.com/moorcheh-ai/memanto), but takes a
|
|
64
|
+
deliberately different shape:
|
|
65
|
+
|
|
66
|
+
| | memanto | foldcrumbs |
|
|
67
|
+
|--|--|--|
|
|
68
|
+
| Retrieval | Moorcheh engine (closed) | the agent's own grep — no engine |
|
|
69
|
+
| Footprint | Docker + engine + LLM + REST API | a folder + hooks |
|
|
70
|
+
| LLM | required for retrieval & answers | async distillation only; recall never needs it |
|
|
71
|
+
| Anti-rot | — | context monitor + checkpoint near 45% |
|
|
72
|
+
| Deps | service stack | zero runtime deps (stdlib) |
|
|
73
|
+
| Scope | tool-agnostic service | per-project memory, agent-side |
|
|
74
|
+
|
|
75
|
+
The original work here is the architecture: grep-based recall, the file store + index, the
|
|
76
|
+
anti-rot monitor, the merge-safe installer, the hooks and CLI. See **Credits** for the parts
|
|
77
|
+
adapted from memanto.
|
|
78
|
+
|
|
79
|
+
## Install
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
foldcrumbs install # Claude Code, global (~/.claude/settings.json)
|
|
83
|
+
foldcrumbs install --local # Claude Code, project (.claude/settings.json)
|
|
84
|
+
foldcrumbs install --agent codex # Codex: hooks.json + prints the config.toml MCP snippet
|
|
85
|
+
foldcrumbs install --agent opencode # OpenCode: opencode.json MCP + plugin + AGENTS.md block
|
|
86
|
+
```
|
|
87
|
+
The installer is merge-safe and idempotent: it appends its own hook groups and leaves existing
|
|
88
|
+
hooks (GSD, graphify, …) untouched. A `.foldcrumbs-bak` backup is written first.
|
|
89
|
+
|
|
90
|
+
On a TTY, install asks **how to distill** (recall never uses an LLM):
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
1) claude-cli Claude subscription — `claude -p`, no API key
|
|
94
|
+
2) codex Codex subscription — `codex exec`, no API key
|
|
95
|
+
3) openai OpenAI-compatible HTTP endpoint (local server or remote gateway)
|
|
96
|
+
4) none no LLM — keyword heuristic only (last resort)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The choice is saved per-machine in `~/.foldcrumbs` (not synced), so a shared store can have one
|
|
100
|
+
indexer with a local model and others using their own CLI subscription. Skip the prompt with
|
|
101
|
+
`foldcrumbs install --backend codex` (or `--no-backend-prompt`), and change it anytime with
|
|
102
|
+
`foldcrumbs backend <name>` (`foldcrumbs backend` alone shows the current one).
|
|
103
|
+
|
|
104
|
+
All agents share **one** memory store per project, so a decision recorded in Claude Code is
|
|
105
|
+
recalled in Codex and OpenCode.
|
|
106
|
+
|
|
107
|
+
## Configure (env)
|
|
108
|
+
|
|
109
|
+
| var | default | meaning |
|
|
110
|
+
|-----|---------|---------|
|
|
111
|
+
| `FOLDCRUMBS_LLM_ENDPOINT` | `http://localhost:8081` | OpenAI-compatible endpoint (MLX server) |
|
|
112
|
+
| `FOLDCRUMBS_LLM_MODEL` | `gemma-4-26b-a4b` | model name |
|
|
113
|
+
| `FOLDCRUMBS_LLM_API_KEY` | – | optional bearer token |
|
|
114
|
+
| `FOLDCRUMBS_CONTEXT_BUDGET` | `200000` | context window size (tokens) for the monitor |
|
|
115
|
+
| `FOLDCRUMBS_CONTEXT_PCT` | `0.45` | fraction at which to checkpoint + nudge |
|
|
116
|
+
| `FOLDCRUMBS_MIN_CONFIDENCE` | `0.7` | write gate floor |
|
|
117
|
+
| `FOLDCRUMBS_DIR` | derived from cwd | override the memory directory |
|
|
118
|
+
|
|
119
|
+
Swap the LLM for a remote gateway or OpenRouter by changing `FOLDCRUMBS_LLM_ENDPOINT` — recall is
|
|
120
|
+
unaffected.
|
|
121
|
+
|
|
122
|
+
## CLI
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
python3 -m foldcrumbs status
|
|
126
|
+
python3 -m foldcrumbs remember "Recall is grep, no vector DB" --type decision --tag arch
|
|
127
|
+
python3 -m foldcrumbs recall "vector db"
|
|
128
|
+
python3 -m foldcrumbs index
|
|
129
|
+
python3 -m foldcrumbs distill transcript.txt # distil durable memories (LLM)
|
|
130
|
+
python3 -m foldcrumbs checkpoint transcript.txt # write a resume handoff (LLM)
|
|
131
|
+
python3 -m foldcrumbs handoff # print the current handoff
|
|
132
|
+
python3 -m foldcrumbs answer "how does recall work?"
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Surviving `/clear` and `/compact`
|
|
136
|
+
|
|
137
|
+
Two layers cross the context switch:
|
|
138
|
+
|
|
139
|
+
- **Durable memories** (decisions, rules, preferences, facts) — always re-injected via
|
|
140
|
+
the `MEMORY.md` index at SessionStart / PostCompact.
|
|
141
|
+
- **Working-state handoff** — a single overwritten snapshot of the *current* task, files
|
|
142
|
+
in flight and next steps, written at each checkpoint and re-injected so you resume the
|
|
143
|
+
exact task after a hard `/clear`.
|
|
144
|
+
|
|
145
|
+
At ~45% context foldcrumbs nudges you; pick `/compact` (keep working) or `/clear` (fresh start) —
|
|
146
|
+
either way the next turn is re-primed. Force a snapshot anytime with `foldcrumbs checkpoint`.
|
|
147
|
+
|
|
148
|
+
## Local LLM
|
|
149
|
+
|
|
150
|
+
Distillation needs any OpenAI-compatible chat endpoint — point `FOLDCRUMBS_LLM_ENDPOINT`
|
|
151
|
+
at whatever you run. It's used only for async distillation, so a cold model load is
|
|
152
|
+
invisible to the editor, and **recall needs no model at all**.
|
|
153
|
+
|
|
154
|
+
Common local servers (all expose `/v1/chat/completions`):
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
# MLX — Apple Silicon only, fastest on Mac
|
|
158
|
+
mlx_lm.server --model <gemma-mlx-repo> --port 8081 # or mlx_vlm.server for VLMs
|
|
159
|
+
|
|
160
|
+
# Ollama — cross-platform (macOS / Linux / Windows)
|
|
161
|
+
ollama serve # endpoint :11434/v1
|
|
162
|
+
|
|
163
|
+
# llama.cpp / LM Studio / vLLM — also OpenAI-compatible
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Then e.g. `export FOLDCRUMBS_LLM_ENDPOINT=http://localhost:11434 FOLDCRUMBS_LLM_MODEL=qwen2.5`.
|
|
167
|
+
A remote gateway or OpenRouter works the same way — only the env var changes.
|
|
168
|
+
|
|
169
|
+
## Tests
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
python3 -m unittest discover -s tests -v
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## MCP server
|
|
176
|
+
|
|
177
|
+
foldcrumbs ships a minimal MCP server (stdio, stdlib only — no `mcp` SDK dependency) exposing
|
|
178
|
+
`remember`, `recall` and `answer` to any MCP client:
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
foldcrumbs-mcp # or: python3 -m foldcrumbs.mcp_server
|
|
182
|
+
```
|
|
183
|
+
Codex and OpenCode are wired to it by `foldcrumbs install --agent …`. Use it directly from any
|
|
184
|
+
MCP-speaking tool by registering the command above.
|
|
185
|
+
|
|
186
|
+
## How each agent is wired
|
|
187
|
+
|
|
188
|
+
| Agent | Inject at start | Capture | Notes |
|
|
189
|
+
|-------|-----------------|---------|-------|
|
|
190
|
+
| Claude Code | SessionStart hook | PostToolUse monitor + SessionEnd | full lifecycle hooks |
|
|
191
|
+
| Codex | SessionStart hook (`additionalContext`) | Stop + PostToolUse hooks | same scripts; + MCP for in-session tool calls |
|
|
192
|
+
| OpenCode | AGENTS.md → agent calls `recall` (MCP) | plugin `session.idle`/`session.compacted` | no inject-capable hook, so prompt-driven recall |
|
|
193
|
+
|
|
194
|
+
## Roadmap
|
|
195
|
+
|
|
196
|
+
- **Phase 1 ✓** — Claude Code: file store, grep recall, distillation, anti-rot.
|
|
197
|
+
- **Phase 2 ✓** — Codex + OpenCode on the same store via a stdlib MCP server + installers.
|
|
198
|
+
- **Phase 3** — embeddings + open vector DB only if scale outgrows grep; document ingest via OCR.
|
|
199
|
+
|
|
200
|
+
## Credits
|
|
201
|
+
|
|
202
|
+
foldcrumbs adapts a few utilities from [memanto](https://github.com/moorcheh-ai/memanto)
|
|
203
|
+
(MIT, © Moorcheh / Edge AI Innovations): the typed-memory categories and confidence/decay
|
|
204
|
+
model, the session-distillation approach, the transcript-reading helper, and the context-block
|
|
205
|
+
rendering idea. These are reimplemented here against a file store; the Moorcheh retrieval engine
|
|
206
|
+
is not used. Full notice in [LICENSE](LICENSE). Thanks to the memanto authors for releasing it
|
|
207
|
+
under MIT.
|
|
208
|
+
|
|
209
|
+
## License
|
|
210
|
+
|
|
211
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# foldcrumbs
|
|
2
|
+
|
|
3
|
+
[](https://github.com/vcnngr/foldcrumbs/actions/workflows/test.yml)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
|
|
6
|
+
Persistent cross-session memory for coding agents — **no Docker, no vector DB, no external service**.
|
|
7
|
+
|
|
8
|
+
`/clear` and compaction wipe Claude Code's knowledge every session. foldcrumbs keeps a small
|
|
9
|
+
folder of typed memory files so the agent reopens already knowing your decisions, conventions
|
|
10
|
+
and codebase facts. It also fights context rot: around 45% context it checkpoints memory in the
|
|
11
|
+
background and nudges you to `/compact` or `/clear` — nothing is lost.
|
|
12
|
+
|
|
13
|
+
## How it works
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
STORE markdown files + MEMORY.md index in
|
|
17
|
+
~/.claude/projects/<project>/memory/
|
|
18
|
+
RECALL Claude Code's own Grep/Read (no LLM, no vector DB)
|
|
19
|
+
+ SessionStart injects the index
|
|
20
|
+
DISTILL async, local LLM only (MLX/Ollama/OpenRouter via env)
|
|
21
|
+
at ~45% context and at session end → gated, dedup'd
|
|
22
|
+
ANTI-ROT PostToolUse monitor → checkpoint + reminder (no forced compaction)
|
|
23
|
+
PostCompact → re-inject index after compaction
|
|
24
|
+
HANDOFF each checkpoint also writes a live working-state snapshot, re-injected
|
|
25
|
+
at SessionStart → resume the exact task after a /clear
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The retrieval engine is the agent itself: it greps the folder when relevant. The LLM is used
|
|
29
|
+
**only** for async distillation — so recall is instant and never depends on a model being up.
|
|
30
|
+
|
|
31
|
+
Pure Python stdlib: hook scripts never fail on a missing import.
|
|
32
|
+
|
|
33
|
+
The `MEMORY.md` index is written in a **deterministic order** (by immutable
|
|
34
|
+
creation time, newest first within each type), so a trust bump, re-touch or
|
|
35
|
+
re-distillation never reshuffles existing entries. Only adding or removing a
|
|
36
|
+
memory changes the file. This keeps the SessionStart-injected prefix identical
|
|
37
|
+
across sessions — so it rides the agent's own prompt cache instead of busting it
|
|
38
|
+
— and keeps the file diff-clean for sync tools like Syncthing.
|
|
39
|
+
|
|
40
|
+
## What's different from memanto
|
|
41
|
+
|
|
42
|
+
foldcrumbs started from ideas in [memanto](https://github.com/moorcheh-ai/memanto), but takes a
|
|
43
|
+
deliberately different shape:
|
|
44
|
+
|
|
45
|
+
| | memanto | foldcrumbs |
|
|
46
|
+
|--|--|--|
|
|
47
|
+
| Retrieval | Moorcheh engine (closed) | the agent's own grep — no engine |
|
|
48
|
+
| Footprint | Docker + engine + LLM + REST API | a folder + hooks |
|
|
49
|
+
| LLM | required for retrieval & answers | async distillation only; recall never needs it |
|
|
50
|
+
| Anti-rot | — | context monitor + checkpoint near 45% |
|
|
51
|
+
| Deps | service stack | zero runtime deps (stdlib) |
|
|
52
|
+
| Scope | tool-agnostic service | per-project memory, agent-side |
|
|
53
|
+
|
|
54
|
+
The original work here is the architecture: grep-based recall, the file store + index, the
|
|
55
|
+
anti-rot monitor, the merge-safe installer, the hooks and CLI. See **Credits** for the parts
|
|
56
|
+
adapted from memanto.
|
|
57
|
+
|
|
58
|
+
## Install
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
foldcrumbs install # Claude Code, global (~/.claude/settings.json)
|
|
62
|
+
foldcrumbs install --local # Claude Code, project (.claude/settings.json)
|
|
63
|
+
foldcrumbs install --agent codex # Codex: hooks.json + prints the config.toml MCP snippet
|
|
64
|
+
foldcrumbs install --agent opencode # OpenCode: opencode.json MCP + plugin + AGENTS.md block
|
|
65
|
+
```
|
|
66
|
+
The installer is merge-safe and idempotent: it appends its own hook groups and leaves existing
|
|
67
|
+
hooks (GSD, graphify, …) untouched. A `.foldcrumbs-bak` backup is written first.
|
|
68
|
+
|
|
69
|
+
On a TTY, install asks **how to distill** (recall never uses an LLM):
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
1) claude-cli Claude subscription — `claude -p`, no API key
|
|
73
|
+
2) codex Codex subscription — `codex exec`, no API key
|
|
74
|
+
3) openai OpenAI-compatible HTTP endpoint (local server or remote gateway)
|
|
75
|
+
4) none no LLM — keyword heuristic only (last resort)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The choice is saved per-machine in `~/.foldcrumbs` (not synced), so a shared store can have one
|
|
79
|
+
indexer with a local model and others using their own CLI subscription. Skip the prompt with
|
|
80
|
+
`foldcrumbs install --backend codex` (or `--no-backend-prompt`), and change it anytime with
|
|
81
|
+
`foldcrumbs backend <name>` (`foldcrumbs backend` alone shows the current one).
|
|
82
|
+
|
|
83
|
+
All agents share **one** memory store per project, so a decision recorded in Claude Code is
|
|
84
|
+
recalled in Codex and OpenCode.
|
|
85
|
+
|
|
86
|
+
## Configure (env)
|
|
87
|
+
|
|
88
|
+
| var | default | meaning |
|
|
89
|
+
|-----|---------|---------|
|
|
90
|
+
| `FOLDCRUMBS_LLM_ENDPOINT` | `http://localhost:8081` | OpenAI-compatible endpoint (MLX server) |
|
|
91
|
+
| `FOLDCRUMBS_LLM_MODEL` | `gemma-4-26b-a4b` | model name |
|
|
92
|
+
| `FOLDCRUMBS_LLM_API_KEY` | – | optional bearer token |
|
|
93
|
+
| `FOLDCRUMBS_CONTEXT_BUDGET` | `200000` | context window size (tokens) for the monitor |
|
|
94
|
+
| `FOLDCRUMBS_CONTEXT_PCT` | `0.45` | fraction at which to checkpoint + nudge |
|
|
95
|
+
| `FOLDCRUMBS_MIN_CONFIDENCE` | `0.7` | write gate floor |
|
|
96
|
+
| `FOLDCRUMBS_DIR` | derived from cwd | override the memory directory |
|
|
97
|
+
|
|
98
|
+
Swap the LLM for a remote gateway or OpenRouter by changing `FOLDCRUMBS_LLM_ENDPOINT` — recall is
|
|
99
|
+
unaffected.
|
|
100
|
+
|
|
101
|
+
## CLI
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
python3 -m foldcrumbs status
|
|
105
|
+
python3 -m foldcrumbs remember "Recall is grep, no vector DB" --type decision --tag arch
|
|
106
|
+
python3 -m foldcrumbs recall "vector db"
|
|
107
|
+
python3 -m foldcrumbs index
|
|
108
|
+
python3 -m foldcrumbs distill transcript.txt # distil durable memories (LLM)
|
|
109
|
+
python3 -m foldcrumbs checkpoint transcript.txt # write a resume handoff (LLM)
|
|
110
|
+
python3 -m foldcrumbs handoff # print the current handoff
|
|
111
|
+
python3 -m foldcrumbs answer "how does recall work?"
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Surviving `/clear` and `/compact`
|
|
115
|
+
|
|
116
|
+
Two layers cross the context switch:
|
|
117
|
+
|
|
118
|
+
- **Durable memories** (decisions, rules, preferences, facts) — always re-injected via
|
|
119
|
+
the `MEMORY.md` index at SessionStart / PostCompact.
|
|
120
|
+
- **Working-state handoff** — a single overwritten snapshot of the *current* task, files
|
|
121
|
+
in flight and next steps, written at each checkpoint and re-injected so you resume the
|
|
122
|
+
exact task after a hard `/clear`.
|
|
123
|
+
|
|
124
|
+
At ~45% context foldcrumbs nudges you; pick `/compact` (keep working) or `/clear` (fresh start) —
|
|
125
|
+
either way the next turn is re-primed. Force a snapshot anytime with `foldcrumbs checkpoint`.
|
|
126
|
+
|
|
127
|
+
## Local LLM
|
|
128
|
+
|
|
129
|
+
Distillation needs any OpenAI-compatible chat endpoint — point `FOLDCRUMBS_LLM_ENDPOINT`
|
|
130
|
+
at whatever you run. It's used only for async distillation, so a cold model load is
|
|
131
|
+
invisible to the editor, and **recall needs no model at all**.
|
|
132
|
+
|
|
133
|
+
Common local servers (all expose `/v1/chat/completions`):
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
# MLX — Apple Silicon only, fastest on Mac
|
|
137
|
+
mlx_lm.server --model <gemma-mlx-repo> --port 8081 # or mlx_vlm.server for VLMs
|
|
138
|
+
|
|
139
|
+
# Ollama — cross-platform (macOS / Linux / Windows)
|
|
140
|
+
ollama serve # endpoint :11434/v1
|
|
141
|
+
|
|
142
|
+
# llama.cpp / LM Studio / vLLM — also OpenAI-compatible
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Then e.g. `export FOLDCRUMBS_LLM_ENDPOINT=http://localhost:11434 FOLDCRUMBS_LLM_MODEL=qwen2.5`.
|
|
146
|
+
A remote gateway or OpenRouter works the same way — only the env var changes.
|
|
147
|
+
|
|
148
|
+
## Tests
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
python3 -m unittest discover -s tests -v
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## MCP server
|
|
155
|
+
|
|
156
|
+
foldcrumbs ships a minimal MCP server (stdio, stdlib only — no `mcp` SDK dependency) exposing
|
|
157
|
+
`remember`, `recall` and `answer` to any MCP client:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
foldcrumbs-mcp # or: python3 -m foldcrumbs.mcp_server
|
|
161
|
+
```
|
|
162
|
+
Codex and OpenCode are wired to it by `foldcrumbs install --agent …`. Use it directly from any
|
|
163
|
+
MCP-speaking tool by registering the command above.
|
|
164
|
+
|
|
165
|
+
## How each agent is wired
|
|
166
|
+
|
|
167
|
+
| Agent | Inject at start | Capture | Notes |
|
|
168
|
+
|-------|-----------------|---------|-------|
|
|
169
|
+
| Claude Code | SessionStart hook | PostToolUse monitor + SessionEnd | full lifecycle hooks |
|
|
170
|
+
| Codex | SessionStart hook (`additionalContext`) | Stop + PostToolUse hooks | same scripts; + MCP for in-session tool calls |
|
|
171
|
+
| OpenCode | AGENTS.md → agent calls `recall` (MCP) | plugin `session.idle`/`session.compacted` | no inject-capable hook, so prompt-driven recall |
|
|
172
|
+
|
|
173
|
+
## Roadmap
|
|
174
|
+
|
|
175
|
+
- **Phase 1 ✓** — Claude Code: file store, grep recall, distillation, anti-rot.
|
|
176
|
+
- **Phase 2 ✓** — Codex + OpenCode on the same store via a stdlib MCP server + installers.
|
|
177
|
+
- **Phase 3** — embeddings + open vector DB only if scale outgrows grep; document ingest via OCR.
|
|
178
|
+
|
|
179
|
+
## Credits
|
|
180
|
+
|
|
181
|
+
foldcrumbs adapts a few utilities from [memanto](https://github.com/moorcheh-ai/memanto)
|
|
182
|
+
(MIT, © Moorcheh / Edge AI Innovations): the typed-memory categories and confidence/decay
|
|
183
|
+
model, the session-distillation approach, the transcript-reading helper, and the context-block
|
|
184
|
+
rendering idea. These are reimplemented here against a file store; the Moorcheh retrieval engine
|
|
185
|
+
is not used. Full notice in [LICENSE](LICENSE). Thanks to the memanto authors for releasing it
|
|
186
|
+
under MIT.
|
|
187
|
+
|
|
188
|
+
## License
|
|
189
|
+
|
|
190
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""foldcrumbs — persistent cross-session memory for coding agents.
|
|
2
|
+
|
|
3
|
+
File-based memory store + lifecycle hooks for Claude Code. No external
|
|
4
|
+
service, no vector DB: retrieval is done by the agent's own grep over the
|
|
5
|
+
memory folder, and a local OpenAI-compatible LLM is used only for async
|
|
6
|
+
distillation. Pure stdlib, so hook scripts never fail on a missing import.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
__version__ = "0.3.0"
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Store integrity audit + pruning.
|
|
2
|
+
|
|
3
|
+
Link integrity: the index links to real files on disk (``store.rebuild_index``
|
|
4
|
+
is path-based), so a dead link — or an active memory the index doesn't link —
|
|
5
|
+
just means the index is stale; ``heal_index`` rebuilds it. Pollution: a memory
|
|
6
|
+
whose title/content is a structural tooling artifact (markdown table, code
|
|
7
|
+
fence, status glyphs, the local-command caveat — distill's strict detector) is
|
|
8
|
+
never durable knowledge and can be pruned. The strict detector deliberately
|
|
9
|
+
excludes prose that merely mentions MEMORY.md so legitimate foldcrumbs design notes
|
|
10
|
+
are never deleted. Superseded/deleted records keep their
|
|
11
|
+
files but drop out of the index/recall; ``prune`` clears those too.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import re
|
|
17
|
+
|
|
18
|
+
from . import config, store
|
|
19
|
+
from .distill import _is_hard_artifact
|
|
20
|
+
|
|
21
|
+
_LINK_RE = re.compile(r"\]\(([^)]+\.md)\)")
|
|
22
|
+
# compute_confidence below this is low-trust (stale/contradicted); prune only on
|
|
23
|
+
# explicit request, never automatically.
|
|
24
|
+
STALE_CONF = 0.3
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _name(m) -> str:
|
|
28
|
+
return m.source_path or m.filename()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _index_links(cwd=None) -> set[str]:
|
|
32
|
+
p = config.index_path(cwd)
|
|
33
|
+
if not p.exists():
|
|
34
|
+
return set()
|
|
35
|
+
try:
|
|
36
|
+
return set(_LINK_RE.findall(p.read_text(encoding="utf-8")))
|
|
37
|
+
except OSError:
|
|
38
|
+
return set()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def audit(cwd=None) -> dict:
|
|
42
|
+
"""Read-only report: dead index links, orphaned active memories (on disk but
|
|
43
|
+
unlinked), artifact pollution, and low-trust/stale memories."""
|
|
44
|
+
linked = _index_links(cwd)
|
|
45
|
+
mems = list(store.iter_memories(cwd))
|
|
46
|
+
active = [m for m in mems if m.status == "active"]
|
|
47
|
+
on_disk = {_name(m) for m in mems}
|
|
48
|
+
active_names = {_name(m) for m in active}
|
|
49
|
+
return {
|
|
50
|
+
"dead_links": sorted(t for t in linked if t not in on_disk),
|
|
51
|
+
"orphans": sorted(n for n in active_names if n not in linked),
|
|
52
|
+
"pollution": sorted(_name(m) for m in active
|
|
53
|
+
if _is_hard_artifact(m.title) or _is_hard_artifact(m.content)),
|
|
54
|
+
"stale": sorted(_name(m) for m in active
|
|
55
|
+
if m.compute_confidence() < STALE_CONF),
|
|
56
|
+
"active": len(active),
|
|
57
|
+
"total": len(mems),
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def heal_index(cwd=None) -> bool:
|
|
62
|
+
"""Rebuild the index if it is stale (dead links or unlinked active memories).
|
|
63
|
+
|
|
64
|
+
Cheap and idempotent; returns True iff it rebuilt. Callers that share a store
|
|
65
|
+
across machines should gate this on ``config.distill_enabled()`` so only a
|
|
66
|
+
writing machine repairs (avoids sync churn)."""
|
|
67
|
+
a = audit(cwd)
|
|
68
|
+
if a["dead_links"] or a["orphans"]:
|
|
69
|
+
store.rebuild_index(cwd)
|
|
70
|
+
return True
|
|
71
|
+
return False
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _delete(name: str, cwd=None) -> bool:
|
|
75
|
+
try:
|
|
76
|
+
(config.memory_dir(cwd) / name).unlink()
|
|
77
|
+
return True
|
|
78
|
+
except OSError:
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def prune_artifacts(cwd=None) -> list[str]:
|
|
83
|
+
"""Delete active memories whose text is a clear tooling artifact, then rebuild
|
|
84
|
+
the index. Conservative — only unambiguous artifacts. Returns deleted names."""
|
|
85
|
+
removed = [
|
|
86
|
+
_name(m)
|
|
87
|
+
for m in list(store.iter_memories(cwd))
|
|
88
|
+
if m.status == "active" and (_is_hard_artifact(m.title) or _is_hard_artifact(m.content))
|
|
89
|
+
]
|
|
90
|
+
removed = [n for n in removed if _delete(n, cwd)]
|
|
91
|
+
if removed:
|
|
92
|
+
store.rebuild_index(cwd)
|
|
93
|
+
return removed
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def prune(cwd=None, apply: bool = False, include_stale: bool = False) -> dict:
|
|
97
|
+
"""Find (and with ``apply``, delete) prune candidates.
|
|
98
|
+
|
|
99
|
+
Candidates: superseded/deleted records (files left behind), active artifact
|
|
100
|
+
pollution, and — only with ``include_stale`` — low-trust active memories.
|
|
101
|
+
Dry-run by default; rebuilds the index when it deletes anything."""
|
|
102
|
+
candidates: dict[str, str] = {}
|
|
103
|
+
for m in store.iter_memories(cwd):
|
|
104
|
+
name = _name(m)
|
|
105
|
+
if m.status in ("deleted", "superseded"):
|
|
106
|
+
candidates[name] = "superseded/deleted"
|
|
107
|
+
elif m.status == "active" and (_is_hard_artifact(m.title) or _is_hard_artifact(m.content)):
|
|
108
|
+
candidates[name] = "artifact"
|
|
109
|
+
elif (include_stale and m.status == "active"
|
|
110
|
+
and m.compute_confidence() < STALE_CONF):
|
|
111
|
+
candidates[name] = "stale"
|
|
112
|
+
removed: list[str] = []
|
|
113
|
+
if apply and candidates:
|
|
114
|
+
removed = [n for n in candidates if _delete(n, cwd)]
|
|
115
|
+
if removed:
|
|
116
|
+
store.rebuild_index(cwd)
|
|
117
|
+
return {"candidates": candidates, "removed": removed, "applied": apply}
|