comfyui-skill-cli 0.2.4__tar.gz → 0.2.5__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 (35) hide show
  1. {comfyui_skill_cli-0.2.4 → comfyui_skill_cli-0.2.5}/PKG-INFO +1 -1
  2. {comfyui_skill_cli-0.2.4 → comfyui_skill_cli-0.2.5}/comfyui_skill_cli.egg-info/PKG-INFO +1 -1
  3. {comfyui_skill_cli-0.2.4 → comfyui_skill_cli-0.2.5}/comfyui_skill_cli.egg-info/SOURCES.txt +5 -0
  4. comfyui_skill_cli-0.2.5/comfyui_skills_cli/__init__.py +1 -0
  5. {comfyui_skill_cli-0.2.4 → comfyui_skill_cli-0.2.5}/comfyui_skills_cli/client.py +36 -3
  6. comfyui_skill_cli-0.2.5/comfyui_skills_cli/commands/cancel.py +51 -0
  7. comfyui_skill_cli-0.2.5/comfyui_skills_cli/commands/free.py +46 -0
  8. comfyui_skill_cli-0.2.5/comfyui_skills_cli/commands/queue.py +82 -0
  9. {comfyui_skill_cli-0.2.4 → comfyui_skill_cli-0.2.5}/comfyui_skills_cli/commands/run.py +9 -2
  10. {comfyui_skill_cli-0.2.4 → comfyui_skill_cli-0.2.5}/comfyui_skills_cli/commands/upload.py +6 -6
  11. comfyui_skill_cli-0.2.5/comfyui_skills_cli/error_hints.py +43 -0
  12. {comfyui_skill_cli-0.2.4 → comfyui_skill_cli-0.2.5}/comfyui_skills_cli/main.py +5 -2
  13. {comfyui_skill_cli-0.2.4 → comfyui_skill_cli-0.2.5}/pyproject.toml +1 -1
  14. comfyui_skill_cli-0.2.5/tests/test_client_new_apis.py +150 -0
  15. comfyui_skill_cli-0.2.4/comfyui_skills_cli/__init__.py +0 -1
  16. {comfyui_skill_cli-0.2.4 → comfyui_skill_cli-0.2.5}/LICENSE +0 -0
  17. {comfyui_skill_cli-0.2.4 → comfyui_skill_cli-0.2.5}/README.md +0 -0
  18. {comfyui_skill_cli-0.2.4 → comfyui_skill_cli-0.2.5}/comfyui_skill_cli.egg-info/dependency_links.txt +0 -0
  19. {comfyui_skill_cli-0.2.4 → comfyui_skill_cli-0.2.5}/comfyui_skill_cli.egg-info/entry_points.txt +0 -0
  20. {comfyui_skill_cli-0.2.4 → comfyui_skill_cli-0.2.5}/comfyui_skill_cli.egg-info/requires.txt +0 -0
  21. {comfyui_skill_cli-0.2.4 → comfyui_skill_cli-0.2.5}/comfyui_skill_cli.egg-info/top_level.txt +0 -0
  22. {comfyui_skill_cli-0.2.4 → comfyui_skill_cli-0.2.5}/comfyui_skills_cli/__main__.py +0 -0
  23. {comfyui_skill_cli-0.2.4 → comfyui_skill_cli-0.2.5}/comfyui_skills_cli/commands/__init__.py +0 -0
  24. {comfyui_skill_cli-0.2.4 → comfyui_skill_cli-0.2.5}/comfyui_skills_cli/commands/config.py +0 -0
  25. {comfyui_skill_cli-0.2.4 → comfyui_skill_cli-0.2.5}/comfyui_skills_cli/commands/deps.py +0 -0
  26. {comfyui_skill_cli-0.2.4 → comfyui_skill_cli-0.2.5}/comfyui_skills_cli/commands/history.py +0 -0
  27. {comfyui_skill_cli-0.2.4 → comfyui_skill_cli-0.2.5}/comfyui_skills_cli/commands/server.py +0 -0
  28. {comfyui_skill_cli-0.2.4 → comfyui_skill_cli-0.2.5}/comfyui_skills_cli/commands/skill.py +0 -0
  29. {comfyui_skill_cli-0.2.4 → comfyui_skill_cli-0.2.5}/comfyui_skills_cli/commands/workflow.py +0 -0
  30. {comfyui_skill_cli-0.2.4 → comfyui_skill_cli-0.2.5}/comfyui_skills_cli/config.py +0 -0
  31. {comfyui_skill_cli-0.2.4 → comfyui_skill_cli-0.2.5}/comfyui_skills_cli/output.py +0 -0
  32. {comfyui_skill_cli-0.2.4 → comfyui_skill_cli-0.2.5}/comfyui_skills_cli/storage.py +0 -0
  33. {comfyui_skill_cli-0.2.4 → comfyui_skill_cli-0.2.5}/comfyui_skills_cli/update_check.py +0 -0
  34. {comfyui_skill_cli-0.2.4 → comfyui_skill_cli-0.2.5}/setup.cfg +0 -0
  35. {comfyui_skill_cli-0.2.4 → comfyui_skill_cli-0.2.5}/tests/test_update_check.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: comfyui-skill-cli
