lcode-cli 0.6.1__tar.gz → 0.7.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.
Files changed (95) hide show
  1. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/CHANGELOG.md +16 -1
  2. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/PKG-INFO +8 -1
  3. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/README.md +7 -0
  4. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/docs/configuration.md +3 -0
  5. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/docs/how-it-works.md +5 -3
  6. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/docs/index.md +7 -0
  7. lcode_cli-0.7.0/docs/memory.md +128 -0
  8. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/docs/quickstart.md +2 -1
  9. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/docs/usage.md +7 -0
  10. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/mkdocs.yml +1 -0
  11. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/pyproject.toml +1 -0
  12. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/src/lcode/__init__.py +1 -1
  13. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/src/lcode/agent.py +26 -4
  14. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/src/lcode/cli.py +10 -0
  15. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/src/lcode/config.py +8 -0
  16. lcode_cli-0.7.0/src/lcode/memory.py +517 -0
  17. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/src/lcode/permissions.py +8 -3
  18. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/src/lcode/repl.py +137 -0
  19. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/src/lcode/tools.py +38 -1
  20. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/tests/conftest.py +3 -1
  21. lcode_cli-0.7.0/tests/test_memory.py +341 -0
  22. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/uv.lock +23 -23
  23. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/.editorconfig +0 -0
  24. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/.github/CODEOWNERS +0 -0
  25. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  26. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  27. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  28. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/.github/ISSUE_TEMPLATE/model_request.yml +0 -0
  29. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  30. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/.github/dependabot.yml +0 -0
  31. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/.github/workflows/ci.yml +0 -0
  32. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/.github/workflows/docs.yml +0 -0
  33. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/.github/workflows/release.yml +0 -0
  34. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/.gitignore +0 -0
  35. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/.pre-commit-config.yaml +0 -0
  36. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/AGENTS.md +0 -0
  37. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/CODE_OF_CONDUCT.md +0 -0
  38. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/CONTRIBUTING.md +0 -0
  39. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/LICENSE +0 -0
  40. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/SECURITY.md +0 -0
  41. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/docs/assets/demo.svg +0 -0
  42. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/docs/assets/extra.css +0 -0
  43. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/docs/assets/logo.svg +0 -0
  44. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/docs/assets/social-preview.png +0 -0
  45. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/docs/bench.md +0 -0
  46. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/docs/changelog.md +0 -0
  47. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/docs/contributing.md +0 -0
  48. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/docs/installation.md +0 -0
  49. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/docs/mcp.md +0 -0
  50. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/docs/models.md +0 -0
  51. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/docs/sandbox.md +0 -0
  52. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/docs/troubleshooting.md +0 -0
  53. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/install.sh +0 -0
  54. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/overrides/main.html +0 -0
  55. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/scripts/record_demo.py +0 -0
  56. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/scripts/social-preview.html +0 -0
  57. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/src/lcode/__main__.py +0 -0
  58. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/src/lcode/bench.py +0 -0
  59. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/src/lcode/catalog.py +0 -0
  60. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/src/lcode/checkpoints.py +0 -0
  61. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/src/lcode/hardware.py +0 -0
  62. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/src/lcode/limits.py +0 -0
  63. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/src/lcode/mcp/__init__.py +0 -0
  64. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/src/lcode/mcp/auth.py +0 -0
  65. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/src/lcode/mcp/catalog.py +0 -0
  66. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/src/lcode/mcp/catalog.toml +0 -0
  67. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/src/lcode/mcp/client.py +0 -0
  68. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/src/lcode/mcp/commands.py +0 -0
  69. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/src/lcode/mcp/config.py +0 -0
  70. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/src/lcode/mcp/http.py +0 -0
  71. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/src/lcode/mcp/manager.py +0 -0
  72. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/src/lcode/mcp/protocol.py +0 -0
  73. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/src/lcode/mcp/stdio.py +0 -0
  74. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/src/lcode/models.toml +0 -0
  75. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/src/lcode/ollama.py +0 -0
  76. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/src/lcode/render.py +0 -0
  77. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/src/lcode/sandbox.py +0 -0
  78. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/src/lcode/sessions.py +0 -0
  79. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/src/lcode/vision.py +0 -0
  80. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/src/lcode/web.py +0 -0
  81. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/tests/fake_mcp_server.py +0 -0
  82. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/tests/test_agent.py +0 -0
  83. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/tests/test_bench.py +0 -0
  84. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/tests/test_catalog.py +0 -0
  85. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/tests/test_checkpoints.py +0 -0
  86. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/tests/test_cli.py +0 -0
  87. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/tests/test_config.py +0 -0
  88. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/tests/test_context_picker.py +0 -0
  89. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/tests/test_mcp.py +0 -0
  90. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/tests/test_permissions.py +0 -0
  91. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/tests/test_sandbox.py +0 -0
  92. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/tests/test_sessions.py +0 -0
  93. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/tests/test_tools.py +0 -0
  94. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/tests/test_vision.py +0 -0
  95. {lcode_cli-0.6.1 → lcode_cli-0.7.0}/tests/test_web.py +0 -0
