lcode-cli 0.4.1__tar.gz → 0.6.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 (92) hide show
  1. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/CHANGELOG.md +32 -1
  2. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/PKG-INFO +7 -3
  3. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/README.md +6 -2
  4. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/SECURITY.md +7 -2
  5. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/docs/configuration.md +5 -0
  6. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/docs/how-it-works.md +2 -0
  7. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/docs/mcp.md +53 -0
  8. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/docs/models.md +4 -2
  9. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/docs/quickstart.md +1 -1
  10. lcode_cli-0.6.0/docs/sandbox.md +79 -0
  11. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/docs/usage.md +36 -2
  12. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/mkdocs.yml +1 -0
  13. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/src/lcode/__init__.py +1 -1
  14. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/src/lcode/agent.py +90 -2
  15. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/src/lcode/cli.py +36 -0
  16. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/src/lcode/config.py +11 -0
  17. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/src/lcode/mcp/catalog.toml +28 -0
  18. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/src/lcode/mcp/config.py +5 -1
  19. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/src/lcode/mcp/manager.py +8 -3
  20. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/src/lcode/mcp/protocol.py +5 -2
  21. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/src/lcode/ollama.py +4 -0
  22. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/src/lcode/repl.py +33 -0
  23. lcode_cli-0.6.0/src/lcode/sandbox.py +204 -0
  24. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/src/lcode/tools.py +86 -28
  25. lcode_cli-0.6.0/src/lcode/vision.py +109 -0
  26. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/tests/conftest.py +14 -2
  27. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/tests/test_mcp.py +18 -0
  28. lcode_cli-0.6.0/tests/test_sandbox.py +166 -0
  29. lcode_cli-0.6.0/tests/test_vision.py +104 -0
  30. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/.editorconfig +0 -0
  31. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/.github/CODEOWNERS +0 -0
  32. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  33. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  34. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  35. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/.github/ISSUE_TEMPLATE/model_request.yml +0 -0
  36. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  37. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/.github/dependabot.yml +0 -0
  38. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/.github/workflows/ci.yml +0 -0
  39. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/.github/workflows/docs.yml +0 -0
  40. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/.github/workflows/release.yml +0 -0
  41. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/.gitignore +0 -0
  42. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/.pre-commit-config.yaml +0 -0
  43. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/AGENTS.md +0 -0
  44. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/CODE_OF_CONDUCT.md +0 -0
  45. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/CONTRIBUTING.md +0 -0
  46. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/LICENSE +0 -0
  47. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/docs/assets/demo.svg +0 -0
  48. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/docs/assets/extra.css +0 -0
  49. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/docs/assets/logo.svg +0 -0
  50. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/docs/assets/social-preview.png +0 -0
  51. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/docs/bench.md +0 -0
  52. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/docs/changelog.md +0 -0
  53. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/docs/contributing.md +0 -0
  54. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/docs/index.md +0 -0
  55. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/docs/installation.md +0 -0
  56. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/docs/troubleshooting.md +0 -0
  57. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/install.sh +0 -0
  58. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/overrides/main.html +0 -0
  59. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/pyproject.toml +0 -0
  60. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/scripts/record_demo.py +0 -0
  61. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/scripts/social-preview.html +0 -0
  62. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/src/lcode/__main__.py +0 -0
  63. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/src/lcode/bench.py +0 -0
  64. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/src/lcode/catalog.py +0 -0
  65. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/src/lcode/checkpoints.py +0 -0
  66. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/src/lcode/hardware.py +0 -0
  67. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/src/lcode/limits.py +0 -0
  68. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/src/lcode/mcp/__init__.py +0 -0
  69. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/src/lcode/mcp/auth.py +0 -0
  70. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/src/lcode/mcp/catalog.py +0 -0
  71. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/src/lcode/mcp/client.py +0 -0
  72. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/src/lcode/mcp/commands.py +0 -0
  73. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/src/lcode/mcp/http.py +0 -0
  74. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/src/lcode/mcp/stdio.py +0 -0
  75. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/src/lcode/models.toml +0 -0
  76. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/src/lcode/permissions.py +0 -0
  77. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/src/lcode/render.py +0 -0
  78. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/src/lcode/sessions.py +0 -0
  79. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/src/lcode/web.py +0 -0
  80. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/tests/fake_mcp_server.py +0 -0
  81. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/tests/test_agent.py +0 -0
  82. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/tests/test_bench.py +0 -0
  83. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/tests/test_catalog.py +0 -0
  84. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/tests/test_checkpoints.py +0 -0
  85. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/tests/test_cli.py +0 -0
  86. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/tests/test_config.py +0 -0
  87. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/tests/test_context_picker.py +0 -0
  88. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/tests/test_permissions.py +0 -0
  89. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/tests/test_sessions.py +0 -0
  90. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/tests/test_tools.py +0 -0
  91. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/tests/test_web.py +0 -0
  92. {lcode_cli-0.4.1 → lcode_cli-0.6.0}/uv.lock +0 -0