3
- Version: 0.2.4
3
+ Version: 0.2.5
4
4
  Summary: ComfyUI Skill CLI — Agent-friendly workflow management
5
5
  Author: HuangYuChuh
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: comfyui-skill-cli
3
- Version: 0.2.4
3
+ Version: 0.2.5
4
4
  Summary: ComfyUI Skill CLI — Agent-friendly workflow management
5
5
  Author: HuangYuChuh
6
6
  License-Expression: MIT
@@ -11,17 +11,22 @@ comfyui_skills_cli/__init__.py
11
11
  comfyui_skills_cli/__main__.py
12
12
  comfyui_skills_cli/client.py
13
13
  comfyui_skills_cli/config.py
14
+ comfyui_skills_cli/error_hints.py
14
15
  comfyui_skills_cli/main.py
15
16
  comfyui_skills_cli/output.py
16
17
  comfyui_skills_cli/storage.py
17
18
  comfyui_skills_cli/update_check.py
18
19
  comfyui_skills_cli/commands/__init__.py
20
+ comfyui_skills_cli/commands/cancel.py
19
21
  comfyui_skills_cli/commands/config.py
20
22
  comfyui_skills_cli/commands/deps.py
23
+ comfyui_skills_cli/commands/free.py
21
24
  comfyui_skills_cli/commands/history.py
25
+ comfyui_skills_cli/commands/queue.py
22
26
  comfyui_skills_cli/commands/run.py
23
27
  comfyui_skills_cli/commands/server.py
24
28
  comfyui_skills_cli/commands/skill.py
25
29
  comfyui_skills_cli/commands/upload.py
26
30
  comfyui_skills_cli/commands/workflow.py
31
+ tests/test_client_new_apis.py
27
32
  tests/test_update_check.py
@@ -0,0 +1 @@
1
+ __version__ = "0.2.5"
@@ -126,13 +126,43 @@ class ComfyUIClient:
126
126
  except (requests.RequestException, ValueError):
127
127
  return None
128
128
 
129
- # -- Image upload --
129
+ # -- Queue management --
130
130
 