@@ -6,6 +6,20 @@ All notable changes to lcode are documented here. The format follows
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.7.0] - 2026-10-05
10
+
11
+ ### Added
12
+
13
+ - Memory across sessions: lcode keeps short notes that later sessions load, only key knowledge and
14
+ not whole conversations. The model saves them with its new `memory` tool (and updates or deletes
15
+ them), you add them with `/remember`, and when a session ends (on quit, `/clear` or before
16
+ compaction) one quick question asks the model what from it is worth remembering. Notes are
17
+ per repository, shared by its git worktrees, plus a few for every repository; they're plain
18
+ markdown files, managed with `/memory`. The index in the system prompt is capped at about 2,000
19
+ tokens. Notes that look like they contain secrets are never saved. The `memory` setting is `ask`
20
+ (confirm each note, the default), `auto` or `off`; `--no-memory` turns it off for a session.
21
+ See [Memory](https://nasser1941.github.io/lcode/memory/).
22
+
9
23
  ## [0.6.1] - 2026-10-01
10
24
 
11
25
  ### Fixed
@@ -211,7 +225,8 @@ First public release.
211
225
  - `AGENTS.md` project instructions and `/init` to generate them.
212
226
  - One-line installer for Linux and macOS.
213
227
 
214
- [Unreleased]: https://github.com/nasser1941/lcode/compare/v0.6.1...HEAD
228
+ [Unreleased]: https://github.com/nasser1941/lcode/compare/v0.7.0...HEAD
229
+ [0.7.0]: https://github.com/nasser1941/lcode/compare/v0.6.1...v0.7.0
215
230
  [0.6.1]: https://github.com/nasser1941/lcode/compare/v0.6.0...v0.6.1
216
231
  [0.6.0]: https://github.com/nasser1941/lcode/compare/v0.5.0...v0.6.0
217
232
  [0.5.0]: https://github.com/nasser1941/lcode/compare/v0.4.1...v0.5.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: lcode-cli
3
- Version: 0.6.1
3
+ Version: 0.7.0
4
4
  Summary: A local-first terminal coding agent powered by open-weight models on your own GPU or Mac, via Ollama.
5
5
  Project-URL: Homepage, https://nasser1941.github.io/lcode/
6
6
  Project-URL: Documentation, https://nasser1941.github.io/lcode/
@@ -82,6 +82,9 @@ through [Ollama](https://ollama.com), and when it needs current information it c
82
82
  Podman) that only sees your project, without network access unless you allow it.
83
83
  - **Undo.** lcode saves a checkpoint before the model changes files; `/undo` takes back the last
84
84
  request's edits, new files and shell-command changes, without touching your git history.
85
+ - **Remembers what matters.** Corrections, decisions and where things live carry over to the next
86
+ session as short notes, saved by the model, by you (`/remember`) or by a quick check at the end
87
+ of a session. Not whole conversations, and never secrets.
85
88
  - **Bring your own model.** Eight curated open-weight models, all tested end to end, or any Ollama model
86
89
  with tool calling.
87
90
  - **MCP servers, ready to go.** Connect Jira and Confluence, GitHub, AWS, Google Drive, Grafana,
@@ -189,6 +192,10 @@ fills up. Details: [How it works](https://nasser1941.github.io/lcode/how-it-work
189
192
 
190
193
  See the pinned [Roadmap issue](https://github.com/nasser1941/lcode/issues/10). Next up:
191
194
 
195
+ - [Parallel subagents](https://github.com/nasser1941/lcode/issues/40) with their own context
196
+ - [Plan mode](https://github.com/nasser1941/lcode/issues/41) and [custom commands and skills](https://github.com/nasser1941/lcode/issues/42)
197
+ - [Ready-made MCP for Metabase, Encord and Valohai](https://github.com/nasser1941/lcode/issues/53)
198
+ - Then: [smarter context for small windows](https://github.com/nasser1941/lcode/issues/46), [language servers](https://github.com/nasser1941/lcode/issues/44), [a repository map with local embeddings](https://github.com/nasser1941/lcode/issues/45), [hooks](https://github.com/nasser1941/lcode/issues/43)
192
199
  - More models in the catalog, and test reports from your hardware ([report one](https://github.com/nasser1941/lcode/issues/new?template=model_request.yml))
193
200
 
194
201
  ## Contributing
@@ -48,6 +48,9 @@ through [Ollama](https://ollama.com), and when it needs current information it c
48
48
  Podman) that only sees your project, without network access unless you allow it.
49
49
  - **Undo.** lcode saves a checkpoint before the model changes files; `/undo` takes back the last
50
50
  request's edits, new files and shell-command changes, without touching your git history.
51
+ - **Remembers what matters.** Corrections, decisions and where things live carry over to the next
52
+ session as short notes, saved by the model, by you (`/remember`) or by a quick check at the end
53
+ of a session. Not whole conversations, and never secrets.
51
54
  - **Bring your own model.** Eight curated open-weight models, all tested end to end, or any Ollama model
52
55
  with tool calling.
53
56
  - **MCP servers, ready to go.** Connect Jira and Confluence, GitHub, AWS, Google Drive, Grafana,
@@ -155,6 +158,10 @@ fills up. Details: [How it works](https://nasser1941.github.io/lcode/how-it-work
155
158
 
156
159
  See the pinned [Roadmap issue](https://github.com/nasser1941/lcode/issues/10). Next up:
157
160
 
161
+ - [Parallel subagents](https://github.com/nasser1941/lcode/issues/40) with their own context
162
+ - [Plan mode](https://github.com/nasser1941/lcode/issues/41) and [custom commands and skills](https://github.com/nasser1941/lcode/issues/42)
163
+ - [Ready-made MCP for Metabase, Encord and Valohai](https://github.com/nasser1941/lcode/issues/53)
164
+ - Then: [smarter context for small windows](https://github.com/nasser1941/lcode/issues/46), [language servers](https://github.com/nasser1941/lcode/issues/44), [a repository map with local embeddings](https://github.com/nasser1941/lcode/issues/45), [hooks](https://github.com/nasser1941/lcode/issues/43)
158
165
  - More models in the catalog, and test reports from your hardware ([report one](https://github.com/nasser1941/lcode/issues/new?template=model_request.yml))
159
166
 
160
167
  ## Contributing
@@ -30,6 +30,7 @@ lcode config path # print the file location
30
30
  | `sandbox_network` | `false` | Let commands in the sandbox use the network |
31
31
  | `vision_model` | `auto` | The model that [looks at images](usage.md#images): `auto`, `off` or an Ollama model that can see |
32
32
  | `mcp_tools` | `auto` | How [MCP](mcp.md#context) tool definitions reach the model: `auto`, `direct` or `search` (on demand) |
33
+ | `memory` | `ask` | [Notes that carry over](memory.md) to later sessions: `ask` (confirm each), `auto` or `off` |
33
34
  | `checkpoints` | `true` | Save a checkpoint before the model changes files, so [`/undo`](usage.md#undo-and-checkpoints) can restore them |
34
35
 
35
36
  Example file:
@@ -55,6 +56,7 @@ Environment variables override the file, which is useful for one-off runs and CI
55
56
  | `OLLAMA_HOST` | `ollama_host` (same variable the Ollama CLI uses) |
56
57
  | `LCODE_WEB` | `web` |
57
58
  | `LCODE_SANDBOX` | `sandbox` |
59
+ | `LCODE_MEMORY` | `memory` |
58
60
  | `OLLAMA_API_KEY`, `BRAVE_API_KEY`, `TAVILY_API_KEY` | API key for that [search provider](usage.md#web-search) (never stored in the config file) |
59
61
  | `LCODE_HOME` | where sessions and prompt history are stored |
60
62
 
@@ -69,6 +71,7 @@ Environment variables override the file, which is useful for one-off runs and CI
69
71
  | `~/.local/state/lcode/mcp-auth/` | Sign-in tokens for remote MCP servers (readable only by you) |
70
72
  | `~/.local/state/lcode/mcp-logs/` | Error output of local MCP servers |
71
73
  | `~/.local/state/lcode/checkpoints/` | Checkpoints for `/undo` (copies of your project's files; deleted after 14 days) |
74
+ | `~/.local/state/lcode/memory/` | [Memory](memory.md) notes: `user/` for every repository, `projects/<repository>/` for each one |
72
75
  | `~/.local/state/lcode/limits.json` | Context sizes that ran out of GPU memory on this machine (safe to delete) |
73
76
 
74
77
  Sessions contain everything the model read, including file contents. Delete the folder to clear them.
@@ -18,7 +18,7 @@ flowchart LR
18
18
  ## The agent loop
19
19
 
20
20
  1. Your message goes to the model along with a system prompt (environment, repository layout,
21
- `AGENTS.md`) and the JSON schemas of the tools.
21
+ `AGENTS.md`, the index of [memory notes](memory.md)) and the JSON schemas of the tools.
22
22
  2. The response streams back. Reasoning shows as a spinner (or text with `--show-thinking`); the
23
23
  answer is rendered as markdown, block by block.
24
24
  3. If the model called tools, lcode runs them, asking for permission where needed, and appends the
@@ -34,8 +34,9 @@ didn't match).
34
34
  Ollama keeps the processed conversation in its KV cache, so each turn only processes the new tokens.
35
35
  lcode tracks how full the window is from Ollama's token counts. At 85% it asks the model to write a
36
36
  summary of the conversation (goals, findings, files changed, next steps) and continues from that
37
- summary. Tool outputs are capped at 30,000 characters (head and tail kept) so one noisy command
38
- can't flood the window.
37
+ summary. Just before that, it asks whether anything from the conversation is worth
38
+ [remembering](memory.md), since the summary leaves details out. Tool outputs are capped at 30,000
39
+ characters (head and tail kept) so one noisy command can't flood the window.
39
40
 
40
41
  ## Memory sizing
41
42
 
@@ -78,6 +79,7 @@ the Ollama server you configure, it only contacts the web when the model searche
78
79
  | `mcp/` | MCP client: stdio and HTTP transports, OAuth sign-in, server settings, the catalog, `lcode mcp` |
79
80
  | `bench.py` | `lcode bench`: the benchmark tasks, their checks and the reports |
80
81
  | `vision.py` | Looking at images with a model that can see |
82
+ | `memory.py` | Notes that carry over between sessions, and the end-of-session check |
81
83
  | `sandbox.py` | The optional container for shell commands |
82
84
  | `checkpoints.py` | Snapshots before the model changes files, for `/undo` and `/rewind` |
83
85
  | `catalog.py`, `models.toml` | Model catalog and memory estimates |
@@ -69,6 +69,13 @@ until the task is done, asking before it changes anything.
69
69
  Every edit is shown as a diff and every non-read-only command needs your approval, unless you
70
70
  choose otherwise. `/undo` takes back a request's file changes.
71
71
 
72
+ - :material-brain:{ .lg .middle } **Remembers what matters**
73
+
74
+ ---
75
+
76
+ Your corrections, decisions and where things live carry over to the next session as short
77
+ [notes](memory.md), never whole conversations or secrets.
78
+
72
79
  - :material-connection:{ .lg .middle } **MCP servers**
73
80
 
74
81
  ---
@@ -0,0 +1,128 @@
1
+ # Memory
2
+
3
+ Every session used to start from zero. Whatever you and the model worked out was lost unless
4
+ someone wrote it into `AGENTS.md`: the test command that actually works, "we use pnpm, not npm",
5
+ that the staging database needs the VPN, a correction you had to give twice.
6
+
7
+ lcode now keeps short **notes** that later sessions load: only key knowledge, not whole
8
+ conversations. A note is one fact, in one line, with optional details.
9
+
10
+ ```text
11
+ ❯ /memory
12
+ Memory
13
+ # For Type Note Updated
14
+ 1 every repo feedback Prefers short answers without a summary at the end 2026-10-03
15
+ 2 this repo project Run the tests with `uv run pytest -q`; plain pyte… 2026-10-05
16
+ 3 this repo reference Staging logs are in Grafana, dashboard "api-prod" 2026-10-04
17
+ ```
18
+
19
+ ## What gets saved
20
+
21
+ Only what a future session needs and can't find in the code, the git history or `AGENTS.md`. There
22
+ are four kinds:
23
+
24
+ | Type | For | Example |
25
+ |---|---|---|
26
+ | `feedback` | Your corrections and preferences | "Use pnpm, not npm" |
27
+ | `project` | Decisions, constraints, ongoing work | "The staging database needs the VPN" |
28
+ | `reference` | Where things live outside the repository | "Bugs are tracked in Linear, project API" |
29
+ | `user` | Who you are and how you like to work | "Prefers short answers" |
30
+
31
+ Never saved: code structure, file paths, what changed in a session (git has that), temporary
32
+ state, and **secrets**. lcode refuses any note that looks like it contains a password, API key,
33
+ token, private key or a URL with a password in it, whoever writes it. Write where the secret lives
34
+ instead ("staging credentials are in 1Password, vault Dev").
35
+
36
+ ## How notes get saved
37
+
38
+ - **The model** saves a note with its `memory` tool when it learns something worth keeping, for
39
+ example after you correct it. It can also update a note (same name) or delete one that turned out
40
+ wrong.
41
+ - **You** save one with `/remember`:
42
+
43
+ ```text
44
+ ❯ /remember the staging database needs the VPN
45
+ ❯ /remember feedback: don't add docstrings to private functions
46
+ ❯ /remember -g user: I'm a backend developer; explain frontend code in more detail
47
+ ```
48
+
49
+ `-g` saves it for every repository. Start with `feedback:`, `reference:` or `user:` to set the
50
+ type (the default is `project`).
51
+
52
+ - **At the end of a session**, because local models rarely save anything on their own, lcode
53
+ asks the model one short question: is anything from this session worth remembering? It happens
54
+ when you quit, on `/clear`, and before the conversation is compacted (the summary leaves details
55
+ out). The model proposes at most three notes:
56
+
57
+ ```text
58
+ ╭─ Worth remembering? ────────────────────────────────────────────────────────╮
59
+ │ 1 [feedback] Use pnpm, not npm (this repository) │
60
+ │ 2 [project] Integration tests need `docker compose up db` first (this │
61
+ │ repository) │
62
+ ╰───────────────────────────────────────────────────────────────────────────────╯
63
+ Save them? [y]es / numbers to keep, e.g. 1,3 / [n]o: 1
64
+ ```
65
+
66
+ The question is quick: it reuses the conversation Ollama already has in memory, so the model
67
+ only writes a few lines. Short sessions (one request and little work) are skipped, and Ctrl+C
68
+ skips it too.
69
+
70
+ ## Asking first: the `memory` setting
71
+
72
+ | `memory` | |
73
+ |---|---|
74
+ | `ask` (default) | Every note the model saves, and the end-of-session proposals, need your OK. Answer `a` once to allow the model's notes for the rest of the session. This holds in `yolo` mode too. |
75
+ | `auto` | Notes are saved without asking; lcode prints each one. |
76
+ | `off` | No notes are loaded or saved, and the model has no `memory` tool. |
77
+
78
+ ```bash
79
+ lcode config set memory auto
80
+ ```
81
+
82
+ `lcode --no-memory` turns memory off for one session. `lcode -p` runs never ask the end-of-session
83
+ question.
84
+
85
+ ## Managing notes
86
+
87
+ | Command | |
88
+ |---|---|
89
+ | `/memory` | List the notes for this repository and for every repository |
90
+ | `/memory show 2` | Show a note with its details (by number or name) |
91
+ | `/memory edit 2` | Open it in your editor (`$VISUAL`, `$EDITOR`, nano or vi) |
92
+ | `/memory delete 2` | Delete it |
93
+ | `/memory path` | Where the notes are stored |
94
+
95
+ Changes you make with `/memory` apply from the next session. Notes added with `/remember` are
96
+ also mentioned to the model right away.
97
+
98
+ ## How it works
99
+
100
+ Notes are plain markdown files with a small header, one fact per file:
101
+
102
+ ```markdown
103
+ ---
104
+ name: test-command
105
+ type: project
106
+ description: Run the tests with `uv run pytest -q`; plain pytest misses the dev dependencies
107
+ modified: 2026-10-05
108
+ ---
109
+ The dev dependencies are in a uv dependency group, which plain pytest doesn't see.
110
+ ```
111
+
112
+ | Folder | Notes |
113
+ |---|---|
114
+ | `~/.local/state/lcode/memory/projects/<repository>/` | For one repository, shared by all its git worktrees and subfolders |
115
+ | `~/.local/state/lcode/memory/user/` | For every repository |
116
+
117
+ Each folder also has a `MEMORY.md` listing its notes, for reading; lcode regenerates it, so edit
118
+ the notes rather than that file. A folder that isn't in a git repository gets its own notes.
119
+
120
+ At the start of a session, the model gets a one-line index of the notes (description, name, date)
121
+ in its instructions. The index is capped at about 2,000 tokens, or 5% of a small context window;
122
+ when there are more notes, the newest are shown and the model can list the rest with its `memory`
123
+ tool, which also reads a note's details. The dates let stale facts stand out, and the model is told
124
+ to trust what it finds in the code over an old note, and to fix the note.
125
+
126
+ Saving under an existing name replaces that note. A new note that says nearly the same as another
127
+ one is not saved twice: the model is told to update the existing note instead, and
128
+ end-of-session proposals that only restate a note are dropped.
@@ -12,12 +12,13 @@ lcode
12
12
 
13
13
  ```text
14
14
  ╭──────────────────────────────────────────────╮
15
- │ lcode v0.6.1 — local coding agent │
15
+ │ lcode v0.7.0 — local coding agent │
16
16
  │ │
17
17
  │ model lcode-qwen3.6-35b │
18
18
  │ context 256K tokens │
19
19
  │ cwd /home/you/code/my-project │
20
20
  │ mode ask (Shift+Tab to cycle) │
21
+ │ memory ask · no notes yet (/remember) │
21
22
  ╰──────────────────────────────────────────────╯
22
23
  ❯
23
24
  ```
@@ -25,6 +25,7 @@ lcode mcp [catalog|add|list] # connect MCP servers: Jira, GitHub, AWS, databases
25
25
  | `--no-think` | Turn off the model's reasoning: faster, less accurate |
26
26
  | `--no-web` | No web search or page fetching in this session |
27
27
  | `--no-mcp` | Don't start [MCP servers](mcp.md) in this session |
28
+ | `--no-memory` | Don't load or save [memory notes](memory.md) in this session |
28
29
  | `--sandbox` | Run shell commands in a container that only sees the project ([Sandbox](sandbox.md)) |
29
30
  | `--show-thinking` | Print the model's reasoning as it streams |
30
31
  | `-V, --version` | Print the version |
@@ -56,6 +57,8 @@ reasoning is on.
56
57
  | `/undo` | Undo the file changes of the last request ([details](#undo-and-checkpoints)) |
57
58
  | `/rewind [N]` | Go back to before request N: its files, and optionally the conversation |
58
59
  | `/checkpoints` | List the requests that changed files, and which files |
60
+ | `/remember [-g] TEXT` | Save a [note](memory.md) for later sessions (`-g`: for every repository) |
61
+ | `/memory [show\|edit\|delete N]` | The [notes](memory.md) lcode remembers |
59
62
  | `/sandbox [network on\|off]` | The [sandbox](sandbox.md) for shell commands |
60
63
  | `/mcp [tools\|login\|restart NAME]` | [MCP servers](mcp.md), their status and tools |
61
64
  | `/compact [focus]` | Summarize the conversation to free context |
@@ -225,6 +228,10 @@ At startup lcode reads `AGENTS.md` (or `LCODE.md`, or `CLAUDE.md`) from the work
225
228
  adds it to the model's instructions. Use it for build and test commands, architecture notes and
226
229
  conventions. `/init` writes a first version for you.
227
230
 
231
+ What doesn't belong in `AGENTS.md` but should carry over, such as your corrections and preferences,
232
+ goes into lcode's [memory](memory.md): short notes the model, you (`/remember`) or a short check at
233
+ the end of a session save, and that every later session in the repository loads.
234
+
228
235
  ## Sessions
229
236
 
230
237
  Every conversation is saved after each request to `~/.local/state/lcode/sessions/`, so you can
@@ -92,6 +92,7 @@ nav:
92
92
  - Quickstart: quickstart.md
93
93
  - Guide:
94
94
  - Using lcode: usage.md
95
+ - Memory: memory.md
95
96
  - Models & context windows: models.md
96
97
  - Benchmarking models: bench.md
97
98
  - MCP servers: mcp.md
@@ -69,6 +69,7 @@ ignore = [
69
69
 
70
70
  [tool.ruff.lint.per-file-ignores]
71
71
  "src/lcode/agent.py" = ["E501"] # the prompts are prose; wrapping them would change what the model sees
72
+ "src/lcode/memory.py" = ["E501"]
72
73
 
73
74
  [tool.pytest.ini_options]
74
75
  testpaths = ["tests"]
@@ -1,3 +1,3 @@
1
1
  """lcode — a local-first terminal coding agent powered by open-weight models via Ollama."""
2
2
 
3
- __version__ = "0.6.1"
3
+ __version__ = "0.7.0"
@@ -21,6 +21,7 @@ from rich.panel import Panel
21
21
  from rich.text import Text
22
22
 
23
23
  from lcode import catalog, limits, sessions, vision, web
24
+ from lcode import memory as memory_notes
24
25
  from lcode.checkpoints import Checkpoints
25
26
  from lcode.config import format_tokens
26
27
  from lcode.mcp import McpManager
@@ -29,6 +30,7 @@ from lcode.permissions import Permissions
29
30
  from lcode.render import MarkdownStreamer
30
31
  from lcode.sandbox import Sandbox, SandboxError, project_root
31
32
  from lcode.tools import (
33
+ MEMORY_SCHEMA,
32
34
  SCHEMAS,
33
35
  VIEW_IMAGE_SCHEMA,
34
36
  WEB_FETCH_SCHEMA,
@@ -74,7 +76,7 @@ SYSTEM_PROMPT = """You are lcode, an autonomous software-engineering agent runni
74
76
 
75
77
  # Top-level layout of the working directory
76
78
  {tree}
77
- {web}{memory}"""
79
+ {web}{project}{notes}"""
78
80
 
79
81
  WEB_PROMPT = """
80
82
  # Web access
@@ -148,6 +150,7 @@ class Settings:
148
150
  sandbox_image: str | None = None
149
151
  sandbox_network: bool = False
150
152
  vision_model: str = "auto" # auto, off or an Ollama model that can see images
153
+ memory: str = "off" # off, ask or auto: notes that carry over to later sessions (lcode.memory)
151
154
 
152
155
 
153
156
  class Agent:
@@ -170,6 +173,9 @@ class Agent:
170
173
  self._vision: str | bool | None = False # the model that looks at images; False = not decided yet
171
174
  self.session_name = ""
172
175
  self.session_title = ""
176
+ self.interactive = True # False for `lcode -p`: no end-of-session questions
177
+ self.reflected = 0 # messages before this index were already checked for notes to remember
178
+ self._memory: memory_notes.Memory | None = None
173
179
  self.ctx_used = 0
174
180
  self.last_speed = 0.0
175
181
  self.usage = {"requests": 0, "prompt_tokens": 0, "prompt_ns": 0, "output_tokens": 0, "output_ns": 0}
@@ -182,11 +188,11 @@ class Agent:
182
188
  return dt.datetime.now().strftime("%Y%m%d-%H%M%S-") + uuid.uuid4().hex[:6]
183
189
 
184
190
  def system_prompt(self) -> str:
185
- memory = ""
191
+ project = ""
186
192
  for name in PROJECT_FILES:
187
193
  f = self.cwd / name
188
194
  if f.is_file():
189
- memory = f"\n# Project instructions ({name})\n{truncate(f.read_text(errors='replace'), 20_000)}\n"
195
+ project = f"\n# Project instructions ({name})\n{truncate(f.read_text(errors='replace'), 20_000)}\n"
190
196
  break
191
197
  return SYSTEM_PROMPT.format(
192
198
  model=self.settings.model,
@@ -196,10 +202,17 @@ class Agent:
196
202
  date=dt.date.today().isoformat(),
197
203
  git=git_info(self.cwd),
198
204
  tree=tree(self.cwd, depth=1, limit=120),
199
- memory=memory,
205
+ project=project,
206
+ notes=self.memory().prompt(self.settings.context) if self.settings.memory != "off" else "",
200
207
  web=self.web_prompt(),
201
208
  )
202
209
 
210
+ def memory(self) -> memory_notes.Memory:
211
+ """The notes for the current folder's repository, plus the user's own."""
212
+ if self._memory is None or self._memory.cwd != self.cwd:
213
+ self._memory = memory_notes.Memory(self.cwd)
214
+ return self._memory
215
+
203
216
  def search_backend(self) -> str | None:
204
217
  if self.settings.web == "off":
205
218
  return None
@@ -211,6 +224,8 @@ class Agent:
211
224
  schemas += [*([WEB_SEARCH_SCHEMA] if self.search_backend() else []), WEB_FETCH_SCHEMA]
212
225
  if self.vision_model():
213
226
  schemas.append(VIEW_IMAGE_SCHEMA)
227
+ if self.settings.memory != "off":
228
+ schemas.append(MEMORY_SCHEMA)
214
229
  if self.mcp:
215
230
  schemas += self.mcp.schemas(self.settings.context)
216
231
  return schemas
@@ -301,6 +316,7 @@ class Agent:
301
316
  self.messages = [{"role": "system", "content": self.system_prompt()}]
302
317
  self._mcp_prompt = ""
303
318
  self.tools.read_mtimes.clear()
319
+ self.reflected = len(self.messages)
304
320
  self.ctx_used = len(self.messages[0]["content"]) // 3
305
321
 
306
322
  def session_file(self) -> Path:
@@ -357,6 +373,7 @@ class Agent:
357
373
  self.session_name = data.get("name", "")
358
374
  self.session_title = data.get("title") or sessions.title_from(self.messages)
359
375
  self.tools.read_mtimes.clear() # files may have changed since; the model must read them again
376
+ self.reflected = len(self.messages) # checked for notes to remember when that session ended
360
377
  self.ctx_used = sum(len(json.dumps(m)) for m in self.messages) // 3
361
378
  return note
362
379
 
@@ -513,6 +530,8 @@ class Agent:
513
530
  return f"glob({args.get('pattern', '')})"
514
531
  if name == "list_dir":
515
532
  return f"list_dir({args.get('path', '.')})"
533
+ if name == "memory":
534
+ return f"memory({' '.join(str(args.get(k) or '') for k in ('action', 'name')).strip()})"
516
535
  if self.mcp and self.mcp.owns(name):
517
536
  if name == "mcp_find_tools":
518
537
  return f"mcp_find_tools({args.get('query', '')!r})"
@@ -646,6 +665,8 @@ class Agent:
646
665
  def compact(self, focus: str = "") -> None:
647
666
  if len(self.messages) <= 2:
648
667
  return
668
+ if self.interactive:
669
+ memory_notes.reflect(self) # what the summary leaves out is gone for good
649
670
  instructions = (
650
671
  "Summarize this conversation so the work can continue in a fresh context. Include: the user's goals "
651
672
  "and requests, key facts learned about the codebase (files, functions, paths with line numbers), "
@@ -664,6 +685,7 @@ class Agent:
664
685
  {"role": "assistant", "content": "Got it — I have the context from the summary and will continue."},
665
686
  ]
666
687
  self.ctx_used = sum(len(m["content"]) for m in self.messages) // 3
688
+ self.reflected = len(self.messages)
667
689
  self.console.print(Panel(Markdown(summary), title="Compacted summary", border_style="blue"))
668
690
 
669
691
  # -- settings changes
@@ -341,6 +341,13 @@ def cmd_doctor(args) -> None:
341
341
  network = "network on" if cfg["sandbox_network"] else "no network"
342
342
  line("Sandbox", problem or f"{cfg['sandbox']} · {network}", problem is None)
343
343
  ok &= problem is None
344
+ if cfg["memory"] == "off":
345
+ line("Memory", "off (lcode config set memory ask)", True)
346
+ else:
347
+ from lcode.memory import Memory
348
+
349
+ project, user = Memory(Path.cwd()).counts()
350
+ line("Memory", f"{cfg['memory']} · {project} note(s) for this repository, {user} for every repository", True)
344
351
  node = shutil.which("npx")
345
352
  line("Node.js", "found" if node else "not found (some MCP servers need npx)", True if node else None)
346
353
  sys.exit(0 if ok else 1)
@@ -509,8 +516,10 @@ def cmd_chat(args) -> None:
509
516
  sandbox_image=cfg["sandbox_image"],
510
517
  sandbox_network=cfg["sandbox_network"],
511
518
  vision_model=cfg["vision_model"],
519
+ memory="off" if args.no_memory else cfg["memory"],
512
520
  )
513
521
  agent = Agent(ollama, settings, cwd, console=console)
522
+ agent.interactive = not args.prompt
514
523
  if agent.sandbox:
515
524
  # Never run commands unsandboxed when the user asked for a sandbox: stop here instead.
516
525
  from lcode.sandbox import SandboxError
@@ -557,6 +566,7 @@ def build_parser() -> argparse.ArgumentParser:
557
566
  parser.add_argument("--no-think", action="store_true", help="disable model reasoning (faster, less accurate)")
558
567
  parser.add_argument("--no-web", action="store_true", help="don't let the model search or fetch web pages")
559
568
  parser.add_argument("--no-mcp", action="store_true", help="don't start MCP servers in this session")
569
+ parser.add_argument("--no-memory", action="store_true", help="don't load or save memory notes in this session")
560
570
  parser.add_argument("--sandbox", action="store_true", help="run shell commands in a container (Docker or Podman)")
561
571
  parser.add_argument("--show-thinking", action="store_true", help="print the model's reasoning as it streams")
562
572
  parser.add_argument("-V", "--version", action="version", version=f"lcode {__version__}")
@@ -26,6 +26,7 @@ WEB_MODES = ("on", "ask", "off")
26
26
  SEARCH_BACKENDS = ("auto", "ollama", "brave", "tavily", "searxng")
27
27
  MCP_TOOL_MODES = ("auto", "direct", "search")
28
28
  SANDBOX_ENGINES = ("off", "docker", "podman")
29
+ MEMORY_MODES = ("off", "ask", "auto")
29
30
 
30
31
  # key -> (default, type, help)
31
32
  SETTINGS: dict[str, tuple[object, type, str]] = {
@@ -45,6 +46,7 @@ SETTINGS: dict[str, tuple[object, type, str]] = {
45
46
  "sandbox_network": (False, bool, "let commands in the sandbox use the network"),
46
47
  "vision_model": ("auto", str, "model that looks at images: auto | off | an Ollama model with vision"),
47
48
  "mcp_tools": ("auto", str, "how MCP tools reach the model: auto | direct | search (on demand, saves context)"),
49
+ "memory": ("ask", str, "notes that carry over to later sessions: off | ask (confirm each) | auto"),
48
50
  }
49
51
  ENV_OVERRIDES = {
50
52
  "LCODE_MODEL": "model",
@@ -54,6 +56,7 @@ ENV_OVERRIDES = {
54
56
  "OLLAMA_HOST": "ollama_host",
55
57
  "LCODE_WEB": "web",
56
58
  "LCODE_SANDBOX": "sandbox",
59
+ "LCODE_MEMORY": "memory",
57
60
  }
58
61
 
59
62
 
@@ -110,6 +113,11 @@ def coerce(key: str, value: object) -> object:
110
113
  if value not in SANDBOX_ENGINES:
111
114
  raise ConfigError(f"sandbox must be one of {', '.join(SANDBOX_ENGINES)}")
112
115
  return value
116
+ if key == "memory":
117
+ value = {"on": "ask", "true": "ask", "false": "off", "no": "off"}.get(str(value).lower(), str(value).lower())
118
+ if value not in MEMORY_MODES:
119
+ raise ConfigError(f"memory must be one of {', '.join(MEMORY_MODES)}")
120
+ return value
113
121
  if key == "mcp_tools" and value not in MCP_TOOL_MODES:
114
122
  raise ConfigError(f"mcp_tools must be one of {', '.join(MCP_TOOL_MODES)}")
115
123
  if key == "search_backend" and value not in SEARCH_BACKENDS: