lcode-cli 0.40.0__tar.gz → 0.41.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 (191) hide show
  1. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/CHANGELOG.md +21 -1
  2. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/PKG-INFO +1 -1
  3. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/docs/configuration.md +1 -0
  4. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/docs/modes.md +63 -10
  5. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/docs/quickstart.md +1 -1
  6. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/docs/usage.md +1 -0
  7. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/__init__.py +1 -1
  8. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/agent.py +4 -0
  9. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/api.py +1 -0
  10. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/cli.py +64 -7
  11. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/comfy.py +13 -19
  12. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/config.py +7 -0
  13. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/media.py +46 -15
  14. lcode_cli-0.41.0/src/lcode/media.toml +74 -0
  15. lcode_cli-0.41.0/src/lcode/media_catalog.py +331 -0
  16. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/modes.py +1 -1
  17. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/repl.py +43 -9
  18. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/workflows/flux2-klein-4b-t2i.json +2 -9
  19. lcode_cli-0.41.0/src/lcode/workflows/z-image-turbo-t2i.json +31 -0
  20. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/comfy_standin.py +12 -2
  21. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_comfy.py +4 -5
  22. lcode_cli-0.41.0/tests/test_media_catalog.py +312 -0
  23. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_modes.py +2 -1
  24. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/.editorconfig +0 -0
  25. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/.github/CODEOWNERS +0 -0
  26. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  27. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  28. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  29. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/.github/ISSUE_TEMPLATE/model_request.yml +0 -0
  30. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  31. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/.github/dependabot.yml +0 -0
  32. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/.github/workflows/ci.yml +0 -0
  33. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/.github/workflows/docs.yml +0 -0
  34. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/.github/workflows/release.yml +0 -0
  35. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/.gitignore +0 -0
  36. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/.pre-commit-config.yaml +0 -0
  37. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/AGENTS.md +0 -0
  38. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/CODE_OF_CONDUCT.md +0 -0
  39. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/CONTRIBUTING.md +0 -0
  40. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/LICENSE +0 -0
  41. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/README.md +0 -0
  42. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/SECURITY.md +0 -0
  43. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/action.yml +0 -0
  44. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/docs/agents.md +0 -0
  45. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/docs/assets/demo.svg +0 -0
  46. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/docs/assets/extra.css +0 -0
  47. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/docs/automation.md +0 -0
  48. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/docs/bench.md +0 -0
  49. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/docs/changelog.md +0 -0
  50. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/docs/commands.md +0 -0
  51. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/docs/contributing.md +0 -0
  52. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/docs/editors.md +0 -0
  53. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/docs/git.md +0 -0
  54. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/docs/hooks.md +0 -0
  55. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/docs/how-it-works.md +0 -0
  56. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/docs/index.md +0 -0
  57. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/docs/installation.md +0 -0
  58. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/docs/lsp.md +0 -0
  59. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/docs/mcp.md +0 -0
  60. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/docs/memory.md +0 -0
  61. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/docs/models.md +0 -0
  62. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/docs/sandbox.md +0 -0
  63. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/docs/search.md +0 -0
  64. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/docs/servers.md +0 -0
  65. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/docs/troubleshooting.md +0 -0
  66. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/examples/README.md +0 -0
  67. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/examples/api/fix_tests.py +0 -0
  68. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/examples/commands/review.md +0 -0
  69. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/examples/skills/commit-message/SKILL.md +0 -0
  70. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/examples/skills/commit-message/scripts/staged.sh +0 -0
  71. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/install.sh +0 -0
  72. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/mkdocs.yml +0 -0
  73. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/overrides/main.html +0 -0
  74. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/pyproject.toml +0 -0
  75. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/scripts/make_logos.py +0 -0
  76. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/scripts/record_demo.py +0 -0
  77. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/scripts/social-preview.html +0 -0
  78. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/__main__.py +0 -0
  79. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/acp.py +0 -0
  80. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/action.py +0 -0
  81. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/backends.py +0 -0
  82. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/bench.py +0 -0
  83. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/bench_backend.py +0 -0
  84. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/bench_runtime.py +0 -0
  85. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/catalog.py +0 -0
  86. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/checkpoints.py +0 -0
  87. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/checks.py +0 -0
  88. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/clipboard.py +0 -0
  89. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/cloud.py +0 -0
  90. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/codesearch.py +0 -0
  91. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/context.py +0 -0
  92. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/escalate.py +0 -0
  93. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/extensions.py +0 -0
  94. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/frontmatter.py +0 -0
  95. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/gitflow.py +0 -0
  96. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/hardware.py +0 -0
  97. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/hooks.py +0 -0
  98. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/jobs.py +0 -0
  99. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/limits.py +0 -0
  100. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/lsp.py +0 -0
  101. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/matching.py +0 -0
  102. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/mcp/__init__.py +0 -0
  103. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/mcp/auth.py +0 -0
  104. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/mcp/catalog.py +0 -0
  105. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/mcp/catalog.toml +0 -0
  106. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/mcp/client.py +0 -0
  107. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/mcp/commands.py +0 -0
  108. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/mcp/config.py +0 -0
  109. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/mcp/http.py +0 -0
  110. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/mcp/manager.py +0 -0
  111. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/mcp/protocol.py +0 -0
  112. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/mcp/server.py +0 -0
  113. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/mcp/stdio.py +0 -0
  114. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/memory.py +0 -0
  115. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/models.toml +0 -0
  116. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/notify.py +0 -0
  117. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/ollama.py +0 -0
  118. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/permissions.py +0 -0
  119. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/planning.py +0 -0
  120. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/render.py +0 -0
  121. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/repeats.py +0 -0
  122. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/repomap.py +0 -0
  123. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/residency.py +0 -0
  124. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/review.py +0 -0
  125. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/sandbox.py +0 -0
  126. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/screenshots.py +0 -0
  127. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/secretscan.py +0 -0
  128. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/servers/__init__.py +0 -0
  129. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/servers/encord.py +0 -0
  130. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/servers/valohai.py +0 -0
  131. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/servers/xai.py +0 -0
  132. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/sessions.py +0 -0
  133. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/steer.py +0 -0
  134. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/subagents.py +0 -0
  135. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/tools.py +0 -0
  136. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/unreachable.py +0 -0
  137. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/update.py +0 -0
  138. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/vision.py +0 -0
  139. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/web.py +0 -0
  140. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/workflows/__init__.py +0 -0
  141. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/conftest.py +0 -0
  142. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/fake_lsp_server.py +0 -0
  143. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/fake_mcp_server.py +0 -0
  144. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_acp.py +0 -0
  145. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_action.py +0 -0
  146. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_agent.py +0 -0
  147. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_api.py +0 -0
  148. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_backends.py +0 -0
  149. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_bench.py +0 -0
  150. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_catalog.py +0 -0
  151. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_checkpoints.py +0 -0
  152. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_checks.py +0 -0
  153. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_cli.py +0 -0
  154. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_clipboard.py +0 -0
  155. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_cloud.py +0 -0
  156. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_codesearch.py +0 -0
  157. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_config.py +0 -0
  158. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_context.py +0 -0
  159. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_context_picker.py +0 -0
  160. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_escalate.py +0 -0
  161. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_extensions.py +0 -0
  162. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_gitflow.py +0 -0
  163. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_hooks.py +0 -0
  164. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_jobs.py +0 -0
  165. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_look.py +0 -0
  166. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_lsp.py +0 -0
  167. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_matching.py +0 -0
  168. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_mcp.py +0 -0
  169. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_memory.py +0 -0
  170. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_model_picker.py +0 -0
  171. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_permissions.py +0 -0
  172. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_plan_mode.py +0 -0
  173. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_repeats.py +0 -0
  174. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_repomap.py +0 -0
  175. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_residency.py +0 -0
  176. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_review.py +0 -0
  177. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_robustness.py +0 -0
  178. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_sandbox.py +0 -0
  179. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_screenshots.py +0 -0
  180. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_secretscan.py +0 -0
  181. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_servers.py +0 -0
  182. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_sessions.py +0 -0
  183. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_steer.py +0 -0
  184. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_stuck.py +0 -0
  185. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_subagents.py +0 -0
  186. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_tools.py +0 -0
  187. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_update.py +0 -0
  188. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_vision.py +0 -0
  189. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_web.py +0 -0
  190. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_xai.py +0 -0
  191. {lcode_cli-0.40.0 → lcode_cli-0.41.0}/uv.lock +0 -0
@@ -6,6 +6,25 @@ All notable changes to lcode are documented here. The format follows
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.41.0] - 2026-10-23
10
+
11
+ ### Added
12
+
13
+ - **Image models to choose from, installed by lcode** (#150). lcode's media catalog lists the image
14
+ models it can run, with their size, license, measured speed and whether they fit your GPU.
15
+ **Z-Image Turbo** (photorealistic) joins FLUX.2 klein 4B (fast): 17 s and 3.5 s per 1024×1024
16
+ image on a 12 GB laptop GPU. See
17
+ [Image models](https://nasser1941.github.io/lcode/modes/#image-models).
18
+ - **Choose one:** `/models` in image mode (numbered) or `/model z-image-turbo` switches; the
19
+ `image_model` setting is the default, and sessions keep theirs.
20
+ - **Installed on demand:** a model that isn't installed downloads with your next image, after you
21
+ say yes, or ahead with `lcode media install <model>`. lcode downloads only the missing files
22
+ (models share text encoders and VAEs) into ComfyUI's model folders, checks the disk first,
23
+ verifies each file's SHA-256, and resumes interrupted downloads. `HF_TOKEN` works for gated
24
+ models.
25
+ - **`lcode media`** lists image and video models with what fits and what's installed, and
26
+ `lcode doctor` says how to install the image model when it's missing.
27
+
9
28
  ## [0.40.0] - 2026-10-23
10
29
 
11
30
  ### Added
@@ -904,7 +923,8 @@ First public release.
904
923
  - `AGENTS.md` project instructions and `/init` to generate them.
905
924
  - One-line installer for Linux and macOS.
906
925
 
907
- [Unreleased]: https://github.com/nasser1941/lcode/compare/v0.40.0...HEAD
926
+ [Unreleased]: https://github.com/nasser1941/lcode/compare/v0.41.0...HEAD
927
+ [0.41.0]: https://github.com/nasser1941/lcode/compare/v0.40.0...v0.41.0
908
928
  [0.40.0]: https://github.com/nasser1941/lcode/compare/v0.39.0...v0.40.0
909
929
  [0.39.0]: https://github.com/nasser1941/lcode/compare/v0.38.0...v0.39.0
910
930
  [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.41.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,7 @@ 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
+ | `image_model` | `flux2-klein-4b` | The model image mode uses: a key from `lcode media` ([image models](modes.md#image-models)) |
30
31
  | `think` | `true` | Let the model reason before answering |
31
32
  | `web` | `on` | Web search and page fetching: `on`, `ask` (before each search/website) or `off` |
32
33
  | `search_backend` | `auto` | `auto`, `ollama`, `brave`, `tavily` or `searxng`; `auto` uses the first one configured |
@@ -34,9 +34,8 @@ in code mode even if they're still in your `mcp.json`. It says so once.
34
34
 
35
35
  In image mode, what you type is a description, and lcode makes the picture. No language model is
36
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).
37
+ For now that's one image per request, 1024×1024, with the image model you choose (see
38
+ [Image models](#image-models)). Sizes, seeds and variations come next (#151).
40
39
 
41
40
  - **Progress:** the status line says what ComfyUI is doing (loading the model, reading the
42
41
  prompt, drawing, decoding), the step, the time left and the time so far. If other jobs are ahead
@@ -73,12 +72,67 @@ ComfyUI runs the image and video models; lcode drives it through its API.
73
72
  uvx --from comfy-cli comfy install
74
73
  ```
75
74
 
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
75
  - **`lcode doctor`** shows whether ComfyUI is running or found, and whether the image model is
81
- there.
76
+ installed.
77
+
78
+ ## Image models
79
+
80
+ lcode's media catalog lists the image models it can run, with what each needs and whether it fits
81
+ your machine. `/models` in image mode shows them, numbered; pick one with its number, or `/model
82
+ z-image-turbo`. `lcode config set image_model z-image-turbo` makes one the default.
83
+
84
+ ```text
85
+ image ❯ /models
86
+ 1. ● FLUX.2 klein 4B (flux2-klein-4b) text-to-image · 12.4 GB · installed · fits (~9 GB of 12 GB GPU memory)
87
+ 1024×1024: 3.5 s per image, 14 s for the first with ComfyUI starting (RTX 4080 Laptop, 12 GB)
88
+ 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)
89
+ 1024×1024: 17 s per image (1.9 s per step), 23 s for the first (RTX 4080 Laptop, 12 GB)
90
+ Switch to which model? (number or name, Enter to keep FLUX.2 klein 4B): 2
91
+ Image model: Z-Image Turbo.
92
+ Not installed yet: 6.5 GB download with your next image (you're asked first).
93
+ ```
94
+
95
+ | Model | Made by | License | Steps | Good at |
96
+ |---|---|---|---|---|
97
+ | **FLUX.2 klein 4B** (default) | Black Forest Labs | Apache 2.0 | 4 | Speed: a few seconds per image |
98
+ | **Z-Image Turbo** | Tongyi-MAI (Alibaba) | Apache 2.0 | 8 | Photorealism, text in images; slower |
99
+
100
+ Measured speeds are in the list (`/models`, `lcode media`) and in [Measured](#measured).
101
+
102
+ - **Installing:** a model that isn't installed downloads with your next image, after you say yes.
103
+ You can also install it ahead with `lcode media install z-image-turbo`. lcode downloads only
104
+ the files ComfyUI doesn't have: models share text encoders and VAEs, so Z-Image Turbo needs
105
+ 6.5 GB once FLUX.2 klein is there. Files go into ComfyUI's model folders, and the folders of
106
+ `extra_model_paths.yaml` count when ComfyUI is running.
107
+ - **Safe downloads:** lcode checks the disk first and keeps 2 GB free. The progress shows the
108
+ speed and the time left. Each file is checked against Hugging Face's SHA-256 before it's used.
109
+ - **Interrupted?** Ctrl+C pauses a download, and so does a dropped connection. Run the same install
110
+ again and it continues where it stopped (a `.part` file holds what arrived).
111
+ - **Gated models:** for a model whose license you must accept on Hugging Face, accept it there
112
+ and set `HF_TOKEN`. lcode sends the token to huggingface.co only.
113
+ - **Will it fit?** On NVIDIA GPUs the list compares an estimate of the GPU memory the model needs
114
+ with your GPU: the largest of its files plus 1.5 GB. ComfyUI loads the text encoder, the model and
115
+ the VAE one after another, so the largest file sets the need. A model too big for the GPU still
116
+ runs, slower, because ComfyUI keeps part of it in RAM. On Apple Silicon, the list compares it
117
+ with unified memory.
118
+
119
+ ```bash
120
+ lcode media # image and video models, what fits, what's installed
121
+ lcode media install z-image-turbo # download what's missing (asks first; --yes doesn't)
122
+ ```
123
+
124
+ ### Measured
125
+
126
+ On an RTX 4080 Laptop GPU (12 GB) with 32 GB of RAM, ComfyUI 0.38, 1024×1024, four different
127
+ prompts each:
128
+
129
+ | Model | First image (ComfyUI starting, model loading) | Each next image | Per step |
130
+ |---|---|---|---|
131
+ | FLUX.2 klein 4B (fp8) | 14.3 s | 3.5 s | 0.6 s (4 steps) |
132
+ | Z-Image Turbo (int8) | 22.9 s | 17.4 s | 1.9 s (8 steps) |
133
+
134
+ ComfyUI uses whatever GPU memory is free to keep models loaded (11.3 to 11.5 GB here), so the
135
+ memory a model *needs* is lower than what `nvidia-smi` shows while it runs.
82
136
 
83
137
  ## One model on the GPU at a time
84
138
 
@@ -95,8 +149,7 @@ themselves:
95
149
 
96
150
  Video mode doesn't generate yet: a request there says what's coming instead of going anywhere.
97
151
 
98
- - **#150 Media model catalog:** image and video models that fit your machine, installed on
99
- demand.
152
+ - **#150 Media model catalog** (image models done; video models come with video generation).
100
153
  - **#151, #152 Image mode:** generating, variations, sizes and seeds, a gallery, editing,
101
154
  references, inpainting, upscaling.
102
155
  - **#153, #154 Video mode:** from text or a start image, first and last frames, several
@@ -12,7 +12,7 @@ lcode
12
12
 
13
13
  ```text
14
14
  ╭──────────────────────────────────────────────╮
15
- │ lcode v0.40.0 — local coding agent │
15
+ │ lcode v0.41.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)
@@ -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.41.0"
7
7
 
8
8
  API = ("Session", "Result", "Options", "SetupError")
9
9
 
@@ -308,6 +308,7 @@ 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)
311
312
  num_batch: int | None = None
312
313
  keep_alive: str = "30m"
313
314
  think: bool = True
@@ -687,6 +688,7 @@ class Agent:
687
688
  "cwd": str(self.workspace), # where `lcode -c` there finds it, wherever the shell moved
688
689
  "model": self.settings.model,
689
690
  "mode": self.settings.mode,
691
+ "image_model": self.settings.image_model,
690
692
  "name": self.session_name,
691
693
  "title": self.session_title,
692
694
  "messages": self.messages,
@@ -735,6 +737,8 @@ class Agent:
735
737
  self.session_name = data.get("name", "")
736
738
  if data.get("mode") in ("code", "image", "video"):
737
739
  self.settings.mode = data["mode"] # a session belongs to the mode it was made in
740
+ if isinstance(data.get("image_model"), str):
741
+ self.settings.image_model = data["image_model"]
738
742
  self.session_title = data.get("title") or sessions.title_from(self.messages)
739
743
  self.tools.read_mtimes.clear() # files may have changed since; the model must read them again
740
744
  self.tools.todos = list(data.get("todos") or [])
@@ -200,6 +200,7 @@ 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"],
203
204
  web=switch(options.web, cfg["web"], "on"),
204
205
  search_backend=cfg["search_backend"],
205
206
  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:
@@ -909,6 +909,13 @@ def build_subparsers() -> dict[str, argparse.ArgumentParser]:
909
909
  subs["setup"] = p
910
910
  subs["models"] = argparse.ArgumentParser(prog="lcode models", description="List models and how they fit here.")
911
911
  subs["doctor"] = argparse.ArgumentParser(prog="lcode doctor", description="Check the installation.")
912
+ p = argparse.ArgumentParser(
913
+ prog="lcode media", description="Image and video models: list them, with how they fit here, or install one."
914
+ )
915
+ p.add_argument("action", nargs="?", choices=["list", "install"], default="list")
916
+ p.add_argument("model", nargs="?", help="with install: a model key from the list, e.g. z-image-turbo")
917
+ p.add_argument("-y", "--yes", action="store_true", help="don't ask before downloading")
918
+ subs["media"] = p
912
919
  p = argparse.ArgumentParser(prog="lcode config", description="Show or change settings.")
913
920
  p.add_argument("action", nargs="?", choices=["show", "set", "unset", "path"], default="show")
914
921
  p.add_argument("key", nargs="?", choices=list(config.SETTINGS))
@@ -1065,6 +1072,55 @@ def cmd_index(args) -> None:
1065
1072
  )
1066
1073
 
1067
1074
 
1075
+ def cmd_media(args) -> None:
1076
+ """lcode media [list] | install <model>: the media catalog, what's installed in ComfyUI, downloads."""
1077
+ from rich.prompt import Confirm
1078
+
1079
+ from lcode import comfy, media
1080
+ from lcode import media_catalog as catalog
1081
+
1082
+ try:
1083
+ cfg = config.load()
1084
+ except ConfigError as e:
1085
+ fail(str(e))
1086
+ cat, hw = catalog.load(), detect()
1087
+ server = comfy.Server(cfg["comfyui_url"])
1088
+ install = comfy.find_install(cfg["comfyui_path"])
1089
+ running = server.stats() is not None
1090
+ installed = catalog.lister(server if running else None, install) if running or install else None
1091
+ if args.action == "install":
1092
+ model = cat.find(args.model or "")
1093
+ if model is None:
1094
+ fail(f"no media model {args.model!r}: lcode media lists them")
1095
+ if not running and install is None:
1096
+ fail(f"lcode needs ComfyUI to install models into. {comfy.install_hint()}")
1097
+ ask = None if args.yes else (lambda q: Confirm.ask(q, default=True, console=console))
1098
+ try:
1099
+ saved = catalog.install(model, server if running else None, install, console, ask)
1100
+ except catalog.CatalogError as e:
1101
+ fail(str(e))
1102
+ how = f" ({len(saved)} file(s) downloaded)." if saved else "."
1103
+ console.print(f"[green]{model.name} is installed[/]{how}")
1104
+ return
1105
+ where = (
1106
+ f"ComfyUI at {server.url}" if running else (f"{install.path} (not running)" if install else "no ComfyUI found")
1107
+ )
1108
+ console.print(f"[dim]{hw.describe()} · {where} · image model: {cfg['image_model']}[/]\n")
1109
+ for mode in ("image", "video"):
1110
+ models = cat.for_mode(mode)
1111
+ console.print(f"[bold]{mode.capitalize()} models[/]")
1112
+ if not models:
1113
+ console.print(f" [dim]{media.SOON[mode]}[/]\n")
1114
+ continue
1115
+ for m in models:
1116
+ line = escape(catalog.describe(m, hw, installed))
1117
+ console.print(f" [bold]{m.key:<16}[/] {escape(m.name)} [dim]· {line}[/]")
1118
+ if m.measured:
1119
+ console.print(f" {'':<16} [dim]{escape(m.measured)}[/]")
1120
+ console.print()
1121
+ console.print("[dim]Install one: lcode media install <model> · choose in image mode: /model <model>[/]")
1122
+
1123
+
1068
1124
  def cmd_mcp(args) -> None:
1069
1125
  from lcode.mcp.commands import run
1070
1126
 
@@ -1075,6 +1131,7 @@ COMMANDS = {
1075
1131
  "setup": cmd_setup,
1076
1132
  "models": cmd_models,
1077
1133
  "doctor": cmd_doctor,
1134
+ "media": cmd_media,
1078
1135
  "config": cmd_config,
1079
1136
  "bench": cmd_bench,
1080
1137
  "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_]+)$")
@@ -45,6 +45,7 @@ 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`"),
48
49
  "think": (True, bool, "let the model reason before answering (slower, better)"),
49
50
  "web": ("on", str, "web search and page fetching for the model: on | ask | off"),
50
51
  "search_backend": ("auto", str, "auto | ollama | brave | tavily | searxng (keys come from environment variables)"),
@@ -210,6 +211,12 @@ def coerce(key: str, value: object) -> object:
210
211
  raise ConfigError(f"permission_mode must be one of {', '.join(PERMISSION_MODES)}")
211
212
  if key == "mode" and value not in WORK_MODES:
212
213
  raise ConfigError(f"mode must be one of {', '.join(WORK_MODES)}")
214
+ if key == "image_model":
215
+ from lcode import media_catalog
216
+
217
+ keys = [m.key for m in media_catalog.load().for_mode("image")]
218
+ if value not in keys:
219
+ raise ConfigError(f"image_model must be one of {', '.join(keys)} (see lcode media)")
213
220
  if typ is bool and isinstance(value, str):
214
221
  if value.lower() not in ("true", "false", "1", "0", "yes", "no", "on", "off"):
215
222
  raise ConfigError(f"{key} must be true or false")
@@ -1,7 +1,7 @@
1
1
  """Image and video mode: a request becomes media, made by ComfyUI (lcode.comfy) with no language model in between.
2
2
 
3
- For now image mode makes one image per request with FLUX.2 klein 4B, the engine's first workflow; models, sizes,
4
- variations and a gallery come with #150 and #151, video with #153.
3
+ Image mode makes one image per request with the image model chosen from the media catalog (lcode.media_catalog);
4
+ sizes, variations and a gallery come with #151, video with #153.
5
5
  """
6
6
 
7
7
  from __future__ import annotations
@@ -16,11 +16,12 @@ from typing import TYPE_CHECKING
16
16
  from rich.text import Text
17
17
 
18
18
  from lcode import comfy
19
+ from lcode import media_catalog as catalog
19
20
 
20
21
  if TYPE_CHECKING:
21
22
  from lcode.agent import Agent
22
23
 
23
- IMAGE_WORKFLOW = "flux2-klein-4b-t2i"
24
+ SOON = {"video": "Video models come with video generation (#153)."}
24
25
  IGNORED = "# lcode's images and videos: not part of the project unless you add them\n*\n"
25
26
 
26
27
 
@@ -28,8 +29,19 @@ class MediaError(Exception):
28
29
  pass
29
30
 
30
31
 
31
- def image_model() -> str:
32
- return comfy.load_workflow(IMAGE_WORKFLOW).name
32
+ def installed_lister(agent: Agent) -> catalog.Installed | None:
33
+ """What ComfyUI has installed: asked from it when it's running, else read from its folders; None: unknown."""
34
+ server = comfy.Server(agent.settings.comfyui_url)
35
+ if server.stats():
36
+ return catalog.lister(server)
37
+ install = comfy.find_install(agent.settings.comfyui_path)
38
+ return catalog.lister(install=install) if install else None
39
+
40
+
41
+ def current(agent: Agent) -> catalog.MediaModel:
42
+ """The image model the agent uses (the image_model setting, or /model in image mode)."""
43
+ cat = catalog.load()
44
+ return cat.find(agent.settings.image_model) or cat.models[catalog.DEFAULT_IMAGE]
33
45
 
34
46
 
35
47
  @dataclass
@@ -183,6 +195,28 @@ class Line:
183
195
  return text
184
196
 
185
197
 
198
+ def ensure_installed(
199
+ agent: Agent, model: catalog.MediaModel, server: comfy.Server, ask: Callable[[str], bool] | None
200
+ ) -> dict[str, str]:
201
+ """The model's files (role -> installed name), downloading the missing ones first when the user agrees."""
202
+ files, missing = catalog.chosen_files(model, catalog.lister(server))
203
+ if not missing:
204
+ return files
205
+ size = sum(f.gb for f in missing)
206
+ if ask is None:
207
+ raise MediaError(f"{model.name} isn't installed ({size:.1f} GB to download): lcode media install {model.key}")
208
+ install = comfy.find_install(agent.settings.comfyui_path)
209
+ try:
210
+ catalog.install(model, server=server, install=install, console=agent.console, ask=ask)
211
+ except catalog.CatalogError as e:
212
+ raise MediaError(str(e)) from e
213
+ files, missing = catalog.chosen_files(model, catalog.lister(server))
214
+ if missing:
215
+ raise MediaError(f"ComfyUI doesn't see {', '.join(f.name for f in missing)} yet: restart it and try again")
216
+ agent.say(f"Installed {model.name}.")
217
+ return files
218
+
219
+
186
220
  def generate_image(
187
221
  agent: Agent, prompt: str, cancelled: Callable[[], bool] = lambda: False, ask: Callable[[str], bool] | None = None
188
222
  ) -> Made:
@@ -193,20 +227,17 @@ def generate_image(
193
227
  server = engine(agent, ask, cancelled)
194
228
  for path in comfy.collect(server):
195
229
  agent.say(f"Saved {shown(agent, path)}: it finished while lcode wasn't watching.")
196
- workflow = comfy.load_workflow(IMAGE_WORKFLOW)
197
- files, missing = comfy.choose_files(server, workflow)
198
- if missing:
199
- raise MediaError(
200
- f"{workflow.name} isn't installed in ComfyUI (missing {', '.join(missing)}). lcode installs image "
201
- "models from #150 on; until then put the files in ComfyUI's models folder."
202
- )
230
+ model = current(agent)
231
+ files = ensure_installed(agent, model, server, ask)
232
+ workflow = comfy.load_workflow(model.workflow)
203
233
  free_gpu(agent)
204
234
  seed = random.randrange(2**32)
205
235
  values = {**workflow.defaults, **files, "prompt": prompt, "seed": seed}
206
236
  graph = comfy.fill(workflow.graph, values)
207
237
  meta = {
208
238
  "prompt": prompt,
209
- "model": workflow.name,
239
+ "model": model.name,
240
+ "model_key": model.key,
210
241
  "workflow": workflow.key,
211
242
  "seed": seed,
212
243
  "width": values["width"],
@@ -217,7 +248,7 @@ def generate_image(
217
248
  }
218
249
  out = folder(agent)
219
250
  stem = f"{time.strftime('%Y%m%d-%H%M%S')}-{comfy.slug(prompt)}"
220
- line = Line(workflow.name, "Esc or Ctrl+C cancels" if agent.interactive else "")
251
+ line = Line(model.name, "Esc or Ctrl+C cancels" if agent.interactive else "")
221
252
  job = {"id": ""}
222
253
 
223
254
  def on_event(event: comfy.Event) -> None:
@@ -252,7 +283,7 @@ def generate_image(
252
283
  comfy.forget(prompt_id)
253
284
  if not saved:
254
285
  raise MediaError("ComfyUI finished but returned no image.")
255
- made = Made(saved, workflow.name, seed, values["width"], values["height"], time.monotonic() - line.started)
286
+ made = Made(saved, model.name, seed, values["width"], values["height"], time.monotonic() - line.started)
256
287
  for path in saved:
257
288
  agent.console.print(
258
289
  Text("◆ ", style="green")
@@ -0,0 +1,74 @@
1
+ # Image and video models lcode can run through ComfyUI (lcode.media_catalog).
2
+ #
3
+ # [files.<key>] one file in ComfyUI's model folders, listed once: models share text encoders and VAEs.
4
+ # folder ComfyUI's model folder (diffusion_models, text_encoders, vae, loras, …)
5
+ # name the file name there
6
+ # url where lcode downloads it from (Hugging Face; HF_TOKEN is used for gated files)
7
+ # gb its size in GB (10^9 bytes)
8
+ # alternatives other file names that do the same job: an installed one is used instead of downloading
9
+ #
10
+ # [models.<key>] one model.
11
+ # mode image or video
12
+ # tasks text-to-image, edit, inpaint, text-to-video, image-to-video, first-last-frame, reference-to-video, audio
13
+ # workflow lcode's workflow for it (lcode/workflows/<workflow>.json)
14
+ # files role -> file key; the workflow's $role placeholders get the file names
15
+ # gpu_gb GPU memory it needs, when known better than lcode's estimate (the largest file + 1.5 GB); ComfyUI
16
+ # fills whatever is free with cached models, so nvidia-smi's peak doesn't tell
17
+ # measured what it did on real hardware; models without it haven't been run yet
18
+
19
+ [files.flux-2-klein-4b-fp8]
20
+ folder = "diffusion_models"
21
+ name = "flux-2-klein-4b-fp8.safetensors"
22
+ url = "https://huggingface.co/black-forest-labs/FLUX.2-klein-4b-fp8/resolve/main/flux-2-klein-4b-fp8.safetensors"
23
+ gb = 4.07
24
+ alternatives = ["flux-2-klein-4b.safetensors"]
25
+
26
+ [files.qwen-3-4b]
27
+ folder = "text_encoders"
28
+ name = "qwen_3_4b.safetensors"
29
+ url = "https://huggingface.co/Comfy-Org/flux2-klein/resolve/main/split_files/text_encoders/qwen_3_4b.safetensors"
30
+ gb = 8.04
31
+ alternatives = ["qwen_3_4b_fp8_mixed.safetensors"]
32
+
33
+ [files.flux2-vae]
34
+ folder = "vae"
35
+ name = "flux2-vae.safetensors"
36
+ url = "https://huggingface.co/Comfy-Org/flux2-dev/resolve/main/split_files/vae/flux2-vae.safetensors"
37
+ gb = 0.34
38
+
39
+ [files.z-image-turbo-int8]
40
+ folder = "diffusion_models"
41
+ name = "z_image_turbo_int8_convrot.safetensors"
42
+ url = "https://huggingface.co/Comfy-Org/z_image_turbo/resolve/main/split_files/diffusion_models/z_image_turbo_int8_convrot.safetensors"
43
+ gb = 6.20
44
+ alternatives = ["z_image_turbo_bf16.safetensors"]
45
+
46
+ [files.flux-ae]
47
+ folder = "vae"
48
+ name = "ae.safetensors"
49
+ url = "https://huggingface.co/Comfy-Org/z_image_turbo/resolve/main/split_files/vae/ae.safetensors"
50
+ gb = 0.34
51
+
52
+ [models.flux2-klein-4b]
53
+ name = "FLUX.2 klein 4B"
54
+ mode = "image"
55
+ publisher = "Black Forest Labs"
56
+ license = "Apache 2.0"
57
+ tasks = ["text-to-image"]
58
+ workflow = "flux2-klein-4b-t2i"
59
+ files = { model = "flux-2-klein-4b-fp8", text_encoder = "qwen-3-4b", vae = "flux2-vae" }
60
+ gpu_gb = 0.0
61
+ measured = "1024×1024: 3.5 s per image, 14 s for the first with ComfyUI starting (RTX 4080 Laptop, 12 GB)"
62
+ notes = "fast and small: 4 steps"
63
+
64
+ [models.z-image-turbo]
65
+ name = "Z-Image Turbo"
66
+ mode = "image"
67
+ publisher = "Tongyi-MAI (Alibaba)"
68
+ license = "Apache 2.0"
69
+ tasks = ["text-to-image"]
70
+ workflow = "z-image-turbo-t2i"
71
+ files = { model = "z-image-turbo-int8", text_encoder = "qwen-3-4b", vae = "flux-ae" }
72
+ gpu_gb = 0.0
73
+ measured = "1024×1024: 17 s per image (1.9 s per step), 23 s for the first (RTX 4080 Laptop, 12 GB)"
74
+ notes = "photorealistic, writes text well: 8 steps"