131
- def upload_image(self, filepath: str) -> dict[str, Any]:
131
+ def interrupt(self, prompt_id: str = "") -> dict[str, Any]:
132
+ payload = {"prompt_id": prompt_id} if prompt_id else {}
133
+ resp = self._post("/interrupt", json_data=payload)
134
+ resp.raise_for_status()
135
+ return {"success": True}
136
+
137
+ def queue_delete(self, prompt_ids: list[str]) -> dict[str, Any]:
138
+ resp = self._post("/queue", json_data={"delete": prompt_ids})
139
+ resp.raise_for_status()
140
+ return {"success": True}
141
+
142
+ def queue_clear(self) -> dict[str, Any]:
143
+ resp = self._post("/queue", json_data={"clear": True})
144
+ resp.raise_for_status()
145
+ return {"success": True}
146
+
147
+ # -- Memory management --
148
+
149
+ def free_memory(self, unload_models: bool = False, free_memory: bool = False) -> dict[str, Any]:
150
+ payload: dict[str, Any] = {}
151
+ if unload_models:
152
+ payload["unload_models"] = True
153
+ if free_memory:
154
+ payload["free_memory"] = True
155
+ resp = self._post("/free", json_data=payload)
156
+ resp.raise_for_status()
157
+ return {"success": True}
158
+
159
+ # -- File upload --
160
+
161
+ def upload_file(self, filepath: str) -> dict[str, Any]:
132
162
  import mimetypes
133
163
  import os
134
164
  filename = os.path.basename(filepath)
135
- content_type = mimetypes.guess_type(filepath)[0] or "image/png"
165
+ content_type = mimetypes.guess_type(filepath)[0] or "application/octet-stream"
136
166
  with open(filepath, "rb") as f:
137
167
  content = f.read()
138
168
 
@@ -154,6 +184,9 @@ class ComfyUIClient:
154
184
  resp.raise_for_status()
155
185
  return resp.json()
156
186
 
187
+ def upload_image(self, filepath: str) -> dict[str, Any]:
188
+ return self.upload_file(filepath)
189
+
157
190
  # -- ComfyUI Userdata API --
158
191
 
159
192
  def list_userdata_workflows(self) -> list[str]:
