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.
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/CHANGELOG.md +32 -1
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/PKG-INFO +7 -3
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/README.md +6 -2
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/SECURITY.md +7 -2
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/docs/configuration.md +5 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/docs/how-it-works.md +2 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/docs/mcp.md +53 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/docs/models.md +4 -2
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/docs/quickstart.md +1 -1
- lcode_cli-0.6.0/docs/sandbox.md +79 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/docs/usage.md +36 -2
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/mkdocs.yml +1 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/src/lcode/__init__.py +1 -1
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/src/lcode/agent.py +90 -2
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/src/lcode/cli.py +36 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/src/lcode/config.py +11 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/src/lcode/mcp/catalog.toml +28 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/src/lcode/mcp/config.py +5 -1
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/src/lcode/mcp/manager.py +8 -3
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/src/lcode/mcp/protocol.py +5 -2
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/src/lcode/ollama.py +4 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/src/lcode/repl.py +33 -0
- lcode_cli-0.6.0/src/lcode/sandbox.py +204 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/src/lcode/tools.py +86 -28
- lcode_cli-0.6.0/src/lcode/vision.py +109 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/tests/conftest.py +14 -2
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/tests/test_mcp.py +18 -0
- lcode_cli-0.6.0/tests/test_sandbox.py +166 -0
- lcode_cli-0.6.0/tests/test_vision.py +104 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/.editorconfig +0 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/.github/CODEOWNERS +0 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/.github/ISSUE_TEMPLATE/model_request.yml +0 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/.github/dependabot.yml +0 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/.github/workflows/ci.yml +0 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/.github/workflows/docs.yml +0 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/.github/workflows/release.yml +0 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/.gitignore +0 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/.pre-commit-config.yaml +0 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/AGENTS.md +0 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/CODE_OF_CONDUCT.md +0 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/CONTRIBUTING.md +0 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/LICENSE +0 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/docs/assets/demo.svg +0 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/docs/assets/extra.css +0 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/docs/assets/logo.svg +0 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/docs/assets/social-preview.png +0 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/docs/bench.md +0 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/docs/changelog.md +0 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/docs/contributing.md +0 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/docs/index.md +0 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/docs/installation.md +0 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/docs/troubleshooting.md +0 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/install.sh +0 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/overrides/main.html +0 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/pyproject.toml +0 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/scripts/record_demo.py +0 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/scripts/social-preview.html +0 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/src/lcode/__main__.py +0 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/src/lcode/bench.py +0 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/src/lcode/catalog.py +0 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/src/lcode/checkpoints.py +0 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/src/lcode/hardware.py +0 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/src/lcode/limits.py +0 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/src/lcode/mcp/__init__.py +0 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/src/lcode/mcp/auth.py +0 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/src/lcode/mcp/catalog.py +0 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/src/lcode/mcp/client.py +0 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/src/lcode/mcp/commands.py +0 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/src/lcode/mcp/http.py +0 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/src/lcode/mcp/stdio.py +0 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/src/lcode/models.toml +0 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/src/lcode/permissions.py +0 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/src/lcode/render.py +0 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/src/lcode/sessions.py +0 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/src/lcode/web.py +0 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/tests/fake_mcp_server.py +0 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/tests/test_agent.py +0 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/tests/test_bench.py +0 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/tests/test_catalog.py +0 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/tests/test_checkpoints.py +0 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/tests/test_cli.py +0 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/tests/test_config.py +0 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/tests/test_context_picker.py +0 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/tests/test_permissions.py +0 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/tests/test_sessions.py +0 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/tests/test_tools.py +0 -0
- {lcode_cli-0.4.1 → lcode_cli-0.6.0}/tests/test_web.py +0 -0
- {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.
|
|
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.
|
|
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
|
|
29
|
-
environments (a
|
|
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
|
|
229
|
-
|
|
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)).
|
|
@@ -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
|
|
105
|
-
throwaway clone, not on a machine with data you care
|
|
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
|
|
|
@@ -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
|
|
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
|