Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
104 commits
Select commit Hold shift + click to select a range
5e88ffe
feat(output): structured JSON envelopes and branded terminal UI
skishore23 May 22, 2026
8c5fe65
feat(auth): OAuth 2.1 + PKCE cloud sign-in and unified routing
skishore23 May 22, 2026
376792d
feat(introspect): CQL query engine for the node graph
skishore23 May 22, 2026
03981d9
feat(run): async workflow execution with job tracking
skishore23 May 22, 2026
c7d924a
feat(skills): bundled agent skill library
skishore23 May 22, 2026
0bff06b
feat(setup): interactive wizard, command wiring, and dependency updates
skishore23 May 22, 2026
f868b7b
fix(transfer): prevent auth header leakage, filename injection, and SSRF
skishore23 May 22, 2026
619be0b
feat(tracking): integrate PostHog dual-send and execution lifecycle e…
skishore23 May 22, 2026
31fd27f
docs: add Quick Start section with comfy setup
skishore23 May 22, 2026
d2d88cf
docs: remove Aider reference
skishore23 May 22, 2026
d229868
feat: improve workflow validation, cloud error surfacing, and JSON mo…
skishore23 May 24, 2026
9a437af
feat: blueprint composition, cloud poll resilience, oauth session ren…
skishore23 Jun 1, 2026
a2a0cd5
chore(deps): bump vulnerable dependencies (Dependabot)
skishore23 Jun 1, 2026
a424f3e
chore: pin ruff to 0.15.15 and align repo formatting
skishore23 Jun 1, 2026
862009a
feat: skills consolidation, upgrade/feedback flows, and type-safety f…
skishore23 Jun 1, 2026
fa3e31e
fix(update): never crash the welcome banner on a malformed update-che…
skishore23 Jun 9, 2026
83c1871
fix(run): use ws:// for plain-HTTP hosts so LAN ComfyUI is watchable …
skishore23 Jun 9, 2026
e745b2d
fix(run): emit output NDJSON events instead of swallowing them via se…
skishore23 Jun 9, 2026
c0fe460
fix(jobs): cancelled/interrupted local jobs reach a terminal state in…
skishore23 Jun 9, 2026
0005f99
fix(jobs): preserve pre-timeout status in watcher_timeout diagnostics
skishore23 Jun 9, 2026
36d5c25
fix(jobs): watch surfaces ok:false/non-zero exit for failed and cance…
skishore23 Jun 9, 2026
784ec81
fix(run): cloud --wait --timeout means per-event silence, matching lo…
skishore23 Jun 9, 2026
6747c71
fix(run): --print-prompt is a true dry-run on the cloud route (no sub…
skishore23 Jun 9, 2026
c559140
fix(cql): fork shared subgraph definitions on interior write for per-…
skishore23 Jun 10, 2026
86da6d8
fix(cql): keep curated subgraph slot addresses when a proxy widget va…
skishore23 Jun 10, 2026
14f14c9
fix(cql): parse slot address on first dot so dotted input names route…
skishore23 Jun 10, 2026
443d251
fix(fragments): foreach no longer namespaces literal string params, o…
skishore23 Jun 10, 2026
79df11e
fix(workflow): chunked compose clears stale unnumbered output and rep…
skishore23 Jun 10, 2026
65a6b6c
feat(nodes): surface stale object_info cache as a machine-readable wa…
skishore23 Jun 10, 2026
8e90993
fix(tracking): feedback attaches an ephemeral id without persisting i…
skishore23 Jun 10, 2026
a241983
test+docs(generate): assert 'Not signed in' and document OAuth-first …
skishore23 Jun 10, 2026
33376f4
feat(generate): route --emit-workflow through the renderer envelope w…
skishore23 Jun 10, 2026
ddbcc51
test(generate): align resolve_api_key_missing assertion to OAuth-firs…
skishore23 Jun 10, 2026
85ae9c9
style: ruff format and lint-fix the review-fix changes
skishore23 Jun 10, 2026
482d492
fix(jobs): watch recognizes 'cancelled' as terminal so it exits 130 i…
skishore23 Jun 10, 2026
0fcf89a
docs(skills): fix debug skill — OAuth-first precedence, per-silence t…
skishore23 Jun 10, 2026
b3cc77c
docs(skills): fragments skill — remove fabricated backward-compat, fi…
skishore23 Jun 10, 2026
5b8fa4a
fix(workflow): teach id-based slot addresses in CLI hints and the com…
skishore23 Jun 10, 2026
7d8be37
feat(fragments): bundled fragment library with resolver fallback and …
skishore23 Jun 10, 2026
a314f6f
feat(fragments): bundled sdxl_t2i_lora fragment — model names are dis…
skishore23 Jun 10, 2026
538abae
feat(discovery): register models and templates surfaces in COMMAND_SC…
skishore23 Jun 10, 2026
b929e05
docs(skills): comfy skill teaches bundled video fragments, ecosystem-…
skishore23 Jun 10, 2026
982629f
feat(skills): path-based skill install and skills validate — third-pa…
skishore23 Jun 10, 2026
7bd70cb
feat(skills): install manifest for provenance/staleness and the skill…
skishore23 Jun 10, 2026
44e7967
style: ruff format alignment test file
skishore23 Jun 10, 2026
f96fd03
feat(generate): partner workflow emitter, OAuth-first credential reso…
skishore23 Jun 10, 2026
6a4fc1d
chore: remove skills-authoring doc; inline the format contract into C…
skishore23 Jun 10, 2026
60cffca
revert(fragments): drop the bundled fragment library — derive from ob…
skishore23 Jun 10, 2026
799059e
docs(skills): teach fragment derivation from templates/CQL instead of…
skishore23 Jun 10, 2026
62f9b63
feat(output): version the machine contract — envelope/1 + event/1 wit…
skishore23 Jun 10, 2026
9a37d3a
refactor(run)!: one JSON dialect — run --json now emits the renderer …
skishore23 Jun 10, 2026
15363e3
refactor(auth): one resolve_cloud_credential for all four credential …
skishore23 Jun 10, 2026
f0faa31
fix(cql): accept int-valued and dict-form COMBOs
skishore23 Jun 10, 2026
a135cca
fix(run): partner detection via api_node flag; surface silent partial…
skishore23 Jun 10, 2026
82cd7e1
fix(output): validate envelope ok now mirrors the verdict and exit code
skishore23 Jun 10, 2026
0898cbc
fix(auth): serialize OAuth refresh across processes; force-refresh on…
skishore23 Jun 10, 2026
ef3b237
docs(skills): correct partner category to partner/* (was api node/*)
skishore23 Jun 10, 2026
e9d6c8a
fix(fragments): accept BOOLEAN param type
skishore23 Jun 11, 2026
b3a4e6d
fix(jobs): map non_retryable_error/lost cloud statuses to error
skishore23 Jun 11, 2026
0984059
fix(jobs): _emit_terminal falls back to top-level error_message
skishore23 Jun 11, 2026
df98be7
fix(watcher): unknown-status stall guard
skishore23 Jun 11, 2026
411e132
feat(jobs_state): record + item_map fields, stashed at terminal
skishore23 Jun 11, 2026
1a7c63c
feat(client): node-keyed extract_outputs
skishore23 Jun 11, 2026
2a7e94a
feat(run): outputs_by_node / outputs_by_item in --wait envelope
skishore23 Jun 11, 2026
6071e3b
feat(jobs): grouped outputs in jobs status / watch terminal envelopes
skishore23 Jun 11, 2026
7b02464
feat(compose): item map in summary + _meta provenance embedded in com…
skishore23 Jun 11, 2026
803bfce
feat(run): strip compose _meta before submit, stash item_map on job s…
skishore23 Jun 11, 2026
0da20f9
feat(download): item-named files + node_id/item provenance in files[]
skishore23 Jun 11, 2026
3de9544
feat(run,jobs): local-path parity for item map + grouped outputs
skishore23 Jun 11, 2026
783c98a
feat(project): comfy_cli/project.py module
skishore23 Jun 11, 2026
88e782b
feat(project): init + status commands
skishore23 Jun 11, 2026
7b3548d
feat(project): where routing precedence + journal hooks + download de…
skishore23 Jun 11, 2026
2c6bc66
fix(project): init resolves the where default instead of hardcoding c…
skishore23 Jun 11, 2026
fbae7ac
refactor(transfer): extract reusable single-file upload helper
skishore23 Jun 11, 2026
5798563
feat(assets): comfy assets push + .comfy/assets.lock.json
skishore23 Jun 11, 2026
1b26762
feat(fragments): $asset references resolved from the push lock
skishore23 Jun 11, 2026
0bfb950
docs(skills): teach the project/1 convention — $asset, push lock, ite…
skishore23 Jun 11, 2026
b4efb4e
feat(fragments): $asset resolves in params and foreach item values
skishore23 Jun 11, 2026
c56ad0b
feat(blueprints): $var.<name> project constants from comfy.yaml vars
skishore23 Jun 11, 2026
cc4347f
fix(skills): bundled skill dirs obey their own naming convention
skishore23 Jun 11, 2026
1fec60c
feat(cql): understand V3 autogrow inputs — validate slot wiring, self…
skishore23 Jun 11, 2026
a224346
fix(auth): gate fatal refresh on token-endpoint error codes, surface …
skishore23 Jun 11, 2026
4dddeb0
fix(fragments): literal video/audio inputs materialize the right load…
skishore23 Jun 11, 2026
baea1bc
fix(output): --json download must emit ONLY the envelope on stdout
skishore23 Jun 11, 2026
1caa668
fix(transfer): download handles piped error envelopes without crashing
skishore23 Jun 11, 2026
43da02f
fix(run): transient HTTP 429/5xx during cloud wait polling backs off …
skishore23 Jun 11, 2026
409f25d
fix(transfer): item-named downloads never silently overwrite
skishore23 Jun 11, 2026
32de43e
refactor!: drop back-compat aliases (BOOL param type, legacy "api nod…
skishore23 Jun 11, 2026
55e5e75
fix(assets): push envelope lists already-current assets
skishore23 Jun 11, 2026
4a3fddc
docs(skills): one-graph video production is the preferred pattern
skishore23 Jun 11, 2026
3ed67f3
Merge origin/main into agent-cli — port telemetry/usage-source work i…
skishore23 Jun 11, 2026
e70ed6c
style: ruff format pass over merge-touched files
skishore23 Jun 11, 2026
5837144
feat(errors): classify execution failures; add retryable transient_au…
skishore23 Jun 12, 2026
f40ed9f
fix(auth): observer commands can no longer clear the shared OAuth ses…
skishore23 Jun 12, 2026
7db86c0
feat(skills): bundle comfy-director — narrative multi-shot video prod…
skishore23 Jun 12, 2026
2530aed
Merge remote-tracking branch 'origin/main' into agent-cli
skishore23 Jun 13, 2026
7b4fec3
fix(ci): install jsonschema for pytest job; declare as dev dependency
skishore23 Jun 13, 2026
509554e
fix: compatibility with click 8.2+ / typer 0.13+ (CI installs latest)
skishore23 Jun 13, 2026
596e7fd
fix: help-json option flags under typer 0.13+; deterministic CI tests
skishore23 Jun 13, 2026
006735d
fix: address CodeRabbit review (34 issues)
skishore23 Jun 13, 2026
c0c90b6
fix(tracking): drop duplicate init in enable(); plain echo strings
skishore23 Jun 13, 2026
842e400
ci(codecov): replace zero-tolerance ratchet with a 70% project floor
skishore23 Jun 13, 2026
d9373f4
test(e2e): pin COMFY_OUTPUT=pretty in the exec harness
skishore23 Jun 13, 2026
3cbea00
fix(locking): guard os.fchmod for Windows (AttributeError, not OSError)
skishore23 Jun 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/pytest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pytest pytest-cov
pip install pytest pytest-cov jsonschema
pip install -e .

- name: Run tests
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ruff_check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
- name: install ruff
run: |
python -m pip install --upgrade pip
pip install ruff
pip install ruff==0.15.15
- name: lint check and then format check with ruff
run: |
ruff check
Expand Down
16 changes: 16 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
__pycache__/
*.py[cod]

# Local agent / editor configs
.claude/settings.local.json

# Generated artifacts — comfy CLI convention dirs
/workflows/
/outputs/
/output/
/inputs/
/variants/
/stories

# Local projects (creative work, demos, experiments)
/projects/
conda.listing.txt

#COMMON CONFIGs
.DS_Store
.src_port
Expand Down Expand Up @@ -61,3 +76,4 @@ requirements.compiled
override.txt
.coverage
coverage.xml

2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ repos:
(^.*\.(json|txt)$)

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.12.4
rev: v0.15.15
hooks:
# Run the linter.
- id: ruff
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,18 @@ workflows, and call hosted partner image models, all from your terminal.
- 🎬 Run workflows against a local ComfyUI server, including auto-conversion of UI-format JSON
- 🧪 Test ComfyUI and frontend pull requests with one flag
- 💻 Cross-platform: Windows, macOS, Linux
- ☁️ Route any workflow to **Comfy Cloud** with `--where cloud` (no GPU required)
- 🤖 Agent-friendly: every command emits structured `--json` envelopes
- 📚 Bundled skills teach Claude / Cursor to drive comfy natively

## Quick Start

```bash
pip install comfy-cli
comfy setup
```

`comfy setup` walks you through everything — local or cloud routing, authentication, and agent skill installation — in one interactive wizard. Pass `-y` for non-interactive (CI/scripted) installs.

## Installation

Expand Down
19 changes: 19 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Codecov configuration.
#
# The default `project` status uses target `auto` with zero tolerance, i.e. a
# hard ratchet that fails on any drop in total coverage. That doesn't suit a
# repo that periodically lands large feature surfaces (new CLI command wrappers
# are thin and lightly tested by design), so we replace the ratchet with an
# explicit floor: total coverage must stay at/above 70%. This is still a real
# gate — it just tolerates the small dips that come with adding command surface —
# and can be raised over time as coverage backfills.
coverage:
status:
project:
default:
target: 70%
threshold: 1%
# No patch (diff-coverage) status — avoid blocking on lightly-tested CLI glue.
patch: false

comment: false
26 changes: 26 additions & 0 deletions comfy_cli/auth/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""Local credential store for comfy-cli.

Where keys live:

${XDG_CONFIG_HOME or platform-equivalent}/comfy-cli/secrets.json

Format::

{
"providers": {
"comfy-cloud": {"key": "sk-…", "updated_at": "2026-05-15T12:00:00Z"},
"civitai": {"key": "...", "updated_at": "..."}
}
}

The file is created with mode ``0600``. Phase 5 will replace this plaintext
JSON with an encrypted ``secrets.bin``; the API surface here is the
forward-compatible interface, so call sites don't need to change.

Local-only: this module never makes a network call.
"""

from comfy_cli.auth import store
from comfy_cli.auth.store import SUPPORTED_PROVIDERS, AuthRecord

__all__ = ["AuthRecord", "SUPPORTED_PROVIDERS", "store"]
157 changes: 157 additions & 0 deletions comfy_cli/auth/command.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
"""``comfy auth`` — manage API tokens for third-party model hosts.

Tokens here are used by ``comfy model download`` when fetching gated
checkpoints / LoRAs / VAEs from Civitai or Hugging Face. Stored locally,
never transmitted except to the issuing provider.

Comfy Cloud sign-in lives in a separate namespace — see ``comfy cloud``.
"""

from __future__ import annotations

from typing import Annotated

import typer

from comfy_cli import tracking
from comfy_cli.auth import store
from comfy_cli.output import get_renderer, rprint

app = typer.Typer(
no_args_is_help=True,
help="Manage API tokens for model hosts (Civitai, Hugging Face).",
)


@app.command("list", help="List third-party API-key providers (civitai, huggingface).")
@tracking.track_command("auth")
def list_cmd():
renderer = get_renderer()
records = store.list_records()
if renderer.is_pretty():
_render_pretty_list(records=records)
renderer.emit(
{
"providers": [r.to_dict(redact=True) for r in records],
"supported": list(store.SUPPORTED_PROVIDERS),
"path": str(store.secrets_path()),
"action": "list",
},
command="auth list",
)


@app.command("set", help="Set or replace the API token for a third-party model host.")
@tracking.track_command("auth")
def set_cmd(
provider: Annotated[
str,
typer.Argument(help="Provider name — `civitai` or `huggingface`. Comfy Cloud uses `comfy cloud login`."),
],
key: Annotated[
str,
typer.Option(
"--key",
show_default=False,
help="The API token. Stored locally; never sent except to the provider.",
),
],
):
renderer = get_renderer()
if provider == "comfy-cloud":
renderer.error(
code="auth_use_login_for_cloud",
message="Comfy Cloud uses OAuth — `auth set --key` is not supported for `comfy-cloud`.",
hint="run: comfy cloud login (or `comfy cloud set-key` for the API-key path)",
details={"provider": provider},
)
raise typer.Exit(code=1)
if not key:
renderer.error(code="auth_invalid_key", message="--key cannot be empty.")
raise typer.Exit(code=1)
try:
record = store.set(provider, key)
except ValueError as e:
renderer.error(code="auth_invalid_key", message=str(e))
raise typer.Exit(code=1)
if renderer.is_pretty():
rprint(f"[bold green]Stored token for {record.provider}[/bold green] ({record.to_dict()['key']})")
if provider not in store.SUPPORTED_PROVIDERS:
rprint(f"[yellow]Note:[/yellow] {provider!r} is not a well-known provider; stored anyway.")
renderer.emit(
{
"providers": [r.to_dict(redact=True) for r in store.list_records()],
"supported": list(store.SUPPORTED_PROVIDERS),
"path": str(store.secrets_path()),
"action": "set",
},
command="auth set",
changed=True,
)


@app.command("remove", help="Remove a stored third-party API token.")
@tracking.track_command("auth")
def remove_cmd(
provider: Annotated[str, typer.Argument(help="Provider name.")],
):
renderer = get_renderer()
if provider == "comfy-cloud":
renderer.error(
code="auth_use_logout_for_cloud",
message="Comfy Cloud uses OAuth — use `comfy cloud logout` to clear the session.",
hint="run: comfy cloud logout",
details={"provider": provider},
)
raise typer.Exit(code=1)
removed = store.remove(provider)
if not removed:
renderer.error(
code="auth_not_found",
message=f"No stored key for {provider!r}.",
hint="run: comfy auth list",
details={"provider": provider},
)
raise typer.Exit(code=1)
if renderer.is_pretty():
rprint(f"[bold]Removed token for {provider}[/bold]")
renderer.emit(
{
"providers": [r.to_dict(redact=True) for r in store.list_records()],
"supported": list(store.SUPPORTED_PROVIDERS),
"path": str(store.secrets_path()),
"action": "remove",
},
command="auth remove",
changed=True,
)


# ---------------------------------------------------------------------------
# helpers
# ---------------------------------------------------------------------------


def _render_pretty_list(*, records):
"""Pretty-render the auth list (third-party provider tokens only)."""
from rich.console import Group
from rich.text import Text

from comfy_cli.config_manager import ConfigManager
from comfy_cli.output.branding import branded_panel
from comfy_cli.output.panels import auth_empty_panel, auth_list_table

renderer = get_renderer()
path = str(store.secrets_path())

if not records:
# Empty-state has its own panel; brand it via the canonical wrapper.
body = auth_empty_panel(supported=list(store.SUPPORTED_PROVIDERS), path=path)
else:
redacted = [r.to_dict(redact=True) for r in records]
body = auth_list_table(redacted, supported=list(store.SUPPORTED_PROVIDERS), path=path)

hint = Text("Comfy Cloud sign-in lives under `comfy cloud whoami`.", style="dim")
group = Group(body, Text(""), hint)

renderer.console().print(branded_panel(group, title="auth", version=ConfigManager().get_cli_version()))
Loading
Loading