@@ -0,0 +1,51 @@
1
+ """comfyui-skill cancel — cancel a running or queued job."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+
7
+ from ..client import ComfyUIClient
8
+ from ..config import get_base_dir, get_default_server_id, get_server, load_config
9
+ from ..output import output_error, output_result
10
+
11
+
12
+ def cancel_cmd(
13
+ ctx: typer.Context,
14
+ prompt_id: str = typer.Argument(help="Prompt ID to cancel"),
15
+ ):
16
+ """Cancel a running or queued job. Interrupts if running, removes from queue if pending."""
17
+ base_dir = get_base_dir(ctx.obj.get("base_dir", ""))
18
+ config = load_config(base_dir)
19
+ server_id = ctx.obj.get("server") or get_default_server_id(config)
20
+ server_config = get_server(config, server_id)
21
+
22
+ if not server_config:
23
+ output_error(ctx, "SERVER_NOT_FOUND", f'Server "{server_id}" not found.')
24
+ return
25
+
26
+ client = ComfyUIClient(
27
+ server_url=server_config.get("url", "http://127.0.0.1:8188"),
28
+ auth=server_config.get("auth", ""),
29
+ )
30
+
31
+ try:
32
+ queue = client.get_queue()
33
+
34
+ # Check if running
35
+ for item in queue.get("queue_running", []):
36
+ if len(item) > 1 and item[1] == prompt_id:
37
+ client.interrupt(prompt_id)
38
+ output_result(ctx, {"status": "interrupted", "prompt_id": prompt_id})
39
+ return
40
+
41
+ # Check if pending
42
+ for item in queue.get("queue_pending", []):
43
+ if len(item) > 1 and item[1] == prompt_id:
44
+ client.queue_delete([prompt_id])
45
+ output_result(ctx, {"status": "removed", "prompt_id": prompt_id})
46
+ return
47
+
48
+ output_result(ctx, {"status": "not_found", "prompt_id": prompt_id})
49
+
50
+ except Exception as exc:
51
+ output_error(ctx, "CANCEL_FAILED", f"Failed to cancel job: {exc}")
@@ -0,0 +1,46 @@
1
+ """comfyui-skill free — release GPU memory and unload models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+
7
+ from ..client import ComfyUIClient
8
+ from ..config import get_base_dir, get_default_server_id, get_server, load_config
9
+ from ..output import output_error, output_result
10
+
11
+
12
+ def free_cmd(
13
+ ctx: typer.Context,
14
+ models: bool = typer.Option(False, "--models", "-m", help="Unload all models from VRAM"),
15
+ memory: bool = typer.Option(False, "--memory", help="Free all cached memory"),
16
+ ):
17
+ """Release GPU memory. With no flags, unloads models and frees memory."""
18
+ base_dir = get_base_dir(ctx.obj.get("base_dir", ""))
19
+ config = load_config(base_dir)
20
+ server_id = ctx.obj.get("server") or get_default_server_id(config)
21
+ server_config = get_server(config, server_id)
22
+
23
+ if not server_config:
24
+ output_error(ctx, "SERVER_NOT_FOUND", f'Server "{server_id}" not found.')
25
+ return
26
+
27
+ client = ComfyUIClient(
28
+ server_url=server_config.get("url", "http://127.0.0.1:8188"),
29
+ auth=server_config.get("auth", ""),
30
+ )
31
+
32
+ # Default: both if no flag specified
33
+ if not models and not memory:
34
+ models = True
35
+ memory = True
36
+
37
+ try:
38
+ client.free_memory(unload_models=models, free_memory=memory)
39
+ actions = []
40
+ if models:
41
+ actions.append("models_unloaded")
42
+ if memory:
43
+ actions.append("memory_freed")
44
+ output_result(ctx, {"status": "ok", "actions": actions, "server_id": server_id})
45
+ except Exception as exc:
46
+ output_error(ctx, "FREE_FAILED", f"Failed to free memory: {exc}")
@@ -0,0 +1,82 @@
1
+ """comfyui-skill queue — view and manage the ComfyUI execution queue."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import List
6
+
7
+ import typer
8
+
9
+ from ..client import ComfyUIClient
10
+ from ..config import get_base_dir, get_default_server_id, get_server, load_config
11
+ from ..output import output_error, output_result
12
+
13
+ app = typer.Typer(no_args_is_help=True)
14
+
15
+
16
+ def _build_client(ctx: typer.Context) -> tuple[ComfyUIClient, str]:
17
+ base_dir = get_base_dir(ctx.obj.get("base_dir", ""))
18
+ config = load_config(base_dir)
19
+ server_id = ctx.obj.get("server") or get_default_server_id(config)
20
+ server_config = get_server(config, server_id)
21
+
22
+ if not server_config:
23
+ output_error(ctx, "SERVER_NOT_FOUND", f'Server "{server_id}" not found.')
24
+
25
+ client = ComfyUIClient(
26
+ server_url=server_config.get("url", "http://127.0.0.1:8188"),
27
+ auth=server_config.get("auth", ""),
28
+ )
29
+ return client, server_id
30
+
31
+
32
+ @app.command("list")
33
+ def queue_list(ctx: typer.Context):
34
+ """Show running and pending jobs in the queue."""
35
+ client, server_id = _build_client(ctx)
36
+
37
+ try:
38
+ queue = client.get_queue()
39
+ running = [
40
+ {"prompt_id": item[1], "status": "running"}
41
+ for item in queue.get("queue_running", [])
42
+ if len(item) > 1
43
+ ]
44
+ pending = [
45
+ {"prompt_id": item[1], "status": "pending", "position": i}
46
+ for i, item in enumerate(queue.get("queue_pending", []))
47
+ if len(item) > 1
48
+ ]
49
+ output_result(ctx, {
50
+ "server_id": server_id,
51
+ "running": running,
52
+ "pending": pending,
53
+ })
54
+ except Exception as exc:
55
+ output_error(ctx, "QUEUE_FAILED", f"Failed to get queue: {exc}")
56
+
57
+
58
+ @app.command("clear")
59
+ def queue_clear(ctx: typer.Context):
60
+ """Clear all pending jobs from the queue (does not stop running jobs)."""
61
+ client, server_id = _build_client(ctx)
62
+
63
+ try:
64
+ client.queue_clear()
65
+ output_result(ctx, {"status": "cleared", "server_id": server_id})
66
+ except Exception as exc:
67
+ output_error(ctx, "QUEUE_FAILED", f"Failed to clear queue: {exc}")
68
+
69
+
70
+ @app.command("delete")
71
+ def queue_delete(
72
+ ctx: typer.Context,
73
+ prompt_ids: List[str] = typer.Argument(help="Prompt IDs to remove from queue"),
74
+ ):
75
+ """Remove specific jobs from the pending queue."""
76
+ client, server_id = _build_client(ctx)
77
+
78
+ try:
79
+ client.queue_delete(prompt_ids)
80
+ output_result(ctx, {"status": "deleted", "prompt_ids": prompt_ids, "server_id": server_id})
81
+ except Exception as exc:
82
+ output_error(ctx, "QUEUE_FAILED", f"Failed to delete from queue: {exc}")
@@ -18,6 +18,7 @@ import typer
18
18
  from ..client import ComfyUIClient
19
19
  from ..config import get_base_dir, get_default_server_id, get_server, load_config
20
20
  from ..output import OutputFormat, get_output_format, is_machine_mode, output_error, output_event, output_result
21
+ from ..error_hints import match_error_hint
21
22
  from ..storage import get_schema, get_workflow_data
22
23
 
23
24
  _POLL_INITIAL = 1.0
@@ -85,8 +86,9 @@ def run_cmd(
85
86
 
86
87
  if status_info.get("status_str") == "error":
87
88
  error_msg = _format_errors(history)
89
+ hint = match_error_hint(error_msg)
88
90
  output_event(ctx, "error", prompt_id=prompt_id, message=error_msg)
89
- output_error(ctx, "EXECUTION_FAILED", error_msg)
91
+ output_error(ctx, "EXECUTION_FAILED", error_msg, hint=hint)
90
92
  return
91
93
 
92
94
  # Check queue position
@@ -166,7 +168,12 @@ def status_cmd(
166
168
  output_result(ctx, {"status": "success", "prompt_id": prompt_id, "outputs": collected})
167
169
  return
168
170
  if status_info.get("status_str") == "error":
169
- output_result(ctx, {"status": "error", "prompt_id": prompt_id, "error": _format_errors(history)})
171
+ error_msg = _format_errors(history)
172
+ hint = match_error_hint(error_msg)
173
+ result: dict[str, Any] = {"status": "error", "prompt_id": prompt_id, "error": error_msg}
174
+ if hint:
175
+ result["hint"] = hint
176
+ output_result(ctx, result)
170
177
  return
171
178
 
172
179
  # Check queue
@@ -1,4 +1,4 @@
1
- """comfyui-skill upload — upload images to ComfyUI server."""
1
+ """comfyui-skill upload — upload files to ComfyUI server."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
@@ -13,11 +13,11 @@ from ..output import output_error, output_result
13
13
 
