lcode-cli 0.39.0__tar.gz → 0.40.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 (188) hide show
  1. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/CHANGELOG.md +30 -1
  2. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/PKG-INFO +4 -1
  3. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/README.md +3 -0
  4. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/docs/configuration.md +2 -0
  5. lcode_cli-0.40.0/docs/modes.md +109 -0
  6. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/docs/quickstart.md +1 -1
  7. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/__init__.py +1 -1
  8. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/agent.py +4 -0
  9. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/api.py +40 -11
  10. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/cli.py +82 -9
  11. lcode_cli-0.40.0/src/lcode/comfy.py +658 -0
  12. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/config.py +2 -0
  13. lcode_cli-0.40.0/src/lcode/media.py +262 -0
  14. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/modes.py +35 -13
  15. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/repl.py +66 -16
  16. lcode_cli-0.40.0/src/lcode/workflows/__init__.py +0 -0
  17. lcode_cli-0.40.0/src/lcode/workflows/flux2-klein-4b-t2i.json +38 -0
  18. lcode_cli-0.40.0/tests/comfy_standin.py +231 -0
  19. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/tests/conftest.py +12 -1
  20. lcode_cli-0.40.0/tests/test_comfy.py +499 -0
  21. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/tests/test_modes.py +20 -9
  22. lcode_cli-0.39.0/docs/modes.md +0 -58
  23. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/.editorconfig +0 -0
  24. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/.github/CODEOWNERS +0 -0
  25. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  26. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  27. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  28. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/.github/ISSUE_TEMPLATE/model_request.yml +0 -0
  29. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  30. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/.github/dependabot.yml +0 -0
  31. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/.github/workflows/ci.yml +0 -0
  32. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/.github/workflows/docs.yml +0 -0
  33. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/.github/workflows/release.yml +0 -0
  34. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/.gitignore +0 -0
  35. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/.pre-commit-config.yaml +0 -0
  36. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/AGENTS.md +0 -0
  37. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/CODE_OF_CONDUCT.md +0 -0
  38. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/CONTRIBUTING.md +0 -0
  39. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/LICENSE +0 -0
  40. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/SECURITY.md +0 -0
  41. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/action.yml +0 -0
  42. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/docs/agents.md +0 -0
  43. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/docs/assets/demo.svg +0 -0
  44. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/docs/assets/extra.css +0 -0
  45. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/docs/automation.md +0 -0
  46. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/docs/bench.md +0 -0
  47. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/docs/changelog.md +0 -0
  48. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/docs/commands.md +0 -0
  49. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/docs/contributing.md +0 -0
  50. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/docs/editors.md +0 -0
  51. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/docs/git.md +0 -0
  52. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/docs/hooks.md +0 -0
  53. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/docs/how-it-works.md +0 -0
  54. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/docs/index.md +0 -0
  55. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/docs/installation.md +0 -0
  56. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/docs/lsp.md +0 -0
  57. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/docs/mcp.md +0 -0
  58. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/docs/memory.md +0 -0
  59. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/docs/models.md +0 -0
  60. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/docs/sandbox.md +0 -0
  61. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/docs/search.md +0 -0
  62. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/docs/servers.md +0 -0
  63. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/docs/troubleshooting.md +0 -0
  64. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/docs/usage.md +0 -0
  65. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/examples/README.md +0 -0
  66. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/examples/api/fix_tests.py +0 -0
  67. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/examples/commands/review.md +0 -0
  68. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/examples/skills/commit-message/SKILL.md +0 -0
  69. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/examples/skills/commit-message/scripts/staged.sh +0 -0
  70. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/install.sh +0 -0
  71. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/mkdocs.yml +0 -0
  72. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/overrides/main.html +0 -0
  73. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/pyproject.toml +0 -0
  74. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/scripts/make_logos.py +0 -0
  75. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/scripts/record_demo.py +0 -0
  76. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/scripts/social-preview.html +0 -0
  77. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/__main__.py +0 -0
  78. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/acp.py +0 -0
  79. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/action.py +0 -0
  80. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/backends.py +0 -0
  81. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/bench.py +0 -0
  82. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/bench_backend.py +0 -0
  83. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/bench_runtime.py +0 -0
  84. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/catalog.py +0 -0
  85. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/checkpoints.py +0 -0
  86. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/checks.py +0 -0
  87. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/clipboard.py +0 -0
  88. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/cloud.py +0 -0
  89. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/codesearch.py +0 -0
  90. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/context.py +0 -0
  91. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/escalate.py +0 -0
  92. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/extensions.py +0 -0
  93. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/frontmatter.py +0 -0
  94. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/gitflow.py +0 -0
  95. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/hardware.py +0 -0
  96. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/hooks.py +0 -0
  97. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/jobs.py +0 -0
  98. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/limits.py +0 -0
  99. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/lsp.py +0 -0
  100. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/matching.py +0 -0
  101. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/mcp/__init__.py +0 -0
  102. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/mcp/auth.py +0 -0
  103. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/mcp/catalog.py +0 -0
  104. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/mcp/catalog.toml +0 -0
  105. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/mcp/client.py +0 -0
  106. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/mcp/commands.py +0 -0
  107. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/mcp/config.py +0 -0
  108. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/mcp/http.py +0 -0
  109. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/mcp/manager.py +0 -0
  110. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/mcp/protocol.py +0 -0
  111. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/mcp/server.py +0 -0
  112. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/mcp/stdio.py +0 -0
  113. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/memory.py +0 -0
  114. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/models.toml +0 -0
  115. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/notify.py +0 -0
  116. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/ollama.py +0 -0
  117. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/permissions.py +0 -0
  118. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/planning.py +0 -0
  119. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/render.py +0 -0
  120. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/repeats.py +0 -0
  121. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/repomap.py +0 -0
  122. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/residency.py +0 -0
  123. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/review.py +0 -0
  124. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/sandbox.py +0 -0
  125. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/screenshots.py +0 -0
  126. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/secretscan.py +0 -0
  127. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/servers/__init__.py +0 -0
  128. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/servers/encord.py +0 -0
  129. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/servers/valohai.py +0 -0
  130. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/servers/xai.py +0 -0
  131. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/sessions.py +0 -0
  132. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/steer.py +0 -0
  133. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/subagents.py +0 -0
  134. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/tools.py +0 -0
  135. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/unreachable.py +0 -0
  136. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/update.py +0 -0
  137. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/vision.py +0 -0
  138. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/src/lcode/web.py +0 -0
  139. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/tests/fake_lsp_server.py +0 -0
  140. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/tests/fake_mcp_server.py +0 -0
  141. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/tests/test_acp.py +0 -0
  142. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/tests/test_action.py +0 -0
  143. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/tests/test_agent.py +0 -0
  144. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/tests/test_api.py +0 -0
  145. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/tests/test_backends.py +0 -0
  146. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/tests/test_bench.py +0 -0
  147. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/tests/test_catalog.py +0 -0
  148. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/tests/test_checkpoints.py +0 -0
  149. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/tests/test_checks.py +0 -0
  150. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/tests/test_cli.py +0 -0
  151. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/tests/test_clipboard.py +0 -0
  152. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/tests/test_cloud.py +0 -0
  153. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/tests/test_codesearch.py +0 -0
  154. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/tests/test_config.py +0 -0
  155. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/tests/test_context.py +0 -0
  156. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/tests/test_context_picker.py +0 -0
  157. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/tests/test_escalate.py +0 -0
  158. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/tests/test_extensions.py +0 -0
  159. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/tests/test_gitflow.py +0 -0
  160. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/tests/test_hooks.py +0 -0
  161. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/tests/test_jobs.py +0 -0
  162. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/tests/test_look.py +0 -0
  163. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/tests/test_lsp.py +0 -0
  164. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/tests/test_matching.py +0 -0
  165. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/tests/test_mcp.py +0 -0
  166. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/tests/test_memory.py +0 -0
  167. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/tests/test_model_picker.py +0 -0
  168. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/tests/test_permissions.py +0 -0
  169. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/tests/test_plan_mode.py +0 -0
  170. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/tests/test_repeats.py +0 -0
  171. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/tests/test_repomap.py +0 -0
  172. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/tests/test_residency.py +0 -0
  173. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/tests/test_review.py +0 -0
  174. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/tests/test_robustness.py +0 -0
  175. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/tests/test_sandbox.py +0 -0
  176. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/tests/test_screenshots.py +0 -0
  177. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/tests/test_secretscan.py +0 -0
  178. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/tests/test_servers.py +0 -0
  179. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/tests/test_sessions.py +0 -0
  180. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/tests/test_steer.py +0 -0
  181. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/tests/test_stuck.py +0 -0
  182. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/tests/test_subagents.py +0 -0
  183. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/tests/test_tools.py +0 -0
  184. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/tests/test_update.py +0 -0
  185. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/tests/test_vision.py +0 -0
  186. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/tests/test_web.py +0 -0
  187. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/tests/test_xai.py +0 -0
  188. {lcode_cli-0.39.0 → lcode_cli-0.40.0}/uv.lock +0 -0