@@ -6,6 +6,35 @@ All notable changes to lcode are documented here. The format follows
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.6.0] - 2026-10-01
10
+
11
+ ### Added
12
+
13
+ - lcode can look at images: attach a screenshot, mockup or diagram with `@path`, and the model can
14
+ open images itself with the new `view_image` tool. A model that can see describes the image in
15
+ detail (all text transcribed, with your question in mind): your model itself if it can see, or
16
+ the original model behind lcode's text-only variant, which `lcode setup` already downloaded
17
+ (`vision_model` setting; `lcode doctor` shows which). Screenshots returned by MCP tools, such as
18
+ Playwright's, are described too.
19
+ - `lcode mcp add comfyui`: generate and edit images with models you run locally in ComfyUI (FLUX,
20
+ SDXL, Stable Diffusion 1.5, Qwen-Image), through ComfyUI's official MCP server, with only its local
21
+ tools enabled.
22
+ - MCP servers can mark tools that need the GPU to themselves (`free_gpu` in `mcp.json`): lcode
23
+ unloads its own model before they run and reloads it afterwards. On for ComfyUI's generation tools,
24
+ so image models work on a 12 GB GPU next to a large coding model.
25
+
26
+ ## [0.5.0] - 2026-10-01
27
+
28
+ ### Added
29
+
30
+ - An optional sandbox for shell commands: with `lcode --sandbox` (or `sandbox = "docker"` /
31
+ `"podman"`), the model's commands run in a container that only sees the project folder, as your
32
+ user, with no network access unless you allow it (`/sandbox network on`, `sandbox_network`), no
33
+ Linux capabilities and a process limit. The file tools are limited to the project too. lcode builds
34
+ a default image with Python, Node.js, git and build tools on first use (`sandbox_image` for your
35
+ own). In `auto-edit` mode, sandboxed commands run without asking. If the sandbox can't start, lcode
36
+ stops instead of running commands without it.
37
+
9
38
  ## [0.4.1] - 2026-10-01
10
39
 
11
40
  ### Added
@@ -174,7 +203,9 @@ First public release.
174
203
  - `AGENTS.md` project instructions and `/init` to generate them.
175
204
  - One-line installer for Linux and macOS.
176
205
 
177
- [Unreleased]: https://github.com/nasser1941/lcode/compare/v0.4.1...HEAD
206
+ [Unreleased]: https://github.com/nasser1941/lcode/compare/v0.6.0...HEAD
207
+ [0.6.0]: https://github.com/nasser1941/lcode/compare/v0.5.0...v0.6.0
208
+ [0.5.0]: https://github.com/nasser1941/lcode/compare/v0.4.1...v0.5.0
178
209
  [0.4.1]: https://github.com/nasser1941/lcode/compare/v0.4.0...v0.4.1
179
210
  [0.4.0]: https://github.com/nasser1941/lcode/compare/v0.3.1...v0.4.0
180
211
  [0.3.1]: https://github.com/nasser1941/lcode/compare/v0.3.0...v0.3.1
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: lcode-cli
3
- Version: 0.4.1
3
+ Version: 0.6.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/
@@ -78,13 +78,18 @@ through [Ollama](https://ollama.com), and when it needs current information it c
78
78
  real coding tasks on your machine.
79
79
  - **Safe by default.** Every edit is shown as a diff and every command that isn't read-only needs
80
80
  your approval. `auto-edit` and `yolo` modes when you want speed.
81
+ - **Sandbox (optional).** `lcode --sandbox` runs the model's shell commands in a container (Docker or
82
+ Podman) that only sees your project, without network access unless you allow it.
81
83
  - **Undo.** lcode saves a checkpoint before the model changes files; `/undo` takes back the last
82
84
  request's edits, new files and shell-command changes, without touching your git history.
83
85
  - **Bring your own model.** Eight curated open-weight models, all tested end to end, or any Ollama model
84
86
  with tool calling.
85
87
  - **MCP servers, ready to go.** Connect Jira and Confluence, GitHub, AWS, Google Drive, Grafana,
86
88
  Google Cloud, Sentry, Linear, Notion, Postgres, Kubernetes and more with one command
87
- (`lcode mcp add atlassian`), or any other MCP server. Browser sign-in (OAuth) is built in.
89
+ (`lcode mcp add atlassian`), image generation with ComfyUI, or any other MCP server. Browser sign-in (OAuth) is built in.
90
+ - **Sees images.** Attach a screenshot or mockup with `@path` and lcode looks at it, using the
91
+ vision part of your model. With ComfyUI (`lcode mcp add comfyui`) it can also generate and edit
92
+ images with local models such as FLUX, SDXL and Qwen-Image.
88
93
  - **Web search when needed.** Looks up the latest versions, docs and error messages with Ollama web
89
94
  search, Brave, Tavily or your own SearXNG, and reads pages as clean text.
90
95
  - **Private.** The model runs on your machine, lcode never uploads your files and has no telemetry.
@@ -184,7 +189,6 @@ fills up. Details: [How it works](https://nasser1941.github.io/lcode/how-it-work
184
189
 
185
190
  See the pinned [Roadmap issue](https://github.com/nasser1941/lcode/issues/10). Next up:
186
191
 
187
- - An [optional sandbox for shell commands](https://github.com/nasser1941/lcode/issues/9)
188
192
  - More models in the catalog, and test reports from your hardware ([report one](https://github.com/nasser1941/lcode/issues/new?template=model_request.yml))
189
193
 
190
194
  ## Contributing
@@ -44,13 +44,18 @@ through [Ollama](https://ollama.com), and when it needs current information it c
44
44
  real coding tasks on your machine.
45
45
  - **Safe by default.** Every edit is shown as a diff and every command that isn't read-only needs
46
46
  your approval. `auto-edit` and `yolo` modes when you want speed.
47
+ - **Sandbox (optional).** `lcode --sandbox` runs the model's shell commands in a container (Docker or
48
+ Podman) that only sees your project, without network access unless you allow it.
47
49
  - **Undo.** lcode saves a checkpoint before the model changes files; `/undo` takes back the last
48
50
  request's edits, new files and shell-command changes, without touching your git history.
49
51
  - **Bring your own model.** Eight curated open-weight models, all tested end to end, or any Ollama model
50
52
  with tool calling.
51
53
  - **MCP servers, ready to go.** Connect Jira and Confluence, GitHub, AWS, Google Drive, Grafana,
52
54
  Google Cloud, Sentry, Linear, Notion, Postgres, Kubernetes and more with one command
53
- (`lcode mcp add atlassian`), or any other MCP server. Browser sign-in (OAuth) is built in.
55
+ (`lcode mcp add atlassian`), image generation with ComfyUI, or any other MCP server. Browser sign-in (OAuth) is built in.
56
+ - **Sees images.** Attach a screenshot or mockup with `@path` and lcode looks at it, using the
57
+ vision part of your model. With ComfyUI (`lcode mcp add comfyui`) it can also generate and edit
58
+ images with local models such as FLUX, SDXL and Qwen-Image.
54
59
  - **Web search when needed.** Looks up the latest versions, docs and error messages with Ollama web
55
60
  search, Brave, Tavily or your own SearXNG, and reads pages as clean text.
56
61
  - **Private.** The model runs on your machine, lcode never uploads your files and has no telemetry.
@@ -150,7 +155,6 @@ fills up. Details: [How it works](https://nasser1941.github.io/lcode/how-it-work
150
155
 
151
156
  See the pinned [Roadmap issue](https://github.com/nasser1941/lcode/issues/10). Next up:
152
157
 
153
- - An [optional sandbox for shell commands](https://github.com/nasser1941/lcode/issues/9)
154
158
  - More models in the catalog, and test reports from your hardware ([report one](https://github.com/nasser1941/lcode/issues/new?template=model_request.yml))
155
159
 
156
160
  ## Contributing
@@ -25,8 +25,13 @@ user, in your working directory**. It is not a sandbox. Keep in mind:
25
25
 
26
26
  - In the default `ask` mode, every file edit and every shell command that isn't on the read-only
27
27
  allowlist (`ls`, `cat`, `grep`, `git status`, …) is shown to you and needs your approval.
28
- `auto-edit` skips approval for edits; `yolo` skips all approval — use it only in disposable
29
- environments (a container, a VM, a throwaway clone).
28
+ `auto-edit` skips approval for edits; `yolo` skips all approval — use it only with the sandbox or
29
+ in disposable environments (a VM, a throwaway clone).
30
+ - The optional sandbox (`sandbox = "docker"` or `"podman"`, or `lcode --sandbox`) runs shell commands
31
+ in a container that sees only the project folder, as your user, without network access unless
32
+ allowed. The file tools are then limited to the project too. It doesn't cover web or MCP tools,
33
+ the project folder itself stays writable, and a container shares the host kernel; see
34
+ https://nasser1941.github.io/lcode/sandbox/ for its limits.
30
35
  - Content the model reads (files, command output, search results and web pages) can contain prompt
31
36
  injections that try to make it run harmful commands. lcode marks web content as untrusted, but
32
37
  review commands before approving them.
@@ -25,6 +25,10 @@ lcode config path # print the file location
25
25
  | `web` | `on` | Web search and page fetching: `on`, `ask` (before each search/website) or `off` |
26
26
  | `search_backend` | `auto` | `auto`, `ollama`, `brave`, `tavily` or `searxng`; `auto` uses the first one configured |
27
27
  | `searxng_url` | | Your SearXNG instance, e.g. `http://localhost:8888` |
28
+ | `sandbox` | `off` | Run shell commands in a container: `docker` or `podman` ([Sandbox](sandbox.md)) |
29
+ | `sandbox_image` | lcode's image | Container image for the sandbox; any image with bash and setsid |
30
+ | `sandbox_network` | `false` | Let commands in the sandbox use the network |
31
+ | `vision_model` | `auto` | The model that [looks at images](usage.md#images): `auto`, `off` or an Ollama model that can see |
28
32
  | `mcp_tools` | `auto` | How [MCP](mcp.md#context) tool definitions reach the model: `auto`, `direct` or `search` (on demand) |
29
33
  | `checkpoints` | `true` | Save a checkpoint before the model changes files, so [`/undo`](usage.md#undo-and-checkpoints) can restore them |
30
34
 
@@ -50,6 +54,7 @@ Environment variables override the file, which is useful for one-off runs and CI
50
54
  | `LCODE_KEEP_ALIVE` | `keep_alive` |
51
55
  | `OLLAMA_HOST` | `ollama_host` (same variable the Ollama CLI uses) |
52
56
  | `LCODE_WEB` | `web` |
57
+ | `LCODE_SANDBOX` | `sandbox` |
53
58
  | `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) |
54
59
  | `LCODE_HOME` | where sessions and prompt history are stored |
55
60
 
@@ -77,6 +77,8 @@ the Ollama server you configure, it only contacts the web when the model searche
77
77
  | `permissions.py` | Approval prompts and the read-only allowlist |
78
78
  | `mcp/` | MCP client: stdio and HTTP transports, OAuth sign-in, server settings, the catalog, `lcode mcp` |
79
79
  | `bench.py` | `lcode bench`: the benchmark tasks, their checks and the reports |
80
+ | `vision.py` | Looking at images with a model that can see |
81
+ | `sandbox.py` | The optional container for shell commands |
80
82
  | `checkpoints.py` | Snapshots before the model changes files, for `/undo` and `/rewind` |
81
83
  | `catalog.py`, `models.toml` | Model catalog and memory estimates |
82
84
  | `hardware.py` | NVIDIA / Apple Silicon detection |
@@ -28,6 +28,7 @@ and find the code for the top one."*
28
28
  | `gcp` | `gcloud` commands on your Google Cloud projects | `npx` and the `gcloud` CLI, signed in |
29
29
  | `github` | Repositories, issues, pull requests, Actions | a GitHub token (or the GitHub CLI) |
30
30
  | `playwright` | A real (headless) browser: open pages, click, fill forms | `npx` |
31
+ | `comfyui` | Generate and edit images with local models (FLUX, SDXL, SD 1.5, Qwen-Image) | `uvx` and [ComfyUI](#images-with-comfyui) running locally |
31
32
  | `context7` | Up-to-date docs and examples for thousands of libraries | nothing (an API key is optional) |
32
33
  | `sentry` | Errors, issues, traces and releases | a Sentry account (browser sign-in) |
33
34
  | `postgres` | Schemas, read-only queries, query performance | `uvx`, a connection URL |
@@ -82,6 +83,57 @@ Google Cloud project:
82
83
  See [Google's guide](https://developers.google.com/workspace/guides/configure-mcp-servers) for
83
84
  details.
84
85
 
86
+ ### Images with ComfyUI
87
+
88
+ [ComfyUI](https://github.com/comfyanonymous/ComfyUI) runs image generation and editing models
89
+ locally; its official MCP server lets lcode use it, for app icons, illustrations, mockups or
90
+ placeholder art.
91
+
92
+ ```bash
93
+ uv tool install comfy-cli && comfy install # ComfyUI, once
94
+ comfy launch --background -- --disable-smart-memory # start it (http://127.0.0.1:8188)
95
+ lcode mcp add comfyui
96
+ ```
97
+
98
+ Then add models in ComfyUI (its model manager, or ask lcode: *"download FLUX.2 Klein 4B in
99
+ ComfyUI"*), and ask for images: *"make a 512×512 app icon of a paper plane and save it in
100
+ assets/"*. To edit an image, ask lcode to upload it and run an editing workflow (Kontext or
101
+ Qwen-Image-Edit).
102
+
103
+ | Model | Good for | On a 12 GB GPU |
104
+ |---|---|---|
105
+ | FLUX.2 Klein 4B | Generation and editing, fast | Fits (about 8 GB) |
106
+ | SDXL and its community models | Generation, inpainting | Fits |
107
+ | Stable Diffusion 1.5 and its community models | Light generation, inpainting | Fits easily |
108
+ | FLUX.1 Dev and finetunes | High-quality generation | Needs an fp8 or GGUF version; slower |
109
+ | FLUX.1 Kontext Dev | Editing an image from instructions | Needs an fp8 or GGUF version; slower |
110
+ | Qwen-Image / Qwen-Image-Edit | Generation and editing, good text in images | Heavy: a GGUF version and RAM offloading |
111
+
112
+ Only ComfyUI's local tools are turned on; Comfy Cloud's partner tools are left out, so prompts and
113
+ images stay on your machine.
114
+
115
+ **Sharing one GPU.** A GPU rarely holds a coding model and an image model at the same time, so the
116
+ two take turns: before ComfyUI generates, lcode unloads its own model (the `free_gpu` setting), and
117
+ ComfyUI started with `--disable-smart-memory` gives the GPU back after each image. lcode's model
118
+ then reloads for the next step, which adds a few seconds to half a minute per image request. With
119
+ enough GPU memory for both (or on a Mac with plenty of memory), remove `free_gpu` from the server's
120
+ settings in `mcp.json`. lcode can also look at the results ([Images](usage.md#images)).
121
+
122
+ Measured on an RTX 4080 Laptop GPU (12 GB) with qwen3.6-35b at 64K context and FLUX.2 Klein Base 4B
123
+ (fp8, 512×512, 20 steps):
124
+
125
+ | | |
126
+ |---|---|
127
+ | Generating while lcode's model is loaded | Fails: ComfyUI runs out of GPU memory |
128
+ | Freeing the GPU (lcode unloads its model) | 0.2 s |
129
+ | Generating one image | about 12 s |
130
+ | Reloading lcode's model afterwards | about 20 s |
131
+
132
+ The first time, the model has to find the right template and fill in its settings (model file names,
133
+ size, prompt), which can take several minutes of trial and error. Once an image comes out right, ask
134
+ lcode to save that workflow in your project (for example `assets/icon.workflow.json`) and reuse it:
135
+ later images are a single `run_workflow` call.
136
+
85
137
  ## Adding your own servers
86
138
 
87
139
  Any MCP server works. For a remote server, give its URL; for a local one, the command that starts it:
@@ -122,6 +174,7 @@ document, so you can also paste a server's example config into that file:
122
174
  | `allow` | Tools that run without asking; `["*"]` for all of the server's tools |
123
175
  | `timeout` | Seconds a tool call may take (default 300) |
124
176
  | `disabled` | `true` to turn the server off (`lcode mcp disable <name>`) |
177
+ | `free_gpu` | Tools that need the GPU to themselves (`true` for all): lcode unloads its model first and reloads it afterwards |
125
178
  | `oauth` | For servers that need a pre-registered OAuth client: `client_id`, `client_secret`, `scopes`, `authorize_params` |
126
179
 
127
180
  Values can use environment variables: `${NAME}`, or `${NAME:-default}`. Keep tokens in environment
@@ -225,7 +225,9 @@ GPU's share of unified memory. Results from other Macs are very welcome in the
225
225
 
226
226
  ## Text-only variants
227
227
 
228
- Some models (the Qwen3.6 family, for example) ship with a vision encoder that a coding agent doesn't
229
- use. `lcode setup` creates a text-only variant named `lcode-<key>` that reuses the downloaded
228
+ Some models (the Qwen3.6 family, for example) ship with a vision encoder that a coding agent rarely
229
+ needs. `lcode setup` creates a text-only variant named `lcode-<key>` that reuses the downloaded
230
230
  weights, so it takes no extra disk space, and frees about 1 GB of GPU memory for the context cache
231
231
  (and for a larger prompt batch, where it fits; see [tuning](configuration.md#tuning-for-speed-and-memory)).
232
+ When you show lcode an image, it borrows the original model, vision encoder included, to look at it
233
+ (see [Images](usage.md#images)).
@@ -12,7 +12,7 @@ lcode
12
12
 
13
13
  ```text
14
14
  ╭──────────────────────────────────────────────╮
15
- │ lcode v0.4.1 — local coding agent │
15
+ │ lcode v0.6.0 — local coding agent │
16
16
  │ │
17
17
  │ model lcode-qwen3.6-35b │
18
18
  │ context 256K tokens │
@@ -0,0 +1,79 @@
1
+ # Sandbox
2
+
3
+ By default the model's shell commands run directly on your machine, as you. The permission prompts
4
+ let you check each command, but they're a guardrail, not isolation, and `yolo` mode removes even
5
+ that. The sandbox runs the commands in a container that can only see your project:
6
+
7
+ ```bash
8
+ lcode --sandbox # this session
9
+ lcode config set sandbox docker # every session (or podman)
10
+ ```
11
+
12
+ The session's banner then shows `sandbox docker · lcode's image · no network`.
13
+
14
+ ## What the container can and can't do
15
+
16
+ | | |
17
+ |---|---|
18
+ | **Sees** | Your project: the git repository you started lcode in (or the folder itself, outside a repository), mounted at the same path, read-write |
19
+ | **Doesn't see** | Anything else on your machine: your home folder, SSH keys, cloud credentials, other projects, the Docker socket |
20
+ | **Network** | None, unless you allow it (`/sandbox network on`, or `sandbox_network = true`) |
21
+ | **Runs as** | Your user ID, so files it creates belong to you; no Linux capabilities, no privilege escalation, at most 1,024 processes |
22
+ | **Lives** | For the session: started when lcode starts, removed when it exits. Containers left behind by a crash are removed the next time |
23
+
24
+ lcode's file tools (reading, editing, searching) run on your machine, but with the sandbox on they're
25
+ limited to the same project folder, so the model's whole world is the project.
26
+
27
+ ## Approvals
28
+
29
+ With the sandbox on, `auto-edit` mode runs shell commands without asking too, since they can only
30
+ change the project, and [`/undo`](usage.md#undo-and-checkpoints) can take those changes back. `ask`
31
+ mode still asks before every command that isn't read-only. `yolo` asks for nothing, with or without
32
+ the sandbox.
33
+
34
+ ## The image
35
+
36
+ lcode builds its default image the first time you use the sandbox (a minute or two): Python 3.12,
37
+ Node.js, git, build tools, ripgrep, curl and jq. Use your own image to match your project's toolchain:
38
+
39
+ ```bash
40
+ lcode config set sandbox_image node:22
41
+ ```
42
+
43
+ Any image with `bash` and `setsid` works. Installing packages (`pip install`, `npm install`) needs
44
+ the network: allow it with `/sandbox network on` for the session, or `sandbox_network = true`. The
45
+ model is told when a command failed because there's no network.
46
+
47
+ ## Settings
48
+
49
+ | Setting | Default | |
50
+ |---|---|---|
51
+ | `sandbox` | `off` | `docker` or `podman` to run commands in a container |
52
+ | `sandbox_image` | lcode's image | Any image with bash and setsid |
53
+ | `sandbox_network` | `false` | Let commands use the network |
54
+
55
+ In a session, `/sandbox` shows the sandbox and `/sandbox network on|off` changes network access (the
56
+ container restarts; files in the project stay). `lcode doctor` checks that Docker or Podman works.
57
+
58
+ ## Requirements
59
+
60
+ - **Linux:** [Docker Engine](https://docs.docker.com/engine/install/) (your user in the `docker`
61
+ group) or [Podman](https://podman.io/docs/installation) (rootless; lcode passes `--userns keep-id`.
62
+ Podman is less tested than Docker, so reports are welcome).
63
+ - **macOS:** Docker Desktop, [OrbStack](https://orbstack.dev) or [colima](https://github.com/abiosoft/colima).
64
+ Your project must be in a folder they share with containers (your home folder is, by default).
65
+
66
+ If the sandbox is on but can't start, lcode stops instead of running commands without it, and says
67
+ how to fix it.
68
+
69
+ ## What the sandbox doesn't cover
70
+
71
+ It isolates **shell commands**. Know its limits:
72
+
73
+ - The project folder is writable: a command can still delete or damage your project's files. Use git,
74
+ and `/undo` for changes since the request started.
75
+ - With the network on, commands can reach the internet, including to send out anything in the project.
76
+ - [Web search and page fetching](usage.md#web-search) and [MCP tools](mcp.md) run on your machine,
77
+ not in the container. They still need their own approval (MCP) or can be turned off (`--no-web`).
78
+ - A container is not a virtual machine: it shares your machine's kernel, so a kernel vulnerability
79
+ could let a process escape. For untrusted code you can't afford to run, use a separate VM.
@@ -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
+ | `--sandbox` | Run shell commands in a container that only sees the project ([Sandbox](sandbox.md)) |
28
29
  | `--show-thinking` | Print the model's reasoning as it streams |
29
30
  | `-V, --version` | Print the version |
30
31
 
@@ -55,6 +56,7 @@ reasoning is on.
55
56
  | `/undo` | Undo the file changes of the last request ([details](#undo-and-checkpoints)) |
56
57
  | `/rewind [N]` | Go back to before request N: its files, and optionally the conversation |
57
58
  | `/checkpoints` | List the requests that changed files, and which files |
59
+ | `/sandbox [network on\|off]` | The [sandbox](sandbox.md) for shell commands |
58
60
  | `/mcp [tools\|login\|restart NAME]` | [MCP servers](mcp.md), their status and tools |
59
61
  | `/compact [focus]` | Summarize the conversation to free context |
60
62
  | `/context [size]` | Show how full the context window is and change its size: pick from a list with memory estimates, or give a size like `/context 128k` (`/ctx` is a shortcut) |
@@ -84,6 +86,34 @@ reasoning is on.
84
86
  lcode refuses to edit a file the model hasn't read in the session, or one that changed on disk since
85
87
  it was read, so the model always edits the current version.
86
88
 
89
+ ## Images
90
+
91
+ lcode can look at screenshots, mockups, diagrams and photos:
92
+
93
+ ```text
94
+ ❯ the layout breaks on mobile, see @screenshots/mobile.png
95
+ ❯ make the settings page match @design/settings.png
96
+ ```
97
+
98
+ - Attach an image with `@path`, like a file. lcode describes it in detail, with all visible text
99
+ transcribed and with your question in mind, and the model works from that description.
100
+ - The model can open images itself with the `view_image` tool, for example a screenshot a test wrote.
101
+ - Screenshots that [MCP](mcp.md) tools return, such as the Playwright browser's, are described too.
102
+
103
+ **Which model looks.** lcode's text-only model variants leave out the vision part to save GPU
104
+ memory, so lcode borrows the original model, which `lcode setup` already downloaded (for example
105
+ `qwen3.6:35b-a3b-coding` for `qwen3.6-35b`). If your model can see, it looks itself. `lcode doctor`
106
+ shows which one is used. With another model, install one that can see and point lcode to it:
107
+
108
+ ```bash
109
+ ollama pull qwen3-vl:8b
110
+ lcode config set vision_model qwen3-vl:8b
111
+ ```
112
+
113
+ Loading a second model takes a moment: on a 12 GB GPU, describing an image with the original
114
+ `qwen3.6:35b-a3b-coding` takes about a minute, and the next request reloads the main model. Turn
115
+ image support off with `lcode config set vision_model off`.
116
+
87
117
  ## Permissions
88
118
 
89
119
  | Mode | File edits | Shell commands |
@@ -101,8 +131,12 @@ or ++n++. Text after ++n++ goes to the model as instructions: `n run the tests w
101
131
 
102
132
  !!! warning "yolo mode"
103
133
 
104
- In `yolo` mode the model can run any command as your user. Use it in a container, a VM or a
105
- throwaway clone, not on a machine with data you care about.
134
+ In `yolo` mode the model can run any command as your user. Use it with the
135
+ [sandbox](sandbox.md), in a VM or in a throwaway clone, not on a machine with data you care
136
+ about.
137
+
138
+ With the [sandbox](sandbox.md) on, `auto-edit` mode runs shell commands without asking too: they can
139
+ only reach the project, and `/undo` can take their changes back.
106
140
 
107
141
  ## Undo and checkpoints
108
142
 
@@ -95,6 +95,7 @@ nav:
95
95
  - Models & context windows: models.md
96
96
  - Benchmarking models: bench.md
97
97
  - MCP servers: mcp.md
98
+ - Sandbox: sandbox.md
98
99
  - Configuration: configuration.md
99
100
  - Reference:
100
101
  - How it works: how-it-works.md
@@ -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.4.1"
3
+ __version__ = "0.6.0"
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import base64
5
6
  import datetime as dt
6
7
  import json
7
8
  import platform
@@ -19,18 +20,21 @@ from rich.markup import escape
19
20
  from rich.panel import Panel
20
21
  from rich.text import Text
21
22
 
22
- from lcode import catalog, limits, sessions, web
23
+ from lcode import catalog, limits, sessions, vision, web
23
24
  from lcode.checkpoints import Checkpoints
24
25
  from lcode.config import format_tokens
25
26
  from lcode.mcp import McpManager
26
27
  from lcode.ollama import Ollama, OllamaError
27
28
  from lcode.permissions import Permissions
28
29
  from lcode.render import MarkdownStreamer
30
+ from lcode.sandbox import Sandbox, SandboxError, project_root
29
31
  from lcode.tools import (
30
32
  SCHEMAS,
33
+ VIEW_IMAGE_SCHEMA,
31
34
  WEB_FETCH_SCHEMA,
32
35
  WEB_SEARCH_SCHEMA,
33
36
  Toolbox,
37
+ ToolError,
34
38
  is_binary,
35
39
  parse_text_tool_calls,
36
40
  tree,
@@ -140,6 +144,10 @@ class Settings:
140
144
  search_backend: str = "auto"
141
145
  searxng_url: str | None = None
142
146
  checkpoints: bool = True # snapshot files before the model changes them, for /undo
147
+ sandbox: str = "off" # off, docker or podman: where the model's shell commands run
148
+ sandbox_image: str | None = None
149
+ sandbox_network: bool = False
150
+ vision_model: str = "auto" # auto, off or an Ollama model that can see images
143
151
 
144
152
 
145
153
  class Agent:
@@ -153,7 +161,13 @@ class Agent:
153
161
  self.session_id = self.new_session_id()
154
162
  self.checkpoints = Checkpoints(self.console, settings.checkpoints)
155
163
  self.mcp: McpManager | None = None # set by the CLI when MCP servers are configured
164
+ self.sandbox = (
165
+ Sandbox(settings.sandbox, settings.sandbox_image, settings.sandbox_network, self.console)
166
+ if settings.sandbox != "off"
167
+ else None
168
+ )
156
169
  self._mcp_prompt = "" # the MCP part at the end of the system prompt
170
+ self._vision: str | bool | None = False # the model that looks at images; False = not decided yet
157
171
  self.session_name = ""
158
172
  self.session_title = ""
159
173
  self.ctx_used = 0
@@ -195,10 +209,65 @@ class Agent:
195
209
  schemas = list(SCHEMAS)
196
210
  if self.settings.web != "off":
197
211
  schemas += [*([WEB_SEARCH_SCHEMA] if self.search_backend() else []), WEB_FETCH_SCHEMA]
212
+ if self.vision_model():
213
+ schemas.append(VIEW_IMAGE_SCHEMA)
198
214
  if self.mcp:
199
215
  schemas += self.mcp.schemas(self.settings.context)
200
216
  return schemas
201
217
 
218
+ def vision_model(self) -> str | None:
219
+ """The model that looks at images for this session, if any (see lcode.vision)."""
220
+ if self._vision is False:
221
+ self._vision = vision.pick_model(self.ollama, self.settings.model, self.settings.vision_model)
222
+ return self._vision or None
223
+
224
+ def free_gpu(self) -> list[str]:
225
+ """Unload lcode's models from Ollama so another program can use the GPU; the next request reloads.
226
+
227
+ Only lcode's own models: the session's and the one that looks at images.
228
+ """
229
+ mine = {self.settings.model, *([self._vision] if isinstance(self._vision, str) else [])}
230
+ freed = []
231
+ for entry in self.ollama.running():
232
+ name = entry.get("name") or entry.get("model") or ""
233
+ if name in mine or name.removesuffix(":latest") in mine:
234
+ try:
235
+ self.ollama.unload(name)
236
+ freed.append(name.removesuffix(":latest"))
237
+ except OllamaError:
238
+ pass
239
+ return freed
240
+
241
+ def look(self, path: Path, question: str = "") -> str:
242
+ """Describe an image with the vision model. Raises ToolError if it can't."""
243
+ model = self.vision_model()
244
+ if not model:
245
+ raise ToolError(f"can't look at {path.name}: {vision.INSTALL_HINT}")
246
+ try:
247
+ image = vision.read_image(path)
248
+ # The session's own model keeps its settings, so Ollama doesn't reload it.
249
+ options = self.options() if model == self.settings.model else None
250
+ loading = "" if model == self.settings.model else " (loads it; the next request reloads the main model)"
251
+ with self.console.status(f"Looking at {path.name} with {model}{loading}…", spinner="dots"):
252
+ return vision.describe(self.ollama, model, image, question, options, self.settings.keep_alive)
253
+ except vision.VisionError as e:
254
+ raise ToolError(str(e)) from e
255
+
256
+ def describe_image_data(self, data: str, mime: str) -> str:
257
+ """For images that tools return (e.g. an MCP browser's screenshots): a description, or a note."""
258
+ model = self.vision_model()
259
+ if not model:
260
+ return f"[{mime} image not shown: {vision.INSTALL_HINT}]"
261
+ options = self.options() if model == self.settings.model else None
262
+ try:
263
+ with self.console.status(f"Looking at the {mime} image with {model}…", spinner="dots"):
264
+ text = vision.describe(
265
+ self.ollama, model, base64.b64decode(data), "", options, self.settings.keep_alive
266
+ )
267
+ except (vision.VisionError, ValueError) as e:
268
+ return f"[{mime} image not shown: {e}]"
269
+ return f"[{mime} image, as described by {model}]\n{text}"
270
+
202
271
  def prepare_mcp(self) -> None:
203
272
  """Before a request: wait for MCP servers still starting and describe them in the system prompt."""
204
273
  if not self.mcp:
@@ -462,6 +531,15 @@ class Agent:
462
531
  finally:
463
532
  self.checkpoints.end_turn()
464
533
 
534
+ def sandbox_root(self) -> Path | None:
535
+ """The folder the model is limited to while the sandbox is on (None when it's off)."""
536
+ if not self.sandbox:
537
+ return None
538
+ try:
539
+ return project_root(self.cwd)
540
+ except SandboxError:
541
+ return self.cwd
542
+
465
543
  def checkpoint(self) -> None:
466
544
  """Called before the model changes files: snapshot them once per request, for /undo."""
467
545
  self.checkpoints.before_change(self.cwd)
@@ -541,7 +619,17 @@ class Agent:
541
619
  attached = []
542
620
  for ref in re.findall(r"(?<!\S)@([\w./~\-]+)", text):
543
621
  p = self.tools.resolve(ref)
544
- if p.is_file() and not is_binary(p) and p.stat().st_size < 200_000:
622
+ if p.is_file() and vision.is_image(p):
623
+ try:
624
+ description = self.look(p, re.sub(r"(?<!\S)@[\w./~\-]+", "", text))
625
+ who = f' described_by="{self.vision_model()}"'
626
+ except ToolError as e:
627
+ description, who = f"(lcode couldn't look at this image: {e})", ""
628
+ self.console.print(Text(f" ⎿ {e}", style="yellow"))
629
+ else:
630
+ self.console.print(Text(f" ⎿ looked at {self.tools.rel(p)}", style="dim"))
631
+ attached.append(f'<image path="{self.tools.rel(p)}"{who}>\n{description}\n</image>')
632
+ elif p.is_file() and not is_binary(p) and p.stat().st_size < 200_000:
545
633
  attached.append(f'<file path="{self.tools.rel(p)}">\n{p.read_text(errors="replace")}\n</file>')
546
634
  self.tools.read_mtimes[str(p)] = p.stat().st_mtime
547
635
  self.console.print(Text(f" ⎿ attached {self.tools.rel(p)}", style="dim"))
@@ -273,6 +273,17 @@ def cmd_doctor(args) -> None:
273
273
  if spec:
274
274
  detail += f" · ~{spec.memory_gib(ctx):.0f} GB needed, ~{hw.budget_gib:.0f} GB available"
275
275
  line("Context", detail + (f" ({note})" if note else ""), None if note else True)
276
+ from lcode import vision
277
+
278
+ seer = vision.pick_model(ollama, model, cfg["vision_model"])
279
+ if seer:
280
+ line(
281
+ "Vision",
282
+ f"{seer} looks at screenshots and images" + ("" if seer == model else " (loaded when needed)"),
283
+ True,
284
+ )
285
+ else:
286
+ line("Vision", "off" if cfg["vision_model"] == "off" else vision.INSTALL_HINT, None)
276
287
  if limits.get(model):
277
288
  line(
278
289
  "Limit",
@@ -321,6 +332,15 @@ def cmd_doctor(args) -> None:
321
332
  except ValueError as e:
322
333
  line("MCP", str(e), False)
323
334
  ok = False
335
+ if cfg["sandbox"] == "off":
336
+ line("Sandbox", "off (lcode config set sandbox docker)", True)
337
+ else:
338
+ from lcode.sandbox import engine_problem
339
+
340
+ problem = engine_problem(cfg["sandbox"])
341
+ network = "network on" if cfg["sandbox_network"] else "no network"
342
+ line("Sandbox", problem or f"{cfg['sandbox']} · {network}", problem is None)
343
+ ok &= problem is None
324
344
  node = shutil.which("npx")
325
345
  line("Node.js", "found" if node else "not found (some MCP servers need npx)", True if node else None)
326
346
  sys.exit(0 if ok else 1)
@@ -485,8 +505,21 @@ def cmd_chat(args) -> None:
485
505
  search_backend=cfg["search_backend"],
486
506
  searxng_url=cfg["searxng_url"],
487
507
  checkpoints=cfg["checkpoints"],
508
+ sandbox=(cfg["sandbox"] if cfg["sandbox"] != "off" else "docker") if args.sandbox else cfg["sandbox"],
509
+ sandbox_image=cfg["sandbox_image"],
510
+ sandbox_network=cfg["sandbox_network"],
511
+ vision_model=cfg["vision_model"],
488
512
  )
489
513
  agent = Agent(ollama, settings, cwd, console=console)
514
+ if agent.sandbox:
515
+ # Never run commands unsandboxed when the user asked for a sandbox: stop here instead.
516
+ from lcode.sandbox import SandboxError
517
+
518
+ try:
519
+ with console.status("Starting the sandbox…"):
520
+ agent.sandbox.ensure(cwd)
521
+ except SandboxError as e:
522
+ fail(f"the sandbox can't start: {e}\nTurn it off with: lcode config set sandbox off")
490
523
  if not args.no_mcp:
491
524
  from lcode.mcp.commands import start_session
492
525
 
@@ -496,6 +529,8 @@ def cmd_chat(args) -> None:
496
529
  finally:
497
530
  if agent.mcp:
498
531
  agent.mcp.close()
532
+ if agent.sandbox:
533
+ agent.sandbox.stop()
499
534
 
500
535
 
501
536
  def build_parser() -> argparse.ArgumentParser:
@@ -522,6 +557,7 @@ def build_parser() -> argparse.ArgumentParser:
522
557
  parser.add_argument("--no-think", action="store_true", help="disable model reasoning (faster, less accurate)")
523
558
  parser.add_argument("--no-web", action="store_true", help="don't let the model search or fetch web pages")
524
559
  parser.add_argument("--no-mcp", action="store_true", help="don't start MCP servers in this session")
560
+ parser.add_argument("--sandbox", action="store_true", help="run shell commands in a container (Docker or Podman)")
525
561
  parser.add_argument("--show-thinking", action="store_true", help="print the model's reasoning as it streams")
526
562
  parser.add_argument("-V", "--version", action="version", version=f"lcode {__version__}")
527
563
  return parser