14
14
  def upload_cmd(
15
15
  ctx: typer.Context,
16
- image_path: str = typer.Argument(help="Path to image file"),
16
+ file_path: str = typer.Argument(help="Path to file (image, mask, audio, etc.)"),
17
17
  ):
18
- """Upload an image to ComfyUI for use in workflows."""
19
- if not os.path.isfile(image_path):
20
- output_error(ctx, "FILE_NOT_FOUND", f'Image file not found: "{image_path}"')
18
+ """Upload a file to ComfyUI for use in workflows (e.g., images, masks, audio)."""
19
+ if not os.path.isfile(file_path):
20
+ output_error(ctx, "FILE_NOT_FOUND", f'File not found: "{file_path}"')
21
21
  return
22
22
 
23
23
  base_dir = get_base_dir(ctx.obj.get("base_dir", ""))
@@ -35,7 +35,7 @@ def upload_cmd(
35
35
  )
36
36
 
37
37
  try:
38
- result = client.upload_image(image_path)
38
+ result = client.upload_file(file_path)
39
39
  output_result(ctx, {
40
40
  "name": result.get("name", ""),
41
41
  "subfolder": result.get("subfolder", ""),
@@ -0,0 +1,43 @@
1
+ """Common error pattern matching — maps ComfyUI errors to actionable hints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ # Common error patterns → actionable hints
6
+ _ERROR_HINTS: list[tuple[str, str]] = [
7
+ (
8
+ "Unauthorized: Please login first",
9
+ "This workflow uses ComfyUI cloud API nodes. "
10
+ "Configure a ComfyUI API Key: (1) Go to https://platform.comfy.org to generate a key, "
11
+ '(2) Add it to server settings via Web UI or config.json "comfy_api_key" field.',
12
+ ),
13
+ (
14
+ "ckpt",
15
+ "A required model file is missing. "
16
+ "Run `comfyui-skill deps check <workflow_id>` to identify missing models.",
17
+ ),
18
+ (
19
+ "safetensors",
20
+ "A required model file is missing. "
21
+ "Run `comfyui-skill deps check <workflow_id>` to identify missing models.",
22
+ ),
23
+ (
24
+ "Connection refused",
25
+ "ComfyUI server is not running. Start ComfyUI and try again.",
26
+ ),
27
+ (
28
+ "CUDA out of memory",
29
+ "GPU memory exhausted. Run `comfyui-skill free` to release VRAM, then retry.",
30
+ ),
31
+ (
32
+ "MPS out of memory",
33
+ "GPU memory exhausted. Run `comfyui-skill free` to release VRAM, then retry.",
34
+ ),
35
+ ]
36
+
37
+
38
+ def match_error_hint(error_msg: str) -> str:
39
+ lower = error_msg.lower()
40
+ for pattern, hint in _ERROR_HINTS:
41
+ if pattern.lower() in lower:
42
+ return hint
43
+ return ""
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  import typer
6
6
 
7
7
  from . import __version__
8
- from .commands import config, deps, history, run, server, skill, upload, workflow
8
+ from .commands import cancel, config, deps, free, history, queue, run, server, skill, upload, workflow
9
9
  from .update_check import is_machine_output, maybe_notify_update
10
10
 
11
11
  app = typer.Typer(
@@ -49,7 +49,7 @@ def main(
49
49
 
50
50
 
51
51
  # Subcommand groups — each needs a callback to inherit parent context
52
- for sub_app in [config.app, deps.app, history.app, server.app, workflow.app]:
52
+ for sub_app in [config.app, deps.app, history.app, queue.app, server.app, workflow.app]:
53
53
  @sub_app.callback()
54
54
  def _pass_context(ctx: typer.Context):
55
55
  if ctx.parent and ctx.parent.obj:
@@ -59,6 +59,7 @@ for sub_app in [config.app, deps.app, history.app, server.app, workflow.app]:
59
59
  app.add_typer(config.app, name="config", help="Import/export configuration")
60
60
  app.add_typer(deps.app, name="deps", help="Manage dependencies")
61
61
  app.add_typer(history.app, name="history", help="Execution history")
62
+ app.add_typer(queue.app, name="queue", help="View and manage execution queue")
62
63
  app.add_typer(server.app, name="server", help="Manage servers")
63
64
  app.add_typer(workflow.app, name="workflow", help="Manage workflows")
64
65
 
@@ -69,3 +70,5 @@ app.command("run")(run.run_cmd)
69
70
  app.command("submit")(run.submit_cmd)
70
71
  app.command("status")(run.status_cmd)
71
72
  app.command("upload")(upload.upload_cmd)
73
+ app.command("cancel")(cancel.cancel_cmd)
74
+ app.command("free")(free.free_cmd)
@@ -4,7 +4,7 @@ requires = ["setuptools>=61"]
4
4
 
5
5
  [project]
6
6
  name = "comfyui-skill-cli"
7
- version = "0.2.4"
7
+ version = "0.2.5"
8
8
  description = "ComfyUI Skill CLI — Agent-friendly workflow management"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -0,0 +1,150 @@
1
+ """Tests for new client methods and error hint matching."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import unittest
6
+ from unittest.mock import MagicMock, patch
7
+
8
+ from comfyui_skills_cli.client import ComfyUIClient
9
+ from comfyui_skills_cli.error_hints import match_error_hint
10
+
11
+
12
+ class InterruptTests(unittest.TestCase):
13
+ def setUp(self) -> None:
14
+ self.client = ComfyUIClient("http://localhost:8188")
15
+
16
+ @patch("comfyui_skills_cli.client.requests.post")
17
+ def test_interrupt_with_prompt_id(self, mock_post: MagicMock) -> None:
18
+ mock_post.return_value = MagicMock(status_code=200)
19
+ result = self.client.interrupt("abc-123")
20
+ self.assertTrue(result["success"])
21
+ call_kwargs = mock_post.call_args
22
+ self.assertIn("/interrupt", call_kwargs.args[0])
23
+ self.assertEqual(call_kwargs.kwargs["json"], {"prompt_id": "abc-123"})
24
+
25
+ @patch("comfyui_skills_cli.client.requests.post")
26
+ def test_interrupt_without_prompt_id(self, mock_post: MagicMock) -> None:
27
+ mock_post.return_value = MagicMock(status_code=200)
28
+ result = self.client.interrupt()
29
+ self.assertTrue(result["success"])
30
+ call_kwargs = mock_post.call_args
31
+ self.assertEqual(call_kwargs.kwargs["json"], {})
32
+
33
+
34
+ class QueueManagementTests(unittest.TestCase):
35
+ def setUp(self) -> None:
36
+ self.client = ComfyUIClient("http://localhost:8188")
37
+
38
+ @patch("comfyui_skills_cli.client.requests.post")
39
+ def test_queue_clear(self, mock_post: MagicMock) -> None:
40
+ mock_post.return_value = MagicMock(status_code=200)
41
+ result = self.client.queue_clear()
42
+ self.assertTrue(result["success"])
43
+ call_kwargs = mock_post.call_args
44
+ self.assertEqual(call_kwargs.kwargs["json"], {"clear": True})
45
+
46
+ @patch("comfyui_skills_cli.client.requests.post")
47
+ def test_queue_delete(self, mock_post: MagicMock) -> None:
48
+ mock_post.return_value = MagicMock(status_code=200)
49
+ result = self.client.queue_delete(["id-1", "id-2"])
50
+ self.assertTrue(result["success"])
51
+ call_kwargs = mock_post.call_args
52
+ self.assertEqual(call_kwargs.kwargs["json"], {"delete": ["id-1", "id-2"]})
53
+
54
+
55
+ class FreeMemoryTests(unittest.TestCase):
56
+ def setUp(self) -> None:
57
+ self.client = ComfyUIClient("http://localhost:8188")
58
+
59
+ @patch("comfyui_skills_cli.client.requests.post")
60
+ def test_free_both(self, mock_post: MagicMock) -> None:
61
+ mock_post.return_value = MagicMock(status_code=200)
62
+ result = self.client.free_memory(unload_models=True, free_memory=True)
63
+ self.assertTrue(result["success"])
64
+ call_kwargs = mock_post.call_args
65
+ self.assertEqual(call_kwargs.kwargs["json"], {"unload_models": True, "free_memory": True})
66
+
67
+ @patch("comfyui_skills_cli.client.requests.post")
68
+ def test_free_models_only(self, mock_post: MagicMock) -> None:
69
+ mock_post.return_value = MagicMock(status_code=200)
70
+ self.client.free_memory(unload_models=True, free_memory=False)
71
+ call_kwargs = mock_post.call_args
72
+ self.assertEqual(call_kwargs.kwargs["json"], {"unload_models": True})
73
+
74
+ @patch("comfyui_skills_cli.client.requests.post")
75
+ def test_free_memory_only(self, mock_post: MagicMock) -> None:
76
+ mock_post.return_value = MagicMock(status_code=200)
77
+ self.client.free_memory(unload_models=False, free_memory=True)
78
+ call_kwargs = mock_post.call_args
79
+ self.assertEqual(call_kwargs.kwargs["json"], {"free_memory": True})
80
+
81
+ @patch("comfyui_skills_cli.client.requests.post")
82
+ def test_free_no_flags_sends_empty(self, mock_post: MagicMock) -> None:
83
+ mock_post.return_value = MagicMock(status_code=200)
84
+ self.client.free_memory(unload_models=False, free_memory=False)
85
+ call_kwargs = mock_post.call_args
86
+ self.assertEqual(call_kwargs.kwargs["json"], {})
87
+
88
+
89
+ class UploadFileTests(unittest.TestCase):
90
+ def setUp(self) -> None:
91
+ self.client = ComfyUIClient("http://localhost:8188")
92
+
93
+ @patch("comfyui_skills_cli.client.requests.post")
94
+ def test_upload_file_calls_upload_image_endpoint(self, mock_post: MagicMock) -> None:
95
+ mock_post.return_value = MagicMock(status_code=200)
96
+ mock_post.return_value.json.return_value = {"name": "test.png", "subfolder": "", "type": "input"}
97
+
98
+ import tempfile, os
99
+ with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as f:
100
+ f.write(b"fake png data")
101
+ tmp_path = f.name
102
+
103
+ try:
104
+ result = self.client.upload_file(tmp_path)
105
+ self.assertEqual(result["name"], "test.png")
106
+ call_args = mock_post.call_args
107
+ self.assertIn("/upload/image", call_args.args[0])
108
+ finally:
109
+ os.unlink(tmp_path)
110
+
111
+ def test_upload_image_delegates_to_upload_file(self) -> None:
112
+ with patch.object(self.client, "upload_file", return_value={"name": "x.png"}) as mock:
113
+ result = self.client.upload_image("/fake/path.png")
114
+ mock.assert_called_once_with("/fake/path.png")
115
+ self.assertEqual(result["name"], "x.png")
116
+
117
+
118
+ class ErrorHintTests(unittest.TestCase):
119
+ def test_unauthorized_hint(self) -> None:
120
+ hint = match_error_hint("Unauthorized: Please login first to use this node.")
121
+ self.assertIn("ComfyUI API Key", hint)
122
+ self.assertIn("platform.comfy.org", hint)
123
+
124
+ def test_cuda_oom_hint(self) -> None:
125
+ hint = match_error_hint("RuntimeError: CUDA out of memory. Tried to allocate 2.00 GiB")
126
+ self.assertIn("comfyui-skill free", hint)
127
+
128
+ def test_mps_oom_hint(self) -> None:
129
+ hint = match_error_hint("RuntimeError: MPS out of memory")
130
+ self.assertIn("comfyui-skill free", hint)
131
+
132
+ def test_connection_refused_hint(self) -> None:
133
+ hint = match_error_hint("ConnectionError: Connection refused")
134
+ self.assertIn("not running", hint)
135
+
136
+ def test_missing_model_ckpt_hint(self) -> None:
137
+ hint = match_error_hint("FileNotFoundError: model_v1.ckpt not found")
138
+ self.assertIn("deps check", hint)
139
+
140
+ def test_missing_model_safetensors_hint(self) -> None:
141
+ hint = match_error_hint("Could not load sdxl_base.safetensors")
142
+ self.assertIn("deps check", hint)
143
+
144
+ def test_no_hint_for_unknown_error(self) -> None:
145
+ hint = match_error_hint("Some random error message")
146
+ self.assertEqual(hint, "")
147
+
148
+
149
+ if __name__ == "__main__":
150
+ unittest.main()
@@ -1 +0,0 @@
1
- __version__ = "0.2.4"