@@ -6,6 +6,34 @@ All notable changes to lcode are documented here. The format follows
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.40.0] - 2026-10-23
10
+
11
+ ### Added
12
+
13
+ - **Image mode makes images** (#149). In image mode, what you type becomes a picture, made by
14
+ FLUX.2 klein 4B on your GPU in about 8 seconds, with no language model involved. lcode drives
15
+ ComfyUI itself. See [Modes](https://nasser1941.github.io/lcode/modes/#image-mode).
16
+ - **ComfyUI is found or started:** lcode uses a running ComfyUI as it is. Otherwise it starts
17
+ yours in the background when you enter image mode (`comfyui_path`, the `comfy` CLI's folder or
18
+ a usual one). It stops that ComfyUI when you quit, go back to code mode or close the terminal.
19
+ With none, it offers to install ComfyUI with comfy-cli.
20
+ - **Progress, cancel and type-ahead:** the status line shows the step, the time left and your
21
+ place in ComfyUI's queue. Esc or Ctrl+C cancels the job in ComfyUI too. What you type
22
+ meanwhile waits in the next prompt.
23
+ - **Files with their settings:** images go to `.lcode/media/` (the `media_dir` setting), each
24
+ with a JSON file of its prompt, model, seed and size, also stored in the PNG itself. Images
25
+ that finish after a crash are saved the next time.
26
+ - `lcode --mode image -p "…"` makes an image from a script, and `--output json` gives the paths.
27
+ `lcode doctor` reports ComfyUI and the image model.
28
+ - **No Ollama needed for images:** lcode starts in image mode without Ollama or the coding
29
+ model; `/mode code` says what's missing.
30
+
31
+ ### Fixed
32
+
33
+ - **lcode cleans up when its terminal closes.** A closed terminal or a `kill` used to end lcode
34
+ without stopping what it had started. Now it saves the session and stops its background jobs and
35
+ servers. On Linux, a ComfyUI lcode started also stops if lcode is killed outright.
36
+
9
37
  ## [0.39.0] - 2026-10-23
10
38
 
11
39
  ### Added
@@ -876,7 +904,8 @@ First public release.
876
904
  - `AGENTS.md` project instructions and `/init` to generate them.
877
905
  - One-line installer for Linux and macOS.
878
906
 
879
- [Unreleased]: https://github.com/nasser1941/lcode/compare/v0.39.0...HEAD
907
+ [Unreleased]: https://github.com/nasser1941/lcode/compare/v0.40.0...HEAD
908
+ [0.40.0]: https://github.com/nasser1941/lcode/compare/v0.39.0...v0.40.0
880
909
  [0.39.0]: https://github.com/nasser1941/lcode/compare/v0.38.0...v0.39.0
881
910
  [0.38.0]: https://github.com/nasser1941/lcode/compare/v0.37.0...v0.38.0
882
911
  [0.37.0]: https://github.com/nasser1941/lcode/compare/v0.36.0...v0.37.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: lcode-cli
3
- Version: 0.39.0
3
+ Version: 0.40.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/
@@ -107,6 +107,9 @@ through [Ollama](https://ollama.com), and when it needs current information it c
107
107
  `lcode --worktree` runs parallel sessions on one repository.
108
108
  - **In your editor.** Zed, JetBrains IDEs, Neovim and other editors that speak the Agent Client
109
109
  Protocol run lcode with `lcode acp`, with diffs and approvals in the editor.
110
+ - **Image mode.** `/mode image`, describe a picture, and FLUX.2 klein 4B makes it on your GPU in
111
+ seconds through ComfyUI, which lcode starts, follows and stops itself. Video mode is next
112
+ ([modes](https://nasser1941.github.io/lcode/modes/)).
110
113
  - **Automation.** `lcode -p --output json` for scripts and CI, a Python API (`from lcode import
111
114
  Session`), and a GitHub Action that answers `@lcode` in issues and pull requests on your own
112
115
  self-hosted runner.
@@ -71,6 +71,9 @@ through [Ollama](https://ollama.com), and when it needs current information it c
71
71
  `lcode --worktree` runs parallel sessions on one repository.
72
72
  - **In your editor.** Zed, JetBrains IDEs, Neovim and other editors that speak the Agent Client
73
73
  Protocol run lcode with `lcode acp`, with diffs and approvals in the editor.
74
+ - **Image mode.** `/mode image`, describe a picture, and FLUX.2 klein 4B makes it on your GPU in
75
+ seconds through ComfyUI, which lcode starts, follows and stops itself. Video mode is next
76
+ ([modes](https://nasser1941.github.io/lcode/modes/)).
74
77
  - **Automation.** `lcode -p --output json` for scripts and CI, a Python API (`from lcode import
75
78
  Session`), and a GitHub Action that answers `@lcode` in issues and pull requests on your own
76
79
  self-hosted runner.
@@ -25,6 +25,8 @@ lcode config path # print the file location
25
25
  | `permission_mode` | `ask` | `ask`, `plan`, `auto-edit` or `yolo` |
26
26
  | `mode` | `code` | What lcode makes when it starts: `code`, `image` or `video` ([modes](modes.md)) |
27
27
  | `comfyui_url` | `http://127.0.0.1:8188` | ComfyUI's address, for image and video mode |
28
+ | `comfyui_path` | (found) | Your ComfyUI folder, if lcode doesn't find it; lcode starts it when needed ([ComfyUI](modes.md#comfyui)) |
29
+ | `media_dir` | `.lcode/media` | Where images and videos go: a folder in the project, or an absolute path |
28
30
  | `think` | `true` | Let the model reason before answering |
29
31
  | `web` | `on` | Web search and page fetching: `on`, `ask` (before each search/website) or `off` |
30
32
  | `search_backend` | `auto` | `auto`, `ollama`, `brave`, `tavily` or `searxng`; `auto` uses the first one configured |
@@ -0,0 +1,109 @@
1
+ # Modes: code, image and video
2
+
3
+ lcode makes three kinds of things, each in its own mode with its own models:
4
+
5
+ | Mode | What it makes | The model doing the work |
6
+ |---|---|---|
7
+ | **code** (default) | Code: it reads, writes, runs and checks your project | A coding language model in Ollama (or another server) |
8
+ | **image** | Images from a description and reference images | An image model, run by ComfyUI on your GPU |
9
+ | **video** | Videos from a description, a start image or references | A video model, run by ComfyUI on your GPU |
10
+
11
+ ```text
12
+ ❯ /mode image
13
+ Mode: image (local image generation).
14
+ Unloaded qwen3.6-35b to free the GPU for images.
15
+ Starting ComfyUI (~/comfy/ComfyUI) in the background.
16
+ Describe an image and press Enter: FLUX.2 klein 4B makes it on your GPU, into .lcode/media.
17
+ image ❯ a red fox sitting in fresh snow at dusk
18
+ ◆ .lcode/media/20261023-184531-a-red-fox-sitting-in-fresh-snow-at-dusk-1.png 1024×1024 · FLUX.2 klein 4B · seed 3714779554 · 8.0 s
19
+ ```
20
+
21
+ - **Switching:** `/mode code`, `/mode image` and `/mode video` switch in a session. `lcode --mode image`
22
+ starts in one, and `lcode config set mode video` makes it the default. `/mode` alone shows
23
+ the current one.
24
+ - **Where it shows:** the banner, the status bar and the prompt (`image ❯`) show the mode.
25
+ - **Sessions:** a session keeps the mode it was made in, and resuming it brings that mode back.
26
+
27
+ ## Code mode makes code only
28
+
29
+ Code mode is the coding agent, as before, and it no longer generates images or videos. The MCP
30
+ servers that did (`comfyui` and `xai`) are gone from the MCP catalog, and lcode doesn't load them
31
+ in code mode even if they're still in your `mcp.json`. It says so once.
32
+
33
+ ## Image mode
34
+
35
+ In image mode, what you type is a description, and lcode makes the picture. No language model is
36
+ involved: lcode fills in its own ComfyUI workflow for the image model and follows the job itself.
37
+ For now that's one image per request, 1024×1024, with **FLUX.2 klein 4B** (4 steps, about 8 seconds
38
+ on a 12 GB laptop GPU; a 4B model that fits in 8 GB). Choosing models, sizes, seeds and variations
39
+ comes next (#150, #151).
40
+
41
+ - **Progress:** the status line says what ComfyUI is doing (loading the model, reading the
42
+ prompt, drawing, decoding), the step, the time left and the time so far. If other jobs are ahead
43
+ in ComfyUI's queue, it says how many.
44
+ - **Cancel** with Esc or Ctrl+C: lcode cancels the job in ComfyUI too, so it stops using the GPU.
45
+ - **Type ahead:** what you type while an image is being made waits in the next prompt.
46
+ - **Files:** each image goes to `.lcode/media/` in the project (the `media_dir` setting), named
47
+ after the time and the prompt, with a `.json` next to it holding the prompt, model, seed, size and
48
+ steps. PNGs carry the same in their own metadata (an `lcode` text chunk), and ComfyUI's workflow
49
+ as well. `.lcode/media/` keeps itself out of git; a folder of your own (`lcode config set
50
+ media_dir assets/generated`) doesn't.
51
+ - **Nothing is lost:** lcode writes each job down while it runs. If lcode crashes or the terminal
52
+ closes before an image arrives, the next request in image mode saves it.
53
+ - **From scripts:** `lcode --mode image -p "a red fox in the snow"` makes one image and exits;
54
+ with `--output json` it prints a result with the file paths, model, seed, size and time.
55
+ - **No Ollama needed:** `lcode --mode image` starts even when Ollama isn't running or the coding
56
+ model isn't installed; `/mode code` then says what's missing.
57
+
58
+ ## ComfyUI
59
+
60
+ ComfyUI runs the image and video models; lcode drives it through its API.
61
+
62
+ - **One that's running** at `comfyui_url` (by default `http://127.0.0.1:8188`) is used as it is.
63
+ - **Otherwise lcode starts one** in the background when you switch to image mode, so the first
64
+ image waits less, and stops it when you quit or go back to code mode, or when the terminal
65
+ closes. It looks in the `comfyui_path` setting, then the folder the `comfy` CLI uses, then
66
+ `~/comfy/ComfyUI`, `~/ComfyUI`, `~/comfyui` and `~/Documents/ComfyUI`. It uses that install's
67
+ own Python (its `.venv` or `venv`), and logs to `comfyui.log` in lcode's state folder.
68
+ - **With none,** your first image request offers to install ComfyUI into lcode's state folder with
69
+ [comfy-cli](https://github.com/Comfy-Org/comfy-cli) and the PyTorch build for your GPU (NVIDIA,
70
+ Apple Silicon or CPU). That downloads several GB. To install it yourself:
71
+
72
+ ```bash
73
+ uvx --from comfy-cli comfy install
74
+ ```
75
+
76
+ - **The image model** has to be in ComfyUI's model folders for now: for FLUX.2 klein 4B,
77
+ `diffusion_models/flux-2-klein-4b-fp8.safetensors`, `text_encoders/qwen_3_4b.safetensors` and
78
+ `vae/flux2-vae.safetensors` (from ComfyUI's FLUX.2 klein template). lcode names any that are
79
+ missing. Installing models from lcode comes with #150.
80
+ - **`lcode doctor`** shows whether ComfyUI is running or found, and whether the image model is
81
+ there.
82
+
83
+ ## One model on the GPU at a time
84
+
85
+ Image and video modes don't need the big coding model: the image or video model does the work. A
86
+ small optional model will help with prompts later. Image and video models need the GPU to
87
+ themselves:
88
+
89
+ - **Into image or video mode:** lcode unloads its coding model from Ollama, and checks again
90
+ before each image.
91
+ - **Back to code mode:** lcode stops the ComfyUI it started, or asks yours to free its memory,
92
+ then loads the coding model again in the background.
93
+
94
+ ## Coming next
95
+
96
+ Video mode doesn't generate yet: a request there says what's coming instead of going anywhere.
97
+
98
+ - **#150 Media model catalog:** image and video models that fit your machine, installed on
99
+ demand.
100
+ - **#151, #152 Image mode:** generating, variations, sizes and seeds, a gallery, editing,
101
+ references, inpainting, upscaling.
102
+ - **#153, #154 Video mode:** from text or a start image, first and last frames, several
103
+ references, camera moves, longer clips, sound.
104
+ - **#155 Movies:** storyboards with a consistent cast, shots stitched into one film.
105
+ - **#156 Prompt assistant:** a small local model for prompts and follow-ups.
106
+ - **#157 Cloud generation**, with costs shown (Grok Imagine first).
107
+
108
+ The permission mode (`ask`, `plan`, `auto-edit`, `yolo`) is a different setting: `/permissions`
109
+ sets it, and Shift+Tab cycles it as before.
@@ -12,7 +12,7 @@ lcode
12
12
 
13
13
  ```text
14
14
  ╭──────────────────────────────────────────────╮
15
- │ lcode v0.39.0 — local coding agent │
15
+ │ lcode v0.40.0 — local coding agent │
16
16
  │ │
17
17
  │ model lcode-qwen3.6-35b │
18
18
  │ context 256K tokens │
@@ -3,7 +3,7 @@
3
3
  From a program: `from lcode import Session` (see lcode.api).
4
4
  """
5
5
 
6
- __version__ = "0.39.0"
6
+ __version__ = "0.40.0"
7
7
 
8
8
  API = ("Session", "Result", "Options", "SetupError")
9
9
 
@@ -306,6 +306,8 @@ class Settings:
306
306
  context: int
307
307
  mode: str = "code" # what lcode makes: code, image or video (lcode.modes)
308
308
  comfyui_url: str = "http://127.0.0.1:8188"
309
+ comfyui_path: str | None = None # a ComfyUI install lcode doesn't find by itself (lcode.comfy)
310
+ media_dir: str = ".lcode/media" # where image and video mode save their files (lcode.media)
309
311
  num_batch: int | None = None
310
312
  keep_alive: str = "30m"
311
313
  think: bool = True
@@ -370,6 +372,8 @@ class Agent:
370
372
  self.session_name = ""
371
373
  self.session_title = ""
372
374
  self.interactive = True # False for `lcode -p`: no end-of-session questions
375
+ self.comfy_process = None # the ComfyUI lcode started for image and video mode, stopped when it exits
376
+ self.no_coding_model = "" # why there's no coding model, when lcode started in image or video mode without
373
377
  self.reflected = 0 # messages before this index were already checked for notes to remember
374
378
  self._memory: memory_notes.Memory | None = None
375
379
  self._agent_types: tuple[Path, dict[str, subagents.AgentType], list[str]] | None = None
@@ -15,6 +15,7 @@ A session is one conversation: each `run` continues it. `stream` yields the same
15
15
 
16
16
  from __future__ import annotations
17
17
 
18
+ import contextlib
18
19
  import io
19
20
  import queue
20
21
  import threading
@@ -130,6 +131,24 @@ def switch(wanted: bool | None, configured: str, default_on: str) -> str:
130
131
  return configured if configured != "off" else default_on
131
132
 
132
133
 
134
+ def coding_model(ollama, options: Options, cfg: dict, requested: int | None, hw: Hardware, console: Console):
135
+ """The coding model to use, its catalog entry and the context window; SetupError when there's none."""
136
+ from lcode import cli
137
+
138
+ check_server(ollama, hw)
139
+ try:
140
+ model, spec = cli.resolve_model(ollama, options.model or cfg["model"])
141
+ except cli.NotInstalled as e:
142
+ first_run = not options.model and cfg["model"] == config.DEFAULT_MODEL and not config.CONFIG_PATH.exists()
143
+ if first_run and not backends.is_openai(ollama):
144
+ raise SetupError("lcode isn't set up yet. Run: lcode setup") from e
145
+ raise SetupError(str(e)) from e
146
+ context, note = cli.choose_context(ollama, model, spec, requested, hw)
147
+ if note:
148
+ console.print(f"[yellow]Context {config.format_tokens(context)}: {note}[/]")
149
+ return model, spec, context
150
+
151
+
133
152
  def open_agent(cwd: Path, options: Options, console: Console, hw: Hardware | None = None) -> Agent:
134
153
  """An agent set up the way `lcode` sets up a session: model, context, hooks, language servers, MCP."""
135
154
  from lcode import catalog, cli, extensions, hooks
@@ -153,17 +172,14 @@ def open_agent(cwd: Path, options: Options, console: Console, hw: Hardware | Non
153
172
  except OllamaError as e:
154
173
  raise SetupError(str(e)) from e
155
174
  hw = hw or detect()
156
- check_server(ollama, hw)
175
+ no_coding_model = "" # why, when image or video mode starts without one
157
176
  try:
158
- model, spec = cli.resolve_model(ollama, options.model or cfg["model"])
159
- except cli.NotInstalled as e:
160
- first_run = not options.model and cfg["model"] == config.DEFAULT_MODEL and not config.CONFIG_PATH.exists()
161
- if first_run and not backends.is_openai(ollama):
162
- raise SetupError("lcode isn't set up yet. Run: lcode setup") from e
163
- raise SetupError(str(e)) from e
164
- context, note = cli.choose_context(ollama, model, spec, requested, hw)
165
- if note:
166
- console.print(f"[yellow]Context {config.format_tokens(context)}: {note}[/]")
177
+ model, spec, context = coding_model(ollama, options, cfg, requested, hw, console)
178
+ except SetupError as e:
179
+ if (options.mode or cfg["mode"]) == "code":
180
+ raise
181
+ # Image and video mode don't need the coding model: start without it, and say so on /mode code.
182
+ model, spec, context, no_coding_model = options.model or cfg["model"], None, requested or 32768, str(e)
167
183
  num_batch = cfg["num_batch"]
168
184
  if num_batch is None and spec and spec.num_batch:
169
185
  if model == spec.local_name:
@@ -182,6 +198,8 @@ def open_agent(cwd: Path, options: Options, console: Console, hw: Hardware | Non
182
198
  permission_mode=options.permission_mode or cfg["permission_mode"],
183
199
  mode=options.mode or cfg["mode"],
184
200
  comfyui_url=cfg["comfyui_url"],
201
+ comfyui_path=cfg["comfyui_path"],
202
+ media_dir=cfg["media_dir"],
185
203
  web=switch(options.web, cfg["web"], "on"),
186
204
  search_backend=cfg["search_backend"],
187
205
  searxng_url=cfg["searxng_url"],
@@ -211,6 +229,7 @@ def open_agent(cwd: Path, options: Options, console: Console, hw: Hardware | Non
211
229
  )
212
230
  agent = Agent(ollama, settings, cwd, console=console)
213
231
  agent.hardware = hw
232
+ agent.no_coding_model = no_coding_model
214
233
  agent.autosave = True
215
234
  agent.interactive = options.interactive
216
235
  if options.max_steps:
@@ -250,15 +269,25 @@ def open_agent(cwd: Path, options: Options, console: Console, hw: Hardware | Non
250
269
 
251
270
 
252
271
  def close_agent(agent: Agent) -> None:
272
+ """Stop what lcode started (background jobs, servers, the sandbox, ComfyUI), then say so: the terminal may be
273
+ gone already (closed), and saying it then fails."""
274
+ from lcode import media
275
+
276
+ said = []
253
277
  stopped = agent.jobs.stop_all()
254
278
  if stopped:
255
- agent.console.print(f"[dim]Stopped {stopped} background job(s).[/]")
279
+ said.append(f"Stopped {stopped} background job(s).")
280
+ if media.stop_engine(agent):
281
+ said.append("Stopped ComfyUI.")
256
282
  if agent.lsp is not None:
257
283
  agent.lsp.close()
258
284
  if agent.mcp:
259
285
  agent.mcp.close()
260
286
  if agent.sandbox:
261
287
  agent.sandbox.stop()
288
+ for line in said:
289
+ with contextlib.suppress(OSError):
290
+ agent.console.print(f"[dim]{line}[/]")
262
291
 
263
292
 
264
293
  # ----------------------------------------------------------------------------- running
@@ -3,9 +3,11 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import argparse
6
+ import contextlib
6
7
  import platform
7
8
  import re
8
9
  import shutil
10
+ import signal
9
11
  import sys
10
12
  import tempfile
11
13
  import time
@@ -466,6 +468,7 @@ def cmd_doctor(args) -> None:
466
468
  except ValueError as e:
467
469
  line("MCP", str(e), False)
468
470
  ok = False
471
+ line("ComfyUI", *comfyui_status(cfg))
469
472
  if cfg["sandbox"] == "off":
470
473
  line("Sandbox", "off (lcode config set sandbox docker)", True)
471
474
  else:
@@ -733,17 +736,10 @@ def cmd_chat(args) -> None:
733
736
  print(json.dumps({"type": "result", "status": "error", "error": str(e)}), flush=True)
734
737
  sys.exit(1)
735
738
  fail(str(e))
739
+ exit_on_hangup()
736
740
  exit_code = 0
737
741
  if args.prompt and agent.settings.mode != "code":
738
- from lcode import modes
739
-
740
- message = modes.COMING[agent.settings.mode]
741
- if quiet:
742
- print(json.dumps({"type": "result", "status": "error", "error": message}), flush=True)
743
- else:
744
- out.print(f"[yellow]{escape(message)}[/]")
745
- api.close_agent(agent)
746
- sys.exit(2)
742
+ media_request(agent, args.prompt, out, quiet) # makes the image or video, then exits
747
743
  if args.escalate and args.prompt:
748
744
  agent.escalate_approved = True # the flag is the user's OK for this run
749
745
  try:
@@ -765,6 +761,83 @@ def cmd_chat(args) -> None:
765
761
  sys.exit(exit_code)
766
762
 
767
763
 
764
+ def exit_on_hangup() -> None:
765
+ """A closed terminal (SIGHUP) or a plain kill (SIGTERM) ends lcode the usual way: it saves the session and stops
766
+ what it started (ComfyUI, MCP servers, background jobs) instead of leaving them running."""
767
+
768
+ def leave(signum, frame):
769
+ raise SystemExit(128 + signum)
770
+
771
+ for name in ("SIGHUP", "SIGTERM"):
772
+ sig = getattr(signal, name, None)
773
+ if sig is not None:
774
+ with contextlib.suppress(ValueError, OSError): # only the main thread can
775
+ signal.signal(sig, leave)
776
+
777
+
778
+ def comfyui_status(cfg: dict) -> tuple[str, bool | None]:
779
+ """lcode doctor's ComfyUI line: running or found, and whether the image model is there."""
780
+ from lcode import comfy, media
781
+
782
+ workflow = comfy.load_workflow(media.IMAGE_WORKFLOW)
783
+ server = comfy.Server(cfg["comfyui_url"])
784
+ stats = server.stats()
785
+ install = None if stats else comfy.find_install(cfg["comfyui_path"])
786
+ if not stats and install is None:
787
+ return "not found: image mode offers to install it (or lcode config set comfyui_path <folder>)", None
788
+ if stats:
789
+ where = f"{(stats.get('system') or {}).get('comfyui_version', '?')} running at {server.url}"
790
+ installed = {folder: set(server.models(folder)) for folder in workflow.models}
791
+ else:
792
+ where = f"{install.path} (lcode starts it in image mode)"
793
+ installed = {folder: set(comfy.local_models(install, folder)) for folder in workflow.models}
794
+ missing = [f"{f}/{names[0]}" for f, names in workflow.models.items() if not installed[f] & set(names)]
795
+ if missing:
796
+ return f"{where} · {workflow.name} not installed (missing {', '.join(missing)})", None
797
+ return f"{where} · image model: {workflow.name}", True
798
+
799
+
800
+ def media_request(agent, prompt: str, out: Console, quiet: bool) -> None:
801
+ """`lcode --mode image -p "…"`: make the image, say where it went (or, with --output json, a result object)."""
802
+ import json
803
+
804
+ from lcode import api, media, modes
805
+
806
+ if agent.settings.mode == "video":
807
+ message = modes.COMING["video"]
808
+ if quiet:
809
+ print(json.dumps({"type": "result", "status": "error", "error": message}), flush=True)
810
+ else:
811
+ out.print(f"[yellow]{escape(message)}[/]")
812
+ api.close_agent(agent)
813
+ sys.exit(2)
814
+ try:
815
+ made = media.generate_image(agent, prompt)
816
+ except (media.MediaError, KeyboardInterrupt) as e:
817
+ message = str(e) or "Cancelled."
818
+ if quiet:
819
+ print(json.dumps({"type": "result", "status": "error", "error": message}), flush=True)
820
+ else:
821
+ out.print(f"[red]{escape(message)}[/]")
822
+ api.close_agent(agent)
823
+ sys.exit(1)
824
+ if quiet:
825
+ result = {
826
+ "type": "result",
827
+ "status": "success",
828
+ "mode": "image",
829
+ "files": [str(path) for path in made.files],
830
+ "model": made.model,
831
+ "seed": made.seed,
832
+ "width": made.width,
833
+ "height": made.height,
834
+ "seconds": round(made.seconds, 1),
835
+ }
836
+ print(json.dumps(result, ensure_ascii=False), flush=True)
837
+ api.close_agent(agent)
838
+ sys.exit(0)
839
+
840
+
768
841
  def compact_tokens(value: str) -> int:
769
842
  """`--compact-at`: a token count such as 64k, or 0."""
770
843
  try: