lcode-cli 0.40.0__tar.gz → 0.42.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 (196) hide show
  1. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/CHANGELOG.md +39 -1
  2. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/PKG-INFO +1 -1
  3. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/docs/configuration.md +2 -0
  4. lcode_cli-0.42.0/docs/modes.md +211 -0
  5. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/docs/quickstart.md +1 -1
  6. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/docs/usage.md +2 -0
  7. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/__init__.py +1 -1
  8. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/agent.py +17 -1
  9. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/api.py +2 -0
  10. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/cli.py +72 -14
  11. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/comfy.py +18 -22
  12. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/config.py +13 -0
  13. lcode_cli-0.42.0/src/lcode/imageopts.py +207 -0
  14. lcode_cli-0.42.0/src/lcode/media.py +473 -0
  15. lcode_cli-0.42.0/src/lcode/media.toml +80 -0
  16. lcode_cli-0.42.0/src/lcode/media_catalog.py +340 -0
  17. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/modes.py +1 -1
  18. lcode_cli-0.42.0/src/lcode/preview.py +152 -0
  19. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/repl.py +150 -12
  20. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/sessions.py +2 -2
  21. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/workflows/flux2-klein-4b-t2i.json +2 -9
  22. lcode_cli-0.42.0/src/lcode/workflows/z-image-turbo-t2i.json +31 -0
  23. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/tests/comfy_standin.py +15 -2
  24. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/tests/test_comfy.py +10 -11
  25. lcode_cli-0.42.0/tests/test_image_generate.py +253 -0
  26. lcode_cli-0.42.0/tests/test_media_catalog.py +312 -0
  27. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/tests/test_modes.py +2 -1
  28. lcode_cli-0.40.0/docs/modes.md +0 -109
  29. lcode_cli-0.40.0/src/lcode/media.py +0 -262
  30. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/.editorconfig +0 -0
  31. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/.github/CODEOWNERS +0 -0
  32. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  33. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  34. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  35. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/.github/ISSUE_TEMPLATE/model_request.yml +0 -0
  36. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  37. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/.github/dependabot.yml +0 -0
  38. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/.github/workflows/ci.yml +0 -0
  39. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/.github/workflows/docs.yml +0 -0
  40. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/.github/workflows/release.yml +0 -0
  41. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/.gitignore +0 -0
  42. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/.pre-commit-config.yaml +0 -0
  43. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/AGENTS.md +0 -0
  44. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/CODE_OF_CONDUCT.md +0 -0
  45. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/CONTRIBUTING.md +0 -0
  46. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/LICENSE +0 -0
  47. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/README.md +0 -0
  48. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/SECURITY.md +0 -0
  49. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/action.yml +0 -0
  50. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/docs/agents.md +0 -0
  51. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/docs/assets/demo.svg +0 -0
  52. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/docs/assets/extra.css +0 -0
  53. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/docs/automation.md +0 -0
  54. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/docs/bench.md +0 -0
  55. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/docs/changelog.md +0 -0
  56. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/docs/commands.md +0 -0
  57. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/docs/contributing.md +0 -0
  58. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/docs/editors.md +0 -0
  59. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/docs/git.md +0 -0
  60. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/docs/hooks.md +0 -0
  61. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/docs/how-it-works.md +0 -0
  62. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/docs/index.md +0 -0
  63. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/docs/installation.md +0 -0
  64. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/docs/lsp.md +0 -0
  65. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/docs/mcp.md +0 -0
  66. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/docs/memory.md +0 -0
  67. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/docs/models.md +0 -0
  68. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/docs/sandbox.md +0 -0
  69. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/docs/search.md +0 -0
  70. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/docs/servers.md +0 -0
  71. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/docs/troubleshooting.md +0 -0
  72. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/examples/README.md +0 -0
  73. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/examples/api/fix_tests.py +0 -0
  74. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/examples/commands/review.md +0 -0
  75. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/examples/skills/commit-message/SKILL.md +0 -0
  76. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/examples/skills/commit-message/scripts/staged.sh +0 -0
  77. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/install.sh +0 -0
  78. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/mkdocs.yml +0 -0
  79. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/overrides/main.html +0 -0
  80. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/pyproject.toml +0 -0
  81. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/scripts/make_logos.py +0 -0
  82. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/scripts/record_demo.py +0 -0
  83. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/scripts/social-preview.html +0 -0
  84. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/__main__.py +0 -0
  85. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/acp.py +0 -0
  86. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/action.py +0 -0
  87. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/backends.py +0 -0
  88. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/bench.py +0 -0
  89. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/bench_backend.py +0 -0
  90. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/bench_runtime.py +0 -0
  91. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/catalog.py +0 -0
  92. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/checkpoints.py +0 -0
  93. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/checks.py +0 -0
  94. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/clipboard.py +0 -0
  95. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/cloud.py +0 -0
  96. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/codesearch.py +0 -0
  97. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/context.py +0 -0
  98. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/escalate.py +0 -0
  99. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/extensions.py +0 -0
  100. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/frontmatter.py +0 -0
  101. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/gitflow.py +0 -0
  102. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/hardware.py +0 -0
  103. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/hooks.py +0 -0
  104. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/jobs.py +0 -0
  105. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/limits.py +0 -0
  106. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/lsp.py +0 -0
  107. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/matching.py +0 -0
  108. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/mcp/__init__.py +0 -0
  109. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/mcp/auth.py +0 -0
  110. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/mcp/catalog.py +0 -0
  111. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/mcp/catalog.toml +0 -0
  112. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/mcp/client.py +0 -0
  113. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/mcp/commands.py +0 -0
  114. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/mcp/config.py +0 -0
  115. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/mcp/http.py +0 -0
  116. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/mcp/manager.py +0 -0
  117. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/mcp/protocol.py +0 -0
  118. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/mcp/server.py +0 -0
  119. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/mcp/stdio.py +0 -0
  120. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/memory.py +0 -0
  121. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/models.toml +0 -0
  122. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/notify.py +0 -0
  123. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/ollama.py +0 -0
  124. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/permissions.py +0 -0
  125. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/planning.py +0 -0
  126. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/render.py +0 -0
  127. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/repeats.py +0 -0
  128. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/repomap.py +0 -0
  129. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/residency.py +0 -0
  130. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/review.py +0 -0
  131. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/sandbox.py +0 -0
  132. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/screenshots.py +0 -0
  133. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/secretscan.py +0 -0
  134. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/servers/__init__.py +0 -0
  135. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/servers/encord.py +0 -0
  136. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/servers/valohai.py +0 -0
  137. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/servers/xai.py +0 -0
  138. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/steer.py +0 -0
  139. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/subagents.py +0 -0
  140. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/tools.py +0 -0
  141. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/unreachable.py +0 -0
  142. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/update.py +0 -0
  143. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/vision.py +0 -0
  144. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/web.py +0 -0
  145. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/src/lcode/workflows/__init__.py +0 -0
  146. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/tests/conftest.py +0 -0
  147. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/tests/fake_lsp_server.py +0 -0
  148. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/tests/fake_mcp_server.py +0 -0
  149. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/tests/test_acp.py +0 -0
  150. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/tests/test_action.py +0 -0
  151. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/tests/test_agent.py +0 -0
  152. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/tests/test_api.py +0 -0
  153. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/tests/test_backends.py +0 -0
  154. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/tests/test_bench.py +0 -0
  155. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/tests/test_catalog.py +0 -0
  156. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/tests/test_checkpoints.py +0 -0
  157. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/tests/test_checks.py +0 -0
  158. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/tests/test_cli.py +0 -0
  159. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/tests/test_clipboard.py +0 -0
  160. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/tests/test_cloud.py +0 -0
  161. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/tests/test_codesearch.py +0 -0
  162. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/tests/test_config.py +0 -0
  163. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/tests/test_context.py +0 -0
  164. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/tests/test_context_picker.py +0 -0
  165. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/tests/test_escalate.py +0 -0
  166. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/tests/test_extensions.py +0 -0
  167. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/tests/test_gitflow.py +0 -0
  168. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/tests/test_hooks.py +0 -0
  169. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/tests/test_jobs.py +0 -0
  170. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/tests/test_look.py +0 -0
  171. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/tests/test_lsp.py +0 -0
  172. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/tests/test_matching.py +0 -0
  173. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/tests/test_mcp.py +0 -0
  174. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/tests/test_memory.py +0 -0
  175. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/tests/test_model_picker.py +0 -0
  176. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/tests/test_permissions.py +0 -0
  177. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/tests/test_plan_mode.py +0 -0
  178. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/tests/test_repeats.py +0 -0
  179. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/tests/test_repomap.py +0 -0
  180. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/tests/test_residency.py +0 -0
  181. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/tests/test_review.py +0 -0
  182. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/tests/test_robustness.py +0 -0
  183. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/tests/test_sandbox.py +0 -0
  184. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/tests/test_screenshots.py +0 -0
  185. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/tests/test_secretscan.py +0 -0
  186. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/tests/test_servers.py +0 -0
  187. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/tests/test_sessions.py +0 -0
  188. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/tests/test_steer.py +0 -0
  189. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/tests/test_stuck.py +0 -0
  190. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/tests/test_subagents.py +0 -0
  191. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/tests/test_tools.py +0 -0
  192. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/tests/test_update.py +0 -0
  193. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/tests/test_vision.py +0 -0
  194. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/tests/test_web.py +0 -0
  195. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/tests/test_xai.py +0 -0
  196. {lcode_cli-0.40.0 → lcode_cli-0.42.0}/uv.lock +0 -0
