lcode-cli 0.39.0__py3-none-any.whl → 0.41.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lcode/__init__.py +1 -1
- lcode/agent.py +8 -0
- lcode/api.py +41 -11
- lcode/cli.py +139 -9
- lcode/comfy.py +652 -0
- lcode/config.py +9 -0
- lcode/media.py +293 -0
- lcode/media.toml +74 -0
- lcode/media_catalog.py +331 -0
- lcode/modes.py +35 -13
- lcode/repl.py +99 -15
- lcode/workflows/__init__.py +0 -0
- lcode/workflows/flux2-klein-4b-t2i.json +31 -0
- lcode/workflows/z-image-turbo-t2i.json +31 -0
- {lcode_cli-0.39.0.dist-info → lcode_cli-0.41.0.dist-info}/METADATA +4 -1
- {lcode_cli-0.39.0.dist-info → lcode_cli-0.41.0.dist-info}/RECORD +19 -12
- {lcode_cli-0.39.0.dist-info → lcode_cli-0.41.0.dist-info}/WHEEL +0 -0
- {lcode_cli-0.39.0.dist-info → lcode_cli-0.41.0.dist-info}/entry_points.txt +0 -0
- {lcode_cli-0.39.0.dist-info → lcode_cli-0.41.0.dist-info}/licenses/LICENSE +0 -0
lcode/__init__.py
CHANGED
lcode/agent.py
CHANGED
|
@@ -306,6 +306,9 @@ class Settings:
|
|
|
306
306
|
context: int
|
|
307
307
|
mode: str = "code" # what lcode makes: code, image or video (lcode.modes)
|
|
308
308
|
comfyui_url: str = "http://127.0.0.1:8188"
|
|
309
|
+
comfyui_path: str | None = None # a ComfyUI install lcode doesn't find by itself (lcode.comfy)
|
|
310
|
+
media_dir: str = ".lcode/media" # where image and video mode save their files (lcode.media)
|
|
311
|
+
image_model: str = "flux2-klein-4b" # the media catalog's model image mode uses (lcode.media_catalog)
|
|
309
312
|
num_batch: int | None = None
|
|
310
313
|
keep_alive: str = "30m"
|
|
311
314
|
think: bool = True
|
|
@@ -370,6 +373,8 @@ class Agent:
|
|
|
370
373
|
self.session_name = ""
|
|
371
374
|
self.session_title = ""
|
|
372
375
|
self.interactive = True # False for `lcode -p`: no end-of-session questions
|
|
376
|
+
self.comfy_process = None # the ComfyUI lcode started for image and video mode, stopped when it exits
|
|
377
|
+
self.no_coding_model = "" # why there's no coding model, when lcode started in image or video mode without
|
|
373
378
|
self.reflected = 0 # messages before this index were already checked for notes to remember
|
|
374
379
|
self._memory: memory_notes.Memory | None = None
|
|
375
380
|
self._agent_types: tuple[Path, dict[str, subagents.AgentType], list[str]] | None = None
|
|
@@ -683,6 +688,7 @@ class Agent:
|
|
|
683
688
|
"cwd": str(self.workspace), # where `lcode -c` there finds it, wherever the shell moved
|
|
684
689
|
"model": self.settings.model,
|
|
685
690
|
"mode": self.settings.mode,
|
|
691
|
+
"image_model": self.settings.image_model,
|
|
686
692
|
"name": self.session_name,
|
|
687
693
|
"title": self.session_title,
|
|
688
694
|
"messages": self.messages,
|
|
@@ -731,6 +737,8 @@ class Agent:
|
|
|
731
737
|
self.session_name = data.get("name", "")
|
|
732
738
|
if data.get("mode") in ("code", "image", "video"):
|
|
733
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"]
|
|
734
742
|
self.session_title = data.get("title") or sessions.title_from(self.messages)
|
|
735
743
|
self.tools.read_mtimes.clear() # files may have changed since; the model must read them again
|
|
736
744
|
self.tools.todos = list(data.get("todos") or [])
|
lcode/api.py
CHANGED
|
@@ -15,6 +15,7 @@ A session is one conversation: each `run` continues it. `stream` yields the same
|
|
|
15
15
|
|
|
16
16
|
from __future__ import annotations
|
|
17
17
|
|
|
18
|
+
import contextlib
|
|
18
19
|
import io
|
|
19
20
|
import queue
|
|
20
21
|
import threading
|
|
@@ -130,6 +131,24 @@ def switch(wanted: bool | None, configured: str, default_on: str) -> str:
|
|
|
130
131
|
return configured if configured != "off" else default_on
|
|
131
132
|
|
|
132
133
|
|
|
134
|
+
def coding_model(ollama, options: Options, cfg: dict, requested: int | None, hw: Hardware, console: Console):
|
|
135
|
+
"""The coding model to use, its catalog entry and the context window; SetupError when there's none."""
|
|
136
|
+
from lcode import cli
|
|
137
|
+
|
|
138
|
+
check_server(ollama, hw)
|
|
139
|
+
try:
|
|
140
|
+
model, spec = cli.resolve_model(ollama, options.model or cfg["model"])
|
|
141
|
+
except cli.NotInstalled as e:
|
|
142
|
+
first_run = not options.model and cfg["model"] == config.DEFAULT_MODEL and not config.CONFIG_PATH.exists()
|
|
143
|
+
if first_run and not backends.is_openai(ollama):
|
|
144
|
+
raise SetupError("lcode isn't set up yet. Run: lcode setup") from e
|
|
145
|
+
raise SetupError(str(e)) from e
|
|
146
|
+
context, note = cli.choose_context(ollama, model, spec, requested, hw)
|
|
147
|
+
if note:
|
|
148
|
+
console.print(f"[yellow]Context {config.format_tokens(context)}: {note}[/]")
|
|
149
|
+
return model, spec, context
|
|
150
|
+
|
|
151
|
+
|
|
133
152
|
def open_agent(cwd: Path, options: Options, console: Console, hw: Hardware | None = None) -> Agent:
|
|
134
153
|
"""An agent set up the way `lcode` sets up a session: model, context, hooks, language servers, MCP."""
|
|
135
154
|
from lcode import catalog, cli, extensions, hooks
|
|
@@ -153,17 +172,14 @@ def open_agent(cwd: Path, options: Options, console: Console, hw: Hardware | Non
|
|
|
153
172
|
except OllamaError as e:
|
|
154
173
|
raise SetupError(str(e)) from e
|
|
155
174
|
hw = hw or detect()
|
|
156
|
-
|
|
175
|
+
no_coding_model = "" # why, when image or video mode starts without one
|
|
157
176
|
try:
|
|
158
|
-
model, spec =
|
|
159
|
-
except
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
context, note = cli.choose_context(ollama, model, spec, requested, hw)
|
|
165
|
-
if note:
|
|
166
|
-
console.print(f"[yellow]Context {config.format_tokens(context)}: {note}[/]")
|
|
177
|
+
model, spec, context = coding_model(ollama, options, cfg, requested, hw, console)
|
|
178
|
+
except SetupError as e:
|
|
179
|
+
if (options.mode or cfg["mode"]) == "code":
|
|
180
|
+
raise
|
|
181
|
+
# Image and video mode don't need the coding model: start without it, and say so on /mode code.
|
|
182
|
+
model, spec, context, no_coding_model = options.model or cfg["model"], None, requested or 32768, str(e)
|
|
167
183
|
num_batch = cfg["num_batch"]
|
|
168
184
|
if num_batch is None and spec and spec.num_batch:
|
|
169
185
|
if model == spec.local_name:
|
|
@@ -182,6 +198,9 @@ def open_agent(cwd: Path, options: Options, console: Console, hw: Hardware | Non
|
|
|
182
198
|
permission_mode=options.permission_mode or cfg["permission_mode"],
|
|
183
199
|
mode=options.mode or cfg["mode"],
|
|
184
200
|
comfyui_url=cfg["comfyui_url"],
|
|
201
|
+
comfyui_path=cfg["comfyui_path"],
|
|
202
|
+
media_dir=cfg["media_dir"],
|
|
203
|
+
image_model=cfg["image_model"],
|
|
185
204
|
web=switch(options.web, cfg["web"], "on"),
|
|
186
205
|
search_backend=cfg["search_backend"],
|
|
187
206
|
searxng_url=cfg["searxng_url"],
|
|
@@ -211,6 +230,7 @@ def open_agent(cwd: Path, options: Options, console: Console, hw: Hardware | Non
|
|
|
211
230
|
)
|
|
212
231
|
agent = Agent(ollama, settings, cwd, console=console)
|
|
213
232
|
agent.hardware = hw
|
|
233
|
+
agent.no_coding_model = no_coding_model
|
|
214
234
|
agent.autosave = True
|
|
215
235
|
agent.interactive = options.interactive
|
|
216
236
|
if options.max_steps:
|
|
@@ -250,15 +270,25 @@ def open_agent(cwd: Path, options: Options, console: Console, hw: Hardware | Non
|
|
|
250
270
|
|
|
251
271
|
|
|
252
272
|
def close_agent(agent: Agent) -> None:
|
|
273
|
+
"""Stop what lcode started (background jobs, servers, the sandbox, ComfyUI), then say so: the terminal may be
|
|
274
|
+
gone already (closed), and saying it then fails."""
|
|
275
|
+
from lcode import media
|
|
276
|
+
|
|
277
|
+
said = []
|
|
253
278
|
stopped = agent.jobs.stop_all()
|
|
254
279
|
if stopped:
|
|
255
|
-
|
|
280
|
+
said.append(f"Stopped {stopped} background job(s).")
|
|
281
|
+
if media.stop_engine(agent):
|
|
282
|
+
said.append("Stopped ComfyUI.")
|
|
256
283
|
if agent.lsp is not None:
|
|
257
284
|
agent.lsp.close()
|
|
258
285
|
if agent.mcp:
|
|
259
286
|
agent.mcp.close()
|
|
260
287
|
if agent.sandbox:
|
|
261
288
|
agent.sandbox.stop()
|
|
289
|
+
for line in said:
|
|
290
|
+
with contextlib.suppress(OSError):
|
|
291
|
+
agent.console.print(f"[dim]{line}[/]")
|
|
262
292
|
|
|
263
293
|
|
|
264
294
|
# ----------------------------------------------------------------------------- running
|
lcode/cli.py
CHANGED
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import argparse
|
|
6
|
+
import contextlib
|
|
6
7
|
import platform
|
|
7
8
|
import re
|
|
8
9
|
import shutil
|
|
10
|
+
import signal
|
|
9
11
|
import sys
|
|
10
12
|
import tempfile
|
|
11
13
|
import time
|
|
@@ -466,6 +468,7 @@ def cmd_doctor(args) -> None:
|
|
|
466
468
|
except ValueError as e:
|
|
467
469
|
line("MCP", str(e), False)
|
|
468
470
|
ok = False
|
|
471
|
+
line("ComfyUI", *comfyui_status(cfg))
|
|
469
472
|
if cfg["sandbox"] == "off":
|
|
470
473
|
line("Sandbox", "off (lcode config set sandbox docker)", True)
|
|
471
474
|
else:
|
|
@@ -733,17 +736,10 @@ def cmd_chat(args) -> None:
|
|
|
733
736
|
print(json.dumps({"type": "result", "status": "error", "error": str(e)}), flush=True)
|
|
734
737
|
sys.exit(1)
|
|
735
738
|
fail(str(e))
|
|
739
|
+
exit_on_hangup()
|
|
736
740
|
exit_code = 0
|
|
737
741
|
if args.prompt and agent.settings.mode != "code":
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
message = modes.COMING[agent.settings.mode]
|
|
741
|
-
if quiet:
|
|
742
|
-
print(json.dumps({"type": "result", "status": "error", "error": message}), flush=True)
|
|
743
|
-
else:
|
|
744
|
-
out.print(f"[yellow]{escape(message)}[/]")
|
|
745
|
-
api.close_agent(agent)
|
|
746
|
-
sys.exit(2)
|
|
742
|
+
media_request(agent, args.prompt, out, quiet) # makes the image or video, then exits
|
|
747
743
|
if args.escalate and args.prompt:
|
|
748
744
|
agent.escalate_approved = True # the flag is the user's OK for this run
|
|
749
745
|
try:
|
|
@@ -765,6 +761,83 @@ def cmd_chat(args) -> None:
|
|
|
765
761
|
sys.exit(exit_code)
|
|
766
762
|
|
|
767
763
|
|
|
764
|
+
def exit_on_hangup() -> None:
|
|
765
|
+
"""A closed terminal (SIGHUP) or a plain kill (SIGTERM) ends lcode the usual way: it saves the session and stops
|
|
766
|
+
what it started (ComfyUI, MCP servers, background jobs) instead of leaving them running."""
|
|
767
|
+
|
|
768
|
+
def leave(signum, frame):
|
|
769
|
+
raise SystemExit(128 + signum)
|
|
770
|
+
|
|
771
|
+
for name in ("SIGHUP", "SIGTERM"):
|
|
772
|
+
sig = getattr(signal, name, None)
|
|
773
|
+
if sig is not None:
|
|
774
|
+
with contextlib.suppress(ValueError, OSError): # only the main thread can
|
|
775
|
+
signal.signal(sig, leave)
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
def comfyui_status(cfg: dict) -> tuple[str, bool | None]:
|
|
779
|
+
"""lcode doctor's ComfyUI line: running or found, and whether the image model is there."""
|
|
780
|
+
from lcode import comfy
|
|
781
|
+
from lcode import media_catalog as catalog
|
|
782
|
+
|
|
783
|
+
model = catalog.load().find(cfg.get("image_model") or "") or catalog.load().models[catalog.DEFAULT_IMAGE]
|
|
784
|
+
server = comfy.Server(cfg["comfyui_url"])
|
|
785
|
+
stats = server.stats()
|
|
786
|
+
install = None if stats else comfy.find_install(cfg["comfyui_path"])
|
|
787
|
+
if not stats and install is None:
|
|
788
|
+
return "not found: image mode offers to install it (or lcode config set comfyui_path <folder>)", None
|
|
789
|
+
if stats:
|
|
790
|
+
where = f"{(stats.get('system') or {}).get('comfyui_version', '?')} running at {server.url}"
|
|
791
|
+
else:
|
|
792
|
+
where = f"{install.path} (lcode starts it in image mode)"
|
|
793
|
+
_, missing = catalog.chosen_files(model, catalog.lister(server if stats else None, install))
|
|
794
|
+
if missing:
|
|
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
|
+
|
|
799
|
+
|
|
800
|
+
def media_request(agent, prompt: str, out: Console, quiet: bool) -> None:
|
|
801
|
+
"""`lcode --mode image -p "…"`: make the image, say where it went (or, with --output json, a result object)."""
|
|
802
|
+
import json
|
|
803
|
+
|
|
804
|
+
from lcode import api, media, modes
|
|
805
|
+
|
|
806
|
+
if agent.settings.mode == "video":
|
|
807
|
+
message = modes.COMING["video"]
|
|
808
|
+
if quiet:
|
|
809
|
+
print(json.dumps({"type": "result", "status": "error", "error": message}), flush=True)
|
|
810
|
+
else:
|
|
811
|
+
out.print(f"[yellow]{escape(message)}[/]")
|
|
812
|
+
api.close_agent(agent)
|
|
813
|
+
sys.exit(2)
|
|
814
|
+
try:
|
|
815
|
+
made = media.generate_image(agent, prompt)
|
|
816
|
+
except (media.MediaError, KeyboardInterrupt) as e:
|
|
817
|
+
message = str(e) or "Cancelled."
|
|
818
|
+
if quiet:
|
|
819
|
+
print(json.dumps({"type": "result", "status": "error", "error": message}), flush=True)
|
|
820
|
+
else:
|
|
821
|
+
out.print(f"[red]{escape(message)}[/]")
|
|
822
|
+
api.close_agent(agent)
|
|
823
|
+
sys.exit(1)
|
|
824
|
+
if quiet:
|
|
825
|
+
result = {
|
|
826
|
+
"type": "result",
|
|
827
|
+
"status": "success",
|
|
828
|
+
"mode": "image",
|
|
829
|
+
"files": [str(path) for path in made.files],
|
|
830
|
+
"model": made.model,
|
|
831
|
+
"seed": made.seed,
|
|
832
|
+
"width": made.width,
|
|
833
|
+
"height": made.height,
|
|
834
|
+
"seconds": round(made.seconds, 1),
|
|
835
|
+
}
|
|
836
|
+
print(json.dumps(result, ensure_ascii=False), flush=True)
|
|
837
|
+
api.close_agent(agent)
|
|
838
|
+
sys.exit(0)
|
|
839
|
+
|
|
840
|
+
|
|
768
841
|
def compact_tokens(value: str) -> int:
|
|
769
842
|
"""`--compact-at`: a token count such as 64k, or 0."""
|
|
770
843
|
try:
|
|
@@ -836,6 +909,13 @@ def build_subparsers() -> dict[str, argparse.ArgumentParser]:
|
|
|
836
909
|
subs["setup"] = p
|
|
837
910
|
subs["models"] = argparse.ArgumentParser(prog="lcode models", description="List models and how they fit here.")
|
|
838
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
|
|
839
919
|
p = argparse.ArgumentParser(prog="lcode config", description="Show or change settings.")
|
|
840
920
|
p.add_argument("action", nargs="?", choices=["show", "set", "unset", "path"], default="show")
|
|
841
921
|
p.add_argument("key", nargs="?", choices=list(config.SETTINGS))
|
|
@@ -992,6 +1072,55 @@ def cmd_index(args) -> None:
|
|
|
992
1072
|
)
|
|
993
1073
|
|
|
994
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
|
+
|
|
995
1124
|
def cmd_mcp(args) -> None:
|
|
996
1125
|
from lcode.mcp.commands import run
|
|
997
1126
|
|
|
@@ -1002,6 +1131,7 @@ COMMANDS = {
|
|
|
1002
1131
|
"setup": cmd_setup,
|
|
1003
1132
|
"models": cmd_models,
|
|
1004
1133
|
"doctor": cmd_doctor,
|
|
1134
|
+
"media": cmd_media,
|
|
1005
1135
|
"config": cmd_config,
|
|
1006
1136
|
"bench": cmd_bench,
|
|
1007
1137
|
"mcp": cmd_mcp,
|