@@ -6,6 +6,42 @@ All notable changes to lcode are documented here. The format follows
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.42.0] - 2026-10-24
10
+
11
+ ### Added
12
+
13
+ - **Image mode's options, results and follow-ups** (#151). See
14
+ [Image mode](https://nasser1941.github.io/lcode/modes/#image-mode).
15
+ - **Options in a request** (`a red fox --n 4 --ar 16:9 --seed 42 --quality best --style photo`)
16
+ or for the session (`/options n=4 ar=16:9`): how many images, the shape, the seed (counting up
17
+ over several images), a quality preset (0.5, 1 or 2 megapixels), ten style presets, and a
18
+ negative prompt for models that use one.
19
+ - **Numbered results, shown in the terminal:** the picture itself in kitty, Ghostty, iTerm2 and
20
+ WezTerm, and a half-block colour preview elsewhere (the `preview` setting).
21
+ - **Follow-ups:** `/again` (new seeds), `/vary 3` (four variations of image #3), `/gallery` and
22
+ `/open 3`. The gallery and options are saved with the session.
23
+ - `lcode --mode image -p "… --n 2"` reports every image's file and seed.
24
+ - One image is now named `<time>-<prompt>.png` (no `-1`).
25
+
26
+ ## [0.41.0] - 2026-10-23
27
+
28
+ ### Added
29
+
30
+ - **Image models to choose from, installed by lcode** (#150). lcode's media catalog lists the image
31
+ models it can run, with their size, license, measured speed and whether they fit your GPU.
32
+ **Z-Image Turbo** (photorealistic) joins FLUX.2 klein 4B (fast): 17 s and 3.5 s per 1024×1024
33
+ image on a 12 GB laptop GPU. See
34
+ [Image models](https://nasser1941.github.io/lcode/modes/#image-models).
35
+ - **Choose one:** `/models` in image mode (numbered) or `/model z-image-turbo` switches; the
36
+ `image_model` setting is the default, and sessions keep theirs.
37
+ - **Installed on demand:** a model that isn't installed downloads with your next image, after you
38
+ say yes, or ahead with `lcode media install <model>`. lcode downloads only the missing files
39
+ (models share text encoders and VAEs) into ComfyUI's model folders, checks the disk first,
40
+ verifies each file's SHA-256, and resumes interrupted downloads. `HF_TOKEN` works for gated
41
+ models.
42
+ - **`lcode media`** lists image and video models with what fits and what's installed, and
43
+ `lcode doctor` says how to install the image model when it's missing.
44
+
9
45
  ## [0.40.0] - 2026-10-23
10
46
 
11
47
  ### Added
@@ -904,7 +940,9 @@ First public release.
904
940
  - `AGENTS.md` project instructions and `/init` to generate them.
905
941
  - One-line installer for Linux and macOS.
906
942
 
907
- [Unreleased]: https://github.com/nasser1941/lcode/compare/v0.40.0...HEAD
943
+ [Unreleased]: https://github.com/nasser1941/lcode/compare/v0.42.0...HEAD
944
+ [0.42.0]: https://github.com/nasser1941/lcode/compare/v0.41.0...v0.42.0
945
+ [0.41.0]: https://github.com/nasser1941/lcode/compare/v0.40.0...v0.41.0
908
946
  [0.40.0]: https://github.com/nasser1941/lcode/compare/v0.39.0...v0.40.0
909
947
  [0.39.0]: https://github.com/nasser1941/lcode/compare/v0.38.0...v0.39.0
910
948
  [0.38.0]: https://github.com/nasser1941/lcode/compare/v0.37.0...v0.38.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: lcode-cli
3
- Version: 0.40.0
3
+ Version: 0.42.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/
@@ -27,6 +27,8 @@ lcode config path # print the file location
27
27
  | `comfyui_url` | `http://127.0.0.1:8188` | ComfyUI's address, for image and video mode |
28
28
  | `comfyui_path` | (found) | Your ComfyUI folder, if lcode doesn't find it; lcode starts it when needed ([ComfyUI](modes.md#comfyui)) |
29
29
  | `media_dir` | `.lcode/media` | Where images and videos go: a folder in the project, or an absolute path |
30
+ | `preview` | `auto` | Images in the terminal: `auto`, `kitty`, `iterm`, `blocks` (colour text) or `off` ([results](modes.md#results)) |
31
+ | `image_model` | `flux2-klein-4b` | The model image mode uses: a key from `lcode media` ([image models](modes.md#image-models)) |
30
32
  | `think` | `true` | Let the model reason before answering |
31
33
  | `web` | `on` | Web search and page fetching: `on`, `ask` (before each search/website) or `off` |
32
34
  | `search_backend` | `auto` | `auto`, `ollama`, `brave`, `tavily` or `searxng`; `auto` uses the first one configured |
@@ -0,0 +1,211 @@
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
+ ◆ #1 .lcode/media/20261023-184531-a-red-fox-sitting-in-fresh-snow-at-dusk.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
+ with the image model you choose (see [Image models](#image-models)).
38
+
39
+ ```text
40
+ image ❯ a red fox in fresh snow --n 2 --ar 16:9 --seed 7
41
+ ◆ #1 .lcode/media/20261023-194019-a-red-fox-in-fresh-snow.png 1360×768 · FLUX.2 klein 4B · seed 7 · 4.9 s
42
+ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ (the picture, in colour)
43
+ ◆ #2 .lcode/media/20261023-194024-a-red-fox-in-fresh-snow.png 1360×768 · FLUX.2 klein 4B · seed 8 · 2.7 s
44
+ image ❯ /vary 1 --n 2
45
+ Variations of #1: a red fox in fresh snow
46
+ ◆ #3 …
47
+ ```
48
+
49
+ ### Options
50
+
51
+ Add them to a request, or set them for the session with `/options` (`/options n=4 ar=16:9
52
+ quality=best style=photo`). A flag in a request wins over `/options` for that request. `/options`
53
+ alone shows them.
54
+
55
+ | Flag | `/options` | What it does |
56
+ |---|---|---|
57
+ | `--n 4` | `n=4` | How many images: 1 to 8. Each gets its own seed. |
58
+ | `--ar 16:9` | `ar=16:9` | The shape: a ratio (`3:2`, `9:16`, up to 4:1), a name (`square`, `portrait`, `landscape`, `wide`, `tall`, `photo`, `cinema`), or pixels (`--size 1344x768`). |
59
+ | `--seed 42` | `seed=42` | The same seed and settings make the same image. With `--n`, the seeds count up (42, 43, …). `seed=random` goes back to a new one each time. |
60
+ | `--quality best` | `quality=best` | `fast` (about 0.5 megapixels, fewer steps for models that allow it), `balanced` (1 megapixel, the default), `best` (2 megapixels, more steps where they help). |
61
+ | `--style photo` | `style=photo` | Adds a style to the description: `photo`, `cinematic`, `anime`, `watercolor`, `oil`, `pixel`, `3d`, `sketch`, `comic`, `flat`. `style=none` takes it off. |
62
+ | `--no "text, people"` | `no="text"` | A negative prompt, for models that use one. FLUX.2 klein and Z-Image Turbo don't (they're distilled), and lcode says so: describe what you want instead. |
63
+
64
+ ### Results
65
+
66
+ - **Numbered:** every image in the session gets a number (`#3`), shown with its file, size, model,
67
+ seed and time.
68
+ - **Shown in the terminal:** in kitty, Ghostty, iTerm2 and WezTerm the picture itself appears
69
+ under its line. Other terminals get a small colour preview drawn with half blocks. The
70
+ `preview` setting chooses: `auto`, `kitty`, `iterm`, `blocks` or `off`. Inside tmux or screen,
71
+ lcode draws the half-block preview.
72
+ - **Follow-ups:**
73
+
74
+ | Command | What it does |
75
+ |---|---|
76
+ | `/again` | The last request again with new seeds; flags change it (`/again --n 4 --ar 9:16`) |
77
+ | `/vary 3` | Four variations of image #3: its prompt and options, new seeds (`/vary 3 --n 2`; the last image without a number) |
78
+ | `/gallery` | The session's images with their prompt, size, model and seed; `/gallery all` lists the latest 50 in the media folder |
79
+ | `/open 3` | Opens image #3 in your image viewer (the last one without a number) |
80
+
81
+ - **Sessions:** the gallery and `/options` are saved with the session, so `lcode -c` or `/resume`
82
+ brings them back. An image session is listed under its first prompt.
83
+
84
+ - **Progress:** the status line says what ComfyUI is doing (loading the model, reading the
85
+ prompt, drawing, decoding), the step, the time left and the time so far. If other jobs are ahead
86
+ in ComfyUI's queue, it says how many.
87
+ - **Cancel** with Esc or Ctrl+C: lcode cancels the job in ComfyUI too, so it stops using the GPU.
88
+ - **Type ahead:** what you type while an image is being made waits in the next prompt.
89
+ - **Files:** each image goes to `.lcode/media/` in the project (the `media_dir` setting), named
90
+ after the time and the prompt, with a `.json` next to it holding the prompt, model, seed, size,
91
+ steps, quality and style. PNGs carry the same in their own metadata (an `lcode` text chunk), and ComfyUI's workflow
92
+ as well. `.lcode/media/` keeps itself out of git; a folder of your own (`lcode config set
93
+ media_dir assets/generated`) doesn't.
94
+ - **Nothing is lost:** lcode writes each job down while it runs. If lcode crashes or the terminal
95
+ closes before an image arrives, the next request in image mode saves it.
96
+ - **From scripts:** `lcode --mode image -p "a red fox in the snow --n 2"` makes the images and
97
+ exits; with `--output json` it prints a result with the file paths, the model, the seeds, the size
98
+ and the time.
99
+ - **No Ollama needed:** `lcode --mode image` starts even when Ollama isn't running or the coding
100
+ model isn't installed; `/mode code` then says what's missing.
101
+
102
+ ## ComfyUI
103
+
104
+ ComfyUI runs the image and video models; lcode drives it through its API.
105
+
106
+ - **One that's running** at `comfyui_url` (by default `http://127.0.0.1:8188`) is used as it is.
107
+ - **Otherwise lcode starts one** in the background when you switch to image mode, so the first
108
+ image waits less, and stops it when you quit or go back to code mode, or when the terminal
109
+ closes. It looks in the `comfyui_path` setting, then the folder the `comfy` CLI uses, then
110
+ `~/comfy/ComfyUI`, `~/ComfyUI`, `~/comfyui` and `~/Documents/ComfyUI`. It uses that install's
111
+ own Python (its `.venv` or `venv`), and logs to `comfyui.log` in lcode's state folder.
112
+ - **With none,** your first image request offers to install ComfyUI into lcode's state folder with
113
+ [comfy-cli](https://github.com/Comfy-Org/comfy-cli) and the PyTorch build for your GPU (NVIDIA,
114
+ Apple Silicon or CPU). That downloads several GB. To install it yourself:
115
+
116
+ ```bash
117
+ uvx --from comfy-cli comfy install
118
+ ```
119
+
120
+ - **`lcode doctor`** shows whether ComfyUI is running or found, and whether the image model is
121
+ installed.
122
+
123
+ ## Image models
124
+
125
+ lcode's media catalog lists the image models it can run, with what each needs and whether it fits
126
+ your machine. `/models` in image mode shows them, numbered; pick one with its number, or `/model
127
+ z-image-turbo`. `lcode config set image_model z-image-turbo` makes one the default.
128
+
129
+ ```text
130
+ image ❯ /models
131
+ 1. ● FLUX.2 klein 4B (flux2-klein-4b) text-to-image · 12.4 GB · installed · fits (~9 GB of 12 GB GPU memory)
132
+ 1024×1024: 3.5 s per image, 14 s for the first with ComfyUI starting (RTX 4080 Laptop, 12 GB)
133
+ 2. Z-Image Turbo (z-image-turbo) text-to-image · 14.6 GB · 6.5 GB to download · fits (~9 GB of 12 GB GPU memory)
134
+ 1024×1024: 17 s per image (1.9 s per step), 23 s for the first (RTX 4080 Laptop, 12 GB)
135
+ Switch to which model? (number or name, Enter to keep FLUX.2 klein 4B): 2
136
+ Image model: Z-Image Turbo.
137
+ Not installed yet: 6.5 GB download with your next image (you're asked first).
138
+ ```
139
+
140
+ | Model | Made by | License | Steps | Good at |
141
+ |---|---|---|---|---|
142
+ | **FLUX.2 klein 4B** (default) | Black Forest Labs | Apache 2.0 | 4 | Speed: a few seconds per image |
143
+ | **Z-Image Turbo** | Tongyi-MAI (Alibaba) | Apache 2.0 | 8 | Photorealism, text in images; slower |
144
+
145
+ Measured speeds are in the list (`/models`, `lcode media`) and in [Measured](#measured).
146
+
147
+ - **Installing:** a model that isn't installed downloads with your next image, after you say yes.
148
+ You can also install it ahead with `lcode media install z-image-turbo`. lcode downloads only
149
+ the files ComfyUI doesn't have: models share text encoders and VAEs, so Z-Image Turbo needs
150
+ 6.5 GB once FLUX.2 klein is there. Files go into ComfyUI's model folders, and the folders of
151
+ `extra_model_paths.yaml` count when ComfyUI is running.
152
+ - **Safe downloads:** lcode checks the disk first and keeps 2 GB free. The progress shows the
153
+ speed and the time left. Each file is checked against Hugging Face's SHA-256 before it's used.
154
+ - **Interrupted?** Ctrl+C pauses a download, and so does a dropped connection. Run the same install
155
+ again and it continues where it stopped (a `.part` file holds what arrived).
156
+ - **Gated models:** for a model whose license you must accept on Hugging Face, accept it there
157
+ and set `HF_TOKEN`. lcode sends the token to huggingface.co only.
158
+ - **Will it fit?** On NVIDIA GPUs the list compares an estimate of the GPU memory the model needs
159
+ with your GPU: the largest of its files plus 1.5 GB. ComfyUI loads the text encoder, the model and
160
+ the VAE one after another, so the largest file sets the need. A model too big for the GPU still
161
+ runs, slower, because ComfyUI keeps part of it in RAM. On Apple Silicon, the list compares it
162
+ with unified memory.
163
+
164
+ ```bash
165
+ lcode media # image and video models, what fits, what's installed
166
+ lcode media install z-image-turbo # download what's missing (asks first; --yes doesn't)
167
+ ```
168
+
169
+ ### Measured
170
+
171
+ On an RTX 4080 Laptop GPU (12 GB), FLUX.2 klein 4B makes a 1360×768 image in 2.6 to 2.8 s and a
172
+ `--quality fast` image (720×720) in 1.3 to 2.0 s once it's loaded.
173
+
174
+
175
+ On an RTX 4080 Laptop GPU (12 GB) with 32 GB of RAM, ComfyUI 0.38, 1024×1024, four different
176
+ prompts each:
177
+
178
+ | Model | First image (ComfyUI starting, model loading) | Each next image | Per step |
179
+ |---|---|---|---|
180
+ | FLUX.2 klein 4B (fp8) | 14.3 s | 3.5 s | 0.6 s (4 steps) |
181
+ | Z-Image Turbo (int8) | 22.9 s | 17.4 s | 1.9 s (8 steps) |
182
+
183
+ ComfyUI uses whatever GPU memory is free to keep models loaded (11.3 to 11.5 GB here), so the
184
+ memory a model *needs* is lower than what `nvidia-smi` shows while it runs.
185
+
186
+ ## One model on the GPU at a time
187
+
188
+ Image and video modes don't need the big coding model: the image or video model does the work. A
189
+ small optional model will help with prompts later. Image and video models need the GPU to
190
+ themselves:
191
+
192
+ - **Into image or video mode:** lcode unloads its coding model from Ollama, and checks again
193
+ before each image.
194
+ - **Back to code mode:** lcode stops the ComfyUI it started, or asks yours to free its memory,
195
+ then loads the coding model again in the background.
196
+
197
+ ## Coming next
198
+
199
+ Video mode doesn't generate yet: a request there says what's coming instead of going anywhere.
200
+
201
+ - **#150 Media model catalog** (image models done; video models come with video generation).
202
+ - **#151, #152 Image mode:** generating, variations, sizes and seeds, a gallery, editing,
203
+ references, inpainting, upscaling.
204
+ - **#153, #154 Video mode:** from text or a start image, first and last frames, several
205
+ references, camera moves, longer clips, sound.
206
+ - **#155 Movies:** storyboards with a consistent cast, shots stitched into one film.
207
+ - **#156 Prompt assistant:** a small local model for prompts and follow-ups.
208
+ - **#157 Cloud generation**, with costs shown (Grok Imagine first).
209
+
210
+ The permission mode (`ask`, `plan`, `auto-edit`, `yolo`) is a different setting: `/permissions`
211
+ sets it, and Shift+Tab cycles it as before.
@@ -12,7 +12,7 @@ lcode
12
12
 
13
13
  ```text
14
14
  ╭──────────────────────────────────────────────╮
15
- │ lcode v0.40.0 — local coding agent │
15
+ │ lcode v0.42.0 — local coding agent │
16
16
  │ │
17
17
  │ model lcode-qwen3.6-35b │
18
18
  │ context 256K tokens │
@@ -7,6 +7,7 @@ lcode [options] # interactive session in the current directory
7
7
  lcode setup [model] # pick, download and configure a model
8
8
  lcode models # the model catalog and what fits this machine
9
9
  lcode doctor # check the installation (include this in bug reports)
10
+ lcode media [install MODEL] # image and video models: what fits here, what's installed; install one
10
11
  lcode config [set|unset] # show or change settings
11
12
  lcode bench [models] # score models on small coding tasks on this machine
12
13
  lcode mcp [catalog|add|list] # connect MCP servers: Jira, GitHub, AWS, databases… (see MCP servers)
@@ -80,6 +81,7 @@ reasoning is on.
80
81
  | `/think [on\|off]` | Toggle reasoning |
81
82
  | `/verbose` | Toggle showing the reasoning text |
82
83
  | `/mode [code\|image\|video]` | Switch what lcode makes: [code, images or videos](modes.md) |
84
+ | `/options`, `/again`, `/vary N`, `/gallery`, `/open N` | Image mode: [options for the session, follow-ups on results](modes.md#results) |
83
85
  | `/permissions [ask\|plan\|auto-edit\|yolo]` | Set the permission mode |
84
86
  | `/cd DIR` | Change the working directory |
85
87
  | `/todos` | Show the model's task list |
@@ -3,7 +3,7 @@
3
3
  From a program: `from lcode import Session` (see lcode.api).
4
4
  """
5
5
 
6
- __version__ = "0.40.0"
6
+ __version__ = "0.42.0"
7
7
 
8
8
  API = ("Session", "Result", "Options", "SetupError")
9
9
 
@@ -308,6 +308,8 @@ class Settings:
308
308
  comfyui_url: str = "http://127.0.0.1:8188"
309
309
  comfyui_path: str | None = None # a ComfyUI install lcode doesn't find by itself (lcode.comfy)
310
310
  media_dir: str = ".lcode/media" # where image and video mode save their files (lcode.media)
311
+ image_model: str = "flux2-klein-4b" # the media catalog's model image mode uses (lcode.media_catalog)
312
+ preview: str = "auto" # images in the terminal: auto, kitty, iterm, blocks or off (lcode.preview)
311
313
  num_batch: int | None = None
312
314
  keep_alive: str = "30m"
313
315
  think: bool = True
@@ -374,6 +376,11 @@ class Agent:
374
376
  self.interactive = True # False for `lcode -p`: no end-of-session questions
375
377
  self.comfy_process = None # the ComfyUI lcode started for image and video mode, stopped when it exits
376
378
  self.no_coding_model = "" # why there's no coding model, when lcode started in image or video mode without
379
+ from lcode.imageopts import Options as ImageOptions
380
+
381
+ self.image_options = ImageOptions() # image mode's defaults for this session (/options)
382
+ self.media_results: list[dict] = [] # the session's images, numbered (/gallery, /vary, /open)
383
+ self.last_request: tuple[str, ImageOptions] | None = None # for /again
377
384
  self.reflected = 0 # messages before this index were already checked for notes to remember
378
385
  self._memory: memory_notes.Memory | None = None
379
386
  self._agent_types: tuple[Path, dict[str, subagents.AgentType], list[str]] | None = None
@@ -678,8 +685,10 @@ class Agent:
678
685
  return any(m.get("role") == "user" for m in self.messages)
679
686
 
680
687
  def save(self) -> None:
681
- if not (self.has_conversation() or self.session_name):
688
+ if not (self.has_conversation() or self.session_name or self.media_results):
682
689
  return # nothing worth resuming
690
+ if not self.session_title and not self.has_conversation() and self.media_results:
691
+ self.session_title = " ".join(self.media_results[0]["prompt"].split())[:60] # an image session
683
692
  self.session_title = self.session_title or sessions.title_from(self.messages)
684
693
  f = self.session_file()
685
694
  f.parent.mkdir(parents=True, exist_ok=True)
@@ -687,6 +696,8 @@ class Agent:
687
696
  "cwd": str(self.workspace), # where `lcode -c` there finds it, wherever the shell moved
688
697
  "model": self.settings.model,
689
698
  "mode": self.settings.mode,
699
+ "image_model": self.settings.image_model,
700
+ "media": {"results": self.media_results, "options": self.image_options.to_json()},
690
701
  "name": self.session_name,
691
702
  "title": self.session_title,
692
703
  "messages": self.messages,
@@ -735,6 +746,11 @@ class Agent:
735
746
  self.session_name = data.get("name", "")
736
747
  if data.get("mode") in ("code", "image", "video"):
737
748
  self.settings.mode = data["mode"] # a session belongs to the mode it was made in
749
+ if isinstance(data.get("image_model"), str):
750
+ self.settings.image_model = data["image_model"]
751
+ media = data.get("media") or {}
752
+ self.media_results = [r for r in media.get("results") or [] if isinstance(r, dict) and "path" in r]
753
+ self.image_options = type(self.image_options).from_json(media.get("options") or {})
738
754
  self.session_title = data.get("title") or sessions.title_from(self.messages)
739
755
  self.tools.read_mtimes.clear() # files may have changed since; the model must read them again
740
756
  self.tools.todos = list(data.get("todos") or [])
@@ -200,6 +200,8 @@ def open_agent(cwd: Path, options: Options, console: Console, hw: Hardware | Non
200
200
  comfyui_url=cfg["comfyui_url"],
201
201
  comfyui_path=cfg["comfyui_path"],
202
202
  media_dir=cfg["media_dir"],
203
+ image_model=cfg["image_model"],
204
+ preview=cfg["preview"],
203
205
  web=switch(options.web, cfg["web"], "on"),
204
206
  search_backend=cfg["search_backend"],
205
207
  searxng_url=cfg["searxng_url"],
@@ -777,9 +777,10 @@ def exit_on_hangup() -> None:
777
777
 
778
778
  def comfyui_status(cfg: dict) -> tuple[str, bool | None]:
779
779
  """lcode doctor's ComfyUI line: running or found, and whether the image model is there."""
780
- from lcode import comfy, media
780
+ from lcode import comfy
781
+ from lcode import media_catalog as catalog
781
782
 
782
- workflow = comfy.load_workflow(media.IMAGE_WORKFLOW)
783
+ model = catalog.load().find(cfg.get("image_model") or "") or catalog.load().models[catalog.DEFAULT_IMAGE]
783
784
  server = comfy.Server(cfg["comfyui_url"])
784
785
  stats = server.stats()
785
786
  install = None if stats else comfy.find_install(cfg["comfyui_path"])
@@ -787,14 +788,13 @@ def comfyui_status(cfg: dict) -> tuple[str, bool | None]:
787
788
  return "not found: image mode offers to install it (or lcode config set comfyui_path <folder>)", None
788
789
  if stats:
789
790
  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
791
  else:
792
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)]
793
+ _, missing = catalog.chosen_files(model, catalog.lister(server if stats else None, install))
795
794
  if missing:
796
- return f"{where} · {workflow.name} not installed (missing {', '.join(missing)})", None
797
- return f"{where} · image model: {workflow.name}", True
795
+ size = sum(f.gb for f in missing)
796
+ return f"{where} · {model.name} not installed: lcode media install {model.key} ({size:.1f} GB)", None
797
+ return f"{where} · image model: {model.name}", True
798
798
 
799
799
 
800
800
  def media_request(agent, prompt: str, out: Console, quiet: bool) -> None:
@@ -812,7 +812,7 @@ def media_request(agent, prompt: str, out: Console, quiet: bool) -> None:
812
812
  api.close_agent(agent)
813
813
  sys.exit(2)
814
814
  try:
815
- made = media.generate_image(agent, prompt)
815
+ made = media.request(agent, prompt)
816
816
  except (media.MediaError, KeyboardInterrupt) as e:
817
817
  message = str(e) or "Cancelled."
818
818
  if quiet:
@@ -826,12 +826,13 @@ def media_request(agent, prompt: str, out: Console, quiet: bool) -> None:
826
826
  "type": "result",
827
827
  "status": "success",
828
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),
829
+ "files": [str(path) for one in made for path in one.files],
830
+ "model": made[0].model,
831
+ "seed": made[0].seed,
832
+ "seeds": [one.seed for one in made],
833
+ "width": made[0].width,
834
+ "height": made[0].height,
835
+ "seconds": round(sum(one.seconds for one in made), 1),
835
836
  }
836
837
  print(json.dumps(result, ensure_ascii=False), flush=True)
837
838
  api.close_agent(agent)
@@ -909,6 +910,13 @@ def build_subparsers() -> dict[str, argparse.ArgumentParser]:
909
910
  subs["setup"] = p
910
911
  subs["models"] = argparse.ArgumentParser(prog="lcode models", description="List models and how they fit here.")
911
912
  subs["doctor"] = argparse.ArgumentParser(prog="lcode doctor", description="Check the installation.")
913
+ p = argparse.ArgumentParser(
914
+ prog="lcode media", description="Image and video models: list them, with how they fit here, or install one."
915
+ )
916
+ p.add_argument("action", nargs="?", choices=["list", "install"], default="list")
917
+ p.add_argument("model", nargs="?", help="with install: a model key from the list, e.g. z-image-turbo")
918
+ p.add_argument("-y", "--yes", action="store_true", help="don't ask before downloading")
919
+ subs["media"] = p
912
920
  p = argparse.ArgumentParser(prog="lcode config", description="Show or change settings.")
913
921
  p.add_argument("action", nargs="?", choices=["show", "set", "unset", "path"], default="show")
914
922
  p.add_argument("key", nargs="?", choices=list(config.SETTINGS))
@@ -1065,6 +1073,55 @@ def cmd_index(args) -> None:
1065
1073
  )
1066
1074
 
1067
1075
 
1076
+ def cmd_media(args) -> None:
1077
+ """lcode media [list] | install <model>: the media catalog, what's installed in ComfyUI, downloads."""
1078
+ from rich.prompt import Confirm
1079
+
1080
+ from lcode import comfy, media
1081
+ from lcode import media_catalog as catalog
1082
+
1083
+ try:
1084
+ cfg = config.load()
1085
+ except ConfigError as e:
1086
+ fail(str(e))
1087
+ cat, hw = catalog.load(), detect()
1088
+ server = comfy.Server(cfg["comfyui_url"])
1089
+ install = comfy.find_install(cfg["comfyui_path"])
1090
+ running = server.stats() is not None
1091
+ installed = catalog.lister(server if running else None, install) if running or install else None
1092
+ if args.action == "install":
1093
+ model = cat.find(args.model or "")
1094
+ if model is None:
1095
+ fail(f"no media model {args.model!r}: lcode media lists them")
1096
+ if not running and install is None:
1097
+ fail(f"lcode needs ComfyUI to install models into. {comfy.install_hint()}")
1098
+ ask = None if args.yes else (lambda q: Confirm.ask(q, default=True, console=console))
1099
+ try:
1100
+ saved = catalog.install(model, server if running else None, install, console, ask)
1101
+ except catalog.CatalogError as e:
1102
+ fail(str(e))
1103
+ how = f" ({len(saved)} file(s) downloaded)." if saved else "."
1104
+ console.print(f"[green]{model.name} is installed[/]{how}")
1105
+ return
1106
+ where = (
1107
+ f"ComfyUI at {server.url}" if running else (f"{install.path} (not running)" if install else "no ComfyUI found")
1108
+ )
1109
+ console.print(f"[dim]{hw.describe()} · {where} · image model: {cfg['image_model']}[/]\n")
1110
+ for mode in ("image", "video"):
1111
+ models = cat.for_mode(mode)
1112
+ console.print(f"[bold]{mode.capitalize()} models[/]")
1113
+ if not models:
1114
+ console.print(f" [dim]{media.SOON[mode]}[/]\n")
1115
+ continue
1116
+ for m in models:
1117
+ line = escape(catalog.describe(m, hw, installed))
1118
+ console.print(f" [bold]{m.key:<16}[/] {escape(m.name)} [dim]· {line}[/]")
1119
+ if m.measured:
1120
+ console.print(f" {'':<16} [dim]{escape(m.measured)}[/]")
1121
+ console.print()
1122
+ console.print("[dim]Install one: lcode media install <model> · choose in image mode: /model <model>[/]")
1123
+
1124
+
1068
1125
  def cmd_mcp(args) -> None:
1069
1126
  from lcode.mcp.commands import run
1070
1127
 
@@ -1075,6 +1132,7 @@ COMMANDS = {
1075
1132
  "setup": cmd_setup,
1076
1133
  "models": cmd_models,
1077
1134
  "doctor": cmd_doctor,
1135
+ "media": cmd_media,
1078
1136
  "config": cmd_config,
1079
1137
  "bench": cmd_bench,
1080
1138
  "mcp": cmd_mcp,
@@ -254,6 +254,14 @@ class Server:
254
254
  except (requests.RequestException, ValueError):
255
255
  return []
256
256
 
257
+ def folder_paths(self) -> dict[str, list[str]]:
258
+ """Where ComfyUI keeps each kind of model (its own folders and extra_model_paths.yaml's)."""
259
+ try:
260
+ r = requests.get(self.url + "/internal/folder_paths", timeout=10)
261
+ return r.json() if r.status_code == 200 else {}
262
+ except (requests.RequestException, ValueError):
263
+ return {}
264
+
257
265
  def upload(self, path: Path) -> str:
258
266
  """Upload an input image; the name to use in a workflow."""
259
267
  with path.open("rb") as f:
@@ -420,32 +428,18 @@ class Events:
420
428
 
421
429
  @dataclass
422
430
  class Workflow:
431
+ """A graph in ComfyUI's API format with `$name` placeholders: the model's files (by role, from media.toml),
432
+ the prompt, the seed and the settings in `defaults`."""
433
+
423
434
  key: str
424
- name: str
425
- task: str
426
- models: dict[str, list[str]] # ComfyUI folder -> file names it can use, preferred first
427
- defaults: dict
428
435
  graph: dict
436
+ defaults: dict = field(default_factory=dict)
429
437
  labels: dict[str, str] = field(default_factory=dict) # node id -> what it does, for progress
430
438
 
431
439
 
432
440
  def load_workflow(key: str) -> Workflow:
433
441
  data = json.loads(resources.files("lcode.workflows").joinpath(f"{key}.json").read_text())
434
- return Workflow(key, data["name"], data["task"], data["models"], data.get("defaults", {}), data["graph"],
435
- data.get("labels", {})) # fmt: skip
436
-
437
-
438
- def choose_files(server: Server, workflow: Workflow) -> tuple[dict[str, str], list[str]]:
439
- """For each model the workflow needs, the installed file to use; and the folders where none is installed."""
440
- chosen, missing = {}, []
441
- for folder, names in workflow.models.items():
442
- installed = set(server.models(folder))
443
- found = next((n for n in names if n in installed), None)
444
- if found:
445
- chosen[folder] = found
446
- else:
447
- missing.append(f"{folder}/{names[0]}")
448
- return chosen, missing
442
+ return Workflow(key, data["graph"], data.get("defaults", {}), data.get("labels", {}))
449
443
 
450
444
 
451
445
  PLACEHOLDER = re.compile(r"^\$([a-z_]+)$")
@@ -566,13 +560,15 @@ def describe_history_error(entry: dict) -> str:
566
560
 
567
561
 
568
562
  def save(server: Server, outputs: dict, folder: Path, stem: str, meta: dict) -> list[Path]:
569
- """Fetch the job's files into `folder`: stem-1.png, … with stem-1.json next to each."""
563
+ """Fetch the job's files into `folder`: stem.png (stem-2.png if that's taken), or stem-1.png, … for several,
564
+ each with its settings in a .json next to it."""
570
565
  folder.mkdir(parents=True, exist_ok=True)
571
566
  items = [item for node in outputs.values() for kind in OUTPUT_KINDS for item in node.get(kind, [])]
567
+ items = [it for it in items if it.get("type", "output") == "output"]
572
568
  saved = []
573
- for i, item in enumerate((it for it in items if it.get("type", "output") == "output"), 1):
569
+ for i, item in enumerate(items, 1):
574
570
  ext = Path(item["filename"]).suffix or ".png"
575
- path = unique(folder / f"{stem}-{i}{ext}")
571
+ path = unique(folder / (f"{stem}{ext}" if len(items) == 1 else f"{stem}-{i}{ext}"))
576
572
  data = server.fetch(item)
577
573
  if ext.lower() == ".png":
578
574
  data = with_text(data, "lcode", json.dumps(meta, ensure_ascii=False))
@@ -45,6 +45,8 @@ SETTINGS: dict[str, tuple[object, type, str]] = {
45
45
  "comfyui_url": ("http://127.0.0.1:8188", str, "ComfyUI, for image and video mode"),
46
46
  "comfyui_path": (None, str, "your ComfyUI folder, if lcode doesn't find it (lcode starts it when needed)"),
47
47
  "media_dir": (".lcode/media", str, "where images and videos go: a folder in the project, or an absolute path"),
48
+ "image_model": ("flux2-klein-4b", str, "the model image mode uses: a key from `lcode media`"),
49
+ "preview": ("auto", str, "images in the terminal: auto | kitty | iterm | blocks (colour text) | off"),
48
50
  "think": (True, bool, "let the model reason before answering (slower, better)"),
49
51
  "web": ("on", str, "web search and page fetching for the model: on | ask | off"),
50
52
  "search_backend": ("auto", str, "auto | ollama | brave | tavily | searxng (keys come from environment variables)"),
@@ -210,6 +212,17 @@ def coerce(key: str, value: object) -> object:
210
212
  raise ConfigError(f"permission_mode must be one of {', '.join(PERMISSION_MODES)}")
211
213
  if key == "mode" and value not in WORK_MODES:
212
214
  raise ConfigError(f"mode must be one of {', '.join(WORK_MODES)}")
215
+ if key == "preview":
216
+ from lcode.preview import PROTOCOLS
217
+
218
+ if value not in PROTOCOLS:
219
+ raise ConfigError(f"preview must be one of {', '.join(PROTOCOLS)}")
220
+ if key == "image_model":
221
+ from lcode import media_catalog
222
+
223
+ keys = [m.key for m in media_catalog.load().for_mode("image")]
224
+ if value not in keys:
225
+ raise ConfigError(f"image_model must be one of {', '.join(keys)} (see lcode media)")
213
226
  if typ is bool and isinstance(value, str):
214
227
  if value.lower() not in ("true", "false", "1", "0", "yes", "no", "on", "off"):
215
228
  raise ConfigError(f"{key} must be true or false")