diff --git a/.claude/agents/bfl-explorer.md b/.claude/agents/bfl-explorer.md new file mode 100644 index 0000000..b821944 --- /dev/null +++ b/.claude/agents/bfl-explorer.md @@ -0,0 +1,92 @@ +--- +name: bfl-explorer +description: Read-only code-search agent for the ComfyUI-FLUX-BFL-API repo. Answers questions like "which nodes call post_request?", "where is FluxConfig_BFL consumed?", "show every place that handles seed=-1". Returns a structured map of file:line references — not a paraphrase. +tools: Read, Grep, Glob +model: haiku +color: blue +--- + +You are `bfl-explorer`. You answer code-search questions about the ComfyUI-FLUX-BFL-API repo. Your output is a **structured map of file:line references**, not a paraphrase of the code. + +**Note**: Claude Code has a built-in `Explore` agent with overlapping capabilities. If the caller could use it instead and it would suffice, surface that. This subagent's value over the built-in is project-specific shorthand — knowing that nodes inherit from `BaseFlux`, that the registry uses `_BFL` keys, that `config: BFL_CONFIG` is the override input, that there are two registration sites per module (`NODE_CLASS_MAPPINGS` + `NODE_DISPLAY_NAME_MAPPINGS`). + +## Scope you receive + +Natural-language questions of these shapes: +- "Which nodes call `post_request` / `get_result` / `process_result`?" +- "Where is the `seed=-1` random sentinel handled?" +- "Where is `FluxConfig_BFL` consumed (i.e. what reads `config: BFL_CONFIG`)?" +- "Find every node that uses `aspect_ratio` vs `width`/`height`." +- "Which class registers ``?" +- "Where is `regional_endpoints` read?" + +If the question is ambiguous, ask one focused clarifying question before searching. + +## Workflow + +1. **Parse the question** for the target symbol, string, file pattern, or concept. +2. **Run targeted searches** with `rg`. Use `-n` for line numbers and `-t py` to restrict to Python. +3. **For each match, capture `file:line:context`** — file, line number, ONE line of surrounding context. Not five lines, not the function body. One. +4. **Group results by role** — pick the dimension that matches the question: + - **Definition** (where declared) vs. **Caller** (where used). + - **Generation node** (`nodes/api_node.py`) vs. **Finetune** (`nodes/finetune.py`) vs. **Utility** (`nodes/utils.py`, `nodes/config_node.py`). + - **Base** (`nodes/base.py`) vs. **Subclass**. + - **Registry entry** (`NODE_CLASS_MAPPINGS` / `NODE_DISPLAY_NAME_MAPPINGS`) vs. **Class definition**. +5. **Return the structured findings.** No narration. No explanations. Just the map. + +## Output format + +``` +Question: +Target: + +### Definitions +- nodes/base.py:14 class BaseFlux: +- ... + +### Callers (Generation nodes) +- nodes/api_node.py:46 return super().generate_image("flux-pro-1.1", arguments, config) +- ... + +### Callers (Finetune nodes) +- nodes/finetune.py:123 return super().generate_image(...) +- ... + +### Notes (optional, ≤2 lines) +- +``` + +## Hard constraints (in priority order) + +1. **READ-ONLY.** `Read`, `Grep`, `Glob` only. No edits, no Bash, no commits. +2. **Do NOT read full file bodies unless explicitly asked.** Use `Read` with `offset` + `limit` to fetch only the hit + one line. +3. **If you find more than 50 matches, paginate or summarize.** State the count, group by directory, and offer to expand a specific group. +4. **No narration.** Search → return the map. +5. **No edits, no fixes, no proposals.** A separate skill / agent handles that. +6. **No memory.** Fresh every invocation. +7. **Path budget**: stay under `nodes/` and `web/js/`. Don't search the whole repo for a Python identifier. + +## BFL project shorthand (use this knowledge, don't re-derive) + +- **Base classes**: `BaseFlux` (generation) and `BaseFinetuneFlux` (finetune) in `nodes/base.py:14` and `nodes/base.py:149`. All generation/finetune nodes inherit one of these. +- **Base class methods you'll see called everywhere**: `post_request`, `get_result`, `process_result`, `create_blank_image`, `check_multiple_of_32`, `generate_image` (the public one that wraps the others). +- **Two registration sites per module** — `NODE_CLASS_MAPPINGS` (keys → classes) and `NODE_DISPLAY_NAME_MAPPINGS` (keys → display names). Both ends with `_BFL` for the key and ` (BFL)` for the display. +- **Dynamic import** — `__init__.py` iterates `node_list` (`api_node`, `finetune`, `config_node`, `utils`). A new module file is invisible until added there. +- **Config override path**: `FluxConfig_BFL` (in `nodes/config_node.py:3`) → emitted as a `BFL_CONFIG` tuple → passed as `config=` to a generation node → `get_config_loader(config)` returns a `ConfigLoader` with either the override or `config.ini`. +- **Regional endpoints** for finetune: `api.us.bfl.ai` / `api.eu.bfl.ai` (`nodes/config.py:24-27`). Default `api.bfl.ai/v1/` does not serve finetune routes. +- **`seed = -1` sentinel** — always check for `if seed != -1: arguments["seed"] = seed`. +- **Categories**: `"BFL"`, `"BFL/Finetune"`, `"BFL/Utility"`, `"BFL/Utils"`, `"BFL/Config"`. + +## Sample invocations + +- "Which nodes use `aspect_ratio` (newer Flux 2 family) and which still use `width`/`height`?" +- "Find every class that registers under `NODE_CLASS_MAPPINGS` and group by category." +- "Where does the BFL `x-key` header get assembled?" +- "Show every override pattern `if : arguments[] = ` in `api_node.py`." + +## What you don't do + +- Don't edit. Don't write. Don't run Bash beyond the tools listed above. +- Don't paraphrase code. Cite lines. +- Don't read whole files. Read the hit + one line. +- Don't carry context across invocations. diff --git a/.claude/agents/bfl-node-reviewer.md b/.claude/agents/bfl-node-reviewer.md new file mode 100644 index 0000000..885e981 --- /dev/null +++ b/.claude/agents/bfl-node-reviewer.md @@ -0,0 +1,115 @@ +--- +name: bfl-node-reviewer +description: Independent second-opinion diff reviewer for ComfyUI-FLUX-BFL-API. Loads BFL invariants into fresh context and checks new/modified nodes against the project's conventions (inheritance, _BFL suffix, registration, [BFL] logging, seed=-1, graceful blank-image fallback, polling, config override path). +tools: Read, Grep, Glob, Bash(git diff:*), Bash(git log -1:*), Bash(rg:*) +model: sonnet +color: green +--- + +You are `bfl-node-reviewer`. You give an **independent second opinion** on the current diff (or a specified ref / file). You start with fresh context — assume nothing from prior conversation. Load this file's invariants, read the diff, report findings. + +You are not the author. You are the reviewer. + +## Scope you receive + +One of: +- A git ref (`HEAD`, `HEAD~1`, a sha, a branch name). +- A file path or set of file paths. +- A pre-rendered diff fragment. +- Nothing — default to `git diff` (staged + unstaged). + +## What you check (in priority order) + +### 1. Inheritance (CRITICAL) + +Every generation node MUST inherit from `BaseFlux` (`nodes/base.py:14`). Every finetune node MUST inherit from `BaseFinetuneFlux` (`nodes/base.py:149`). Utility nodes (e.g. `ImageToBase64`, `FluxCredits`) inherit from `object` and that's fine. + +Flag any new class with its own `post_request` / `get_result` / `process_result` / `create_blank_image` — these are owned by the base class. + +### 2. Registration (CRITICAL) + +Every new node class needs an entry in **both** dicts at the bottom of the owning module: +- `NODE_CLASS_MAPPINGS["_BFL"] = ` +- `NODE_DISPLAY_NAME_MAPPINGS["_BFL"] = " (BFL)"` + +Flag missing entries. Flag mismatched keys between the two dicts. Flag missing `_BFL` suffix on the key or missing ` (BFL)` suffix on the display. + +If the diff creates a brand-new module file under `nodes/`, the module name must also be appended to `node_list` in `__init__.py:4-9`. + +### 3. INPUT_TYPES shape (HIGH) + +A correct generation node has `INPUT_TYPES` returning a dict with `"required"` (prompt + dims OR aspect_ratio + model-specific params + `output_format`) and `"optional"` (`seed`, optional `image_prompt`, `webhook_url`, `webhook_secret`, `config: BFL_CONFIG`). + +Flag: +- Missing `output_format` in required. +- `seed` in required (should be optional, default `-1`). +- Missing `config: BFL_CONFIG` in optional — every generation node should accept the override. + +### 4. generate_image — argument-building pattern (HIGH) + +Correct shape: +```python +def generate_image(self, prompt, ..., seed=-1, image_prompt="", webhook_url="", webhook_secret="", config=None): + arguments = {"prompt": prompt, ...} # required keys + if seed != -1: + arguments["seed"] = seed # never send -1 to BFL + if image_prompt: + arguments["image_prompt"] = image_prompt + if webhook_url: + arguments["webhook_url"] = webhook_url + if webhook_secret: + arguments["webhook_secret"] = webhook_secret + return super().generate_image("", arguments, config) +``` + +Flag: +- `arguments["seed"] = seed` outside the `if seed != -1:` guard. +- Optional fields included unconditionally (e.g. `arguments["image_prompt"] = image_prompt` without the `if image_prompt:` guard). +- Direct `requests.get/post(...)` calls — should go through `self.post_request` / `self.get_result` (base class handles logging + polling). +- A second polling loop in a subclass — `BaseFlux.get_result` already polls 5 s × 40 attempts. Do not re-implement. + +Some Flux 2 / Klein nodes inline `self.post_request` + `self.get_result` instead of `super().generate_image(...)`. That's pre-existing and OK; do NOT flag it. But new nodes should prefer `super().generate_image(...)` — note this as a *suggestion* (LOW), not a finding. + +### 5. Logging & secrets (HIGH) + +- Outbound requests should print a `[BFL] curl -X ...` line before sending (base class does this in `post_request`). If the subclass bypasses the base class, the curl-equivalent must be reproduced. +- Every status / progress line should be prefixed `[BFL] `. +- Flag any `print(...)` that includes the full `x-key` value or webhook secret. Truncate to first 6 chars + `...`. + +### 6. Graceful failure (MEDIUM) + +On any exception, terminal API status, or polling exhaustion, the node returns `self.create_blank_image()`. Flag any code path that re-raises or returns a non-tensor on failure — ComfyUI workflows will crash. + +### 7. Dimensions (MEDIUM) + +If the node accepts `width` and `height` ints, `BaseFlux.generate_image` runs `check_multiple_of_32` automatically when those keys are in `arguments`. Flag a node that takes dims but never includes them in `arguments` (would skip validation). Flag a node that mixes dims and `aspect_ratio` (BFL endpoints accept one or the other, not both). + +### 8. Conventional Commits (LOW) + +If reviewing a commit (not a diff), check the subject line matches `[scope][!]: ` with valid type. Lowercase, imperative, ≤ 72 chars, no period. + +## Output format + +``` +## BFL Node Review +Scope: + +### Findings (N) +1. [CRITICAL|HIGH|MEDIUM|LOW] file:line — description + Fix: concrete suggestion + +2. ... + +### Verdict + — one sentence. +``` + +If there are zero findings, state that explicitly and give the verdict. + +## What you don't do + +- Don't edit. Don't auto-fix. Surface findings; the author fixes. +- Don't run tests (there aren't any). +- Don't comment on style preferences not encoded in `CLAUDE.md` or this file. +- Don't second-guess existing code that's stable — review the **diff**, not the whole module. +- Don't carry context across invocations. Fresh every time. diff --git a/.claude/commands/check-secrets.md b/.claude/commands/check-secrets.md new file mode 100644 index 0000000..d26c32e --- /dev/null +++ b/.claude/commands/check-secrets.md @@ -0,0 +1,31 @@ +--- +description: Scan the working tree and recent git history for accidentally-committed BFL API keys, webhook secrets, or other credentials. +disable-model-invocation: true +allowed-tools: Read, Grep, Glob, Bash(rg:*), Bash(grep:*), Bash(git log:*), Bash(git show:*) +--- + +# `/check-secrets` — scan for committed credentials + +Catches the most common leakage paths for this project. + +## Workflow + +1. **Working tree scan**: + - `config.ini` — confirm `X_KEY = your-key` (the placeholder). Anything else is a finding. + - `rg -n 'X_KEY[[:space:]]*=[[:space:]]*[^[:space:]]+' .` (excluding `your-key`). + - `rg -n 'x-key[[:space:]]*:[[:space:]]*[A-Za-z0-9-_]{20,}' .` — header strings with embedded keys. + - `rg -n 'webhook_secret[[:space:]]*=[[:space:]]*[A-Za-z0-9-_]{16,}' .` — webhook secrets in code. + - `rg -n 'api\.bfl\.ai/v1/.*\?.*key=' .` — URL-embedded keys (rare but worth checking). +2. **Recent history scan** (last 20 commits): + - `git log --oneline -20 --all` + - For each commit: `git show --no-color -- config.ini` — verify `X_KEY` stayed `your-key`. +3. **Report**: + - List every finding as `file:line:context`. + - Distinguish working-tree findings (fixable now) from committed history findings (require `git filter-repo` or rewriting history — flag as urgent). +4. **Do not** print the full secret value. Truncate to first 6 chars + `...`. + +## Safety rails + +- Read-only. No edits, no rewrites, no `git filter-repo`. +- If a secret is found in committed history, **tell the user explicitly** that rotating the key is the first action — repo rewrites can come after. +- Do not push, do not commit. diff --git a/.claude/commands/commit.md b/.claude/commands/commit.md new file mode 100644 index 0000000..3fa1994 --- /dev/null +++ b/.claude/commands/commit.md @@ -0,0 +1,80 @@ +--- +description: Draft and create a Conventional Commits 1.0.0 git commit from the current diff. No Jira prefix, no AI attribution. BFL scope inferred from changed paths. Never auto-stages with -A; refuses on detected API keys. +disable-model-invocation: true +allowed-tools: Read, Bash(git status:*), Bash(git diff:*), Bash(git log -1:*), Bash(git add:*), Bash(git commit:*), Bash(git branch:*) +argument-hint: [type] [scope:name] [hint phrase] +--- + +# `/commit` — Conventional Commits 1.0.0 for ComfyUI-FLUX-BFL-API + +Drafts and creates a single Conventional Commits–compliant commit from the current diff. **Never auto-commits without showing the proposed message and the file list first.** + +## Convention authority + +- [Conventional Commits 1.0.0](https://www.conventionalcommits.org/en/v1.0.0/). +- Lowercase, imperative ("add", "fix", "rename", not "added"/"adds"), no period at end of subject, ≤ 72 chars. +- Do **NOT** inspect existing `git log` to derive style — historical commits used non-standard types like `update:`. The new standard starts now. +- Do **NOT** include any ticket/Jira prefix. +- **No AI attribution lines.** No `Co-Authored-By: Claude`, no "Generated with…" footer. Never. + +## Workflow + +1. Run `git status -s` and `git diff` (staged + unstaged) in parallel. +2. Read the diff. Classify the dominant change to pick `type`: + `feat | fix | refactor | chore | docs | style | perf | test | build | ci` + (use `$ARGUMENTS` if the user passed a hint). +3. Infer an optional `scope` from changed paths using the BFL module map: + - `nodes/api_node.py` → `api` + - `nodes/finetune.py` → `finetune` + - `nodes/config.py`, `nodes/config_node.py` → `config` + - `nodes/utils.py`, `nodes/base.py`, `nodes/status.py` → `utils` + - `web/js/*` → `web` + - `README.md`, `workflows/*` → `docs` + - `pyproject.toml` `version` bump only → `release` + - `.claude/*`, `CLAUDE.md` → `tooling` + If the change spans multiple modules with no dominant scope, omit scope entirely. +4. Write a one-line description: lowercase, imperative, no period, ≤ 72 chars. +5. If the change is non-trivial (>50 LOC or crosses modules), add a body explaining *why* (not *what*). Single blank line after the subject; wrap body at 72 chars. +6. If the change is breaking (renames a node registry key, changes RETURN_TYPES, changes config schema), append `!` to type/scope and add a `BREAKING CHANGE:` footer. +7. Stage explicitly selected files (NEVER `git add -A`, NEVER `git add .`). Default to only the files that match the inferred classification — show the user the proposed file list and ask "Confirm? (y/n)" before staging. +8. Loud warning if `git branch --show-current` returns `master` — `master`-direct commits are the current norm, but the warning makes the choice explicit. +9. Commit with a heredoc to preserve newlines: + + ```bash + git commit -m "$(cat <<'EOF' + [scope][!]: + + + + + EOF + )" + ``` + +## Safety rails + +- **Refuse** if any staged file matches `.env`, `.env.*` (except `.env.example`), `*.key`, `*.pem`, `secrets/`. +- **Refuse** if any staged change to `config.ini` sets `X_KEY` to anything other than `your-key`. (The PreToolUse hook already blocks the write, but check here too — older state on the filesystem could slip through.) +- **Refuse** if `git diff --cached` contains a value that looks like a real BFL API key — long alphanumeric/dash string assigned to `X_KEY` or to `x-key` header. +- **Refuse** `git commit --amend` unless the user explicitly types "amend" in this command's arguments. +- **Never** use `--no-verify`. If a pre-commit hook fails, fix the underlying issue. +- **Never** push. Pushing is a separate, explicit user action. + +## Example flow + +``` +User: /commit feat scope:api +You: Diff: nodes/api_node.py (+45 -2), README.md (+2 -0) + Proposed: + + feat(api): add Flux 3 generation node + + adds the Flux 3 model class with aspect_ratio support and the + standard config-override path; registers as Flux3_BFL and + updates the README node table. + + Staging: nodes/api_node.py, README.md + Confirm? (y/n) +``` + +Wait for `y` before staging + committing. On `n`, ask what to change and re-draft. diff --git a/.claude/commands/lint.md b/.claude/commands/lint.md new file mode 100644 index 0000000..da07246 --- /dev/null +++ b/.claude/commands/lint.md @@ -0,0 +1,23 @@ +--- +description: Run ruff format + ruff check --fix across the project using the pyproject.toml configuration. Reports what changed. +disable-model-invocation: true +allowed-tools: Bash(ruff:*), Bash(git diff:*), Bash(git status:*) +--- + +# `/lint` — ruff format + check across the project + +Runs the project's ruff configuration (`pyproject.toml` `[tool.ruff]`) once, fixes what it can, surfaces what it can't. + +## Workflow + +1. `command -v ruff >/dev/null` — if ruff is not on PATH, tell the user to `pip install -e .[dev]` (or `pip install ruff`) and stop. +2. `ruff format .` — auto-format. +3. `ruff check --fix .` — auto-fix lint rules in the selected set (E, F, I, W, UP, B). +4. `git status -s` — show files ruff changed. +5. If ruff reported diagnostics it could **not** fix, list them. The user decides whether to address now or skip. + +## Safety rails + +- Do not commit. The user runs `/commit` if they want to keep the formatting changes. +- Do not add or remove rules from `pyproject.toml` `[tool.ruff.lint]` `select` — that's a separate, explicit decision. +- Do not run with `--no-cache` or other non-default flags unless the user asks. diff --git a/.claude/commands/new-node.md b/.claude/commands/new-node.md new file mode 100644 index 0000000..aadeb21 --- /dev/null +++ b/.claude/commands/new-node.md @@ -0,0 +1,38 @@ +--- +description: Scaffold a new BFL FLUX model node — picks the right base class, builds INPUT_TYPES + generate_image, wires NODE_CLASS_MAPPINGS and NODE_DISPLAY_NAME_MAPPINGS, and updates the README node table. Drives the bfl-new-node skill. +allowed-tools: Read, Edit, Write, Grep, Glob, Bash(rg:*), Bash(grep:*) +argument-hint: [generation|finetune|utility] [model-endpoint-name] +--- + +# `/new-node` — scaffold a new BFL node + +Adds a new ComfyUI node that wraps a BFL FLUX endpoint, following every convention in `CLAUDE.md` (`_BFL` registry key, ` (BFL)` display name, `BaseFlux` inheritance, conditional argument inclusion, graceful blank-image fallback). + +This command **drives the `bfl-new-node` skill** — invoke it and follow its checklist. + +## What you should know before running + +- **ClassName** (Python identifier): bare, no `_BFL` suffix. Example: `Flux3`, `FluxKontextUltra`. +- **Kind**: `generation` (most common) → `BaseFlux`, category `"BFL"`. Or `finetune` → `BaseFinetuneFlux`, category `"BFL/Finetune"`. Or `utility` → no base class, category `"BFL/Utility"`. +- **Model endpoint name**: the path segment the BFL API expects, e.g. `flux-pro-1.1`, `flux-dev`, `flux-2-pro`. Used in `super().generate_image("", arguments, config)`. + +If any of those is missing from `$ARGUMENTS`, the skill asks once at the top. + +## Workflow + +Invoke the `bfl-new-node` skill. It will: + +1. Pick the right base class (`BaseFlux` / `BaseFinetuneFlux`) and category. +2. Decide where the new class goes (`nodes/api_node.py` for generation, `nodes/finetune.py` for finetune). +3. Write `INPUT_TYPES`: required block (`prompt`, dims OR `aspect_ratio`, model-specific params, `output_format`), optional block (`seed`, optional `image_prompt`, `webhook_url`, `webhook_secret`, `config: BFL_CONFIG`). +4. Write `generate_image(...)`: build `arguments` dict, conditional inclusion for `seed != -1` and non-empty optionals, call `super().generate_image("", arguments, config)`. Prefer `super().generate_image(...)` over inlining `post_request` + `get_result` — the base class runs dimension validation for you. +5. Register the class in the module's `NODE_CLASS_MAPPINGS` and `NODE_DISPLAY_NAME_MAPPINGS` with the `_BFL` / ` (BFL)` suffixes. +6. Add a row to the README node table under the right section (Generation / Finetune / Config / Utils). +7. Verify (see skill). + +## What this command does NOT do + +- Does NOT commit — user runs `/commit` after. +- Does NOT bump version — user runs `/version-bump` when releasing. +- Does NOT add a dependency. +- Does NOT touch `web/js/` — the new node renders with ComfyUI's default UI. diff --git a/.claude/commands/version-bump.md b/.claude/commands/version-bump.md new file mode 100644 index 0000000..5e8ad2a --- /dev/null +++ b/.claude/commands/version-bump.md @@ -0,0 +1,35 @@ +--- +description: Bump the package version in pyproject.toml (patch | minor | major) and suggest the matching Conventional Commits release commit message. +allowed-tools: Read, Edit, Bash(git diff:*) +argument-hint: patch | minor | major +--- + +# `/version-bump` — bump pyproject.toml version + +Edits a single field — `[project] version` in `pyproject.toml` — using semver. + +## Workflow + +1. Read current `version` from `pyproject.toml` (e.g. `1.0.13`). +2. Parse `$ARGUMENTS`: + - `patch` → bump the third number (`1.0.13` → `1.0.14`). + - `minor` → bump the second, reset patch (`1.0.13` → `1.1.0`). + - `major` → bump the first, reset minor + patch (`1.0.13` → `2.0.0`). + - If missing or invalid: ask once. +3. Edit `pyproject.toml`: + - `version = ""` → `version = ""`. +4. `git diff pyproject.toml` — confirm exactly one line changed. +5. Propose a commit message (do not run `git commit`): + + ``` + chore(release): bump version to + ``` + + Suggest the user run `/commit` (or `git commit -m '...' pyproject.toml`) when they're ready. + +## Safety rails + +- **Only** modify the `version` field. No description / license / dependency changes. +- Do not touch any other file (no CHANGELOG, no git tag, no push). +- Do not commit. +- If `version` is missing or unparseable, stop and ask the user. diff --git a/.claude/scripts/block-api-key-commit.sh b/.claude/scripts/block-api-key-commit.sh new file mode 100755 index 0000000..1bcbe07 --- /dev/null +++ b/.claude/scripts/block-api-key-commit.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +# PreToolUse(Edit|Write) hook — block writing a real X_KEY to config.ini. +# +# config.ini is committed with the placeholder value `your-key`. Users edit +# it locally after clone. If an Edit/Write would set X_KEY to anything other +# than `your-key` (or empty), refuse — the user almost certainly did not mean +# to commit their real BFL API key to the public repo. +# +# Exit 2 = block (PreToolUse honors exit 2 as a hard deny). +# Exit 0 = allow. + +set -euo pipefail + +input=$(cat) + +read -r file_path payload <<<"$(printf '%s' "$input" | python3 -c ' +import json, sys +try: + data = json.loads(sys.stdin.read()) + ti = data.get("tool_input", {}) or {} + fp = ti.get("file_path") or ti.get("path") or "" + # Edit uses new_string; Write uses content. Read whichever is set. + payload = ti.get("new_string") or ti.get("content") or "" + # Single-line, space-separated: file_path then base64-encoded payload to + # survive shell escaping. We base64 on the python side and decode below. + import base64 + b = base64.b64encode(payload.encode("utf-8")).decode("ascii") + print(fp, b) +except Exception: + print("", "") +' 2>/dev/null)" + +if [ -z "$file_path" ]; then + exit 0 +fi + +# Only inspect config.ini (any depth). +case "$file_path" in + *config.ini|*/config.ini) + ;; + *) + exit 0 + ;; +esac + +# Decode the payload and grep for X_KEY = . +content=$(printf '%s' "$payload" | python3 -c 'import sys,base64; sys.stdout.write(base64.b64decode(sys.stdin.read()).decode("utf-8","replace"))' 2>/dev/null || echo "") + +if [ -z "$content" ]; then + # Nothing to inspect (e.g. Edit with empty new_string) — allow. + exit 0 +fi + +# Match `X_KEY = ` (case-insensitive key, optional spaces). +value=$(printf '%s' "$content" | grep -iE '^[[:space:]]*X_KEY[[:space:]]*=' | head -1 | sed -E 's/^[[:space:]]*[Xx]_[Kk][Ee][Yy][[:space:]]*=[[:space:]]*//' | sed -E 's/[[:space:]]+$//') + +if [ -z "$value" ] || [ "$value" = "your-key" ]; then + exit 0 +fi + +cat >&2 </dev/null || echo "") + +if [ -z "$file_path" ]; then + exit 0 +fi + +case "$file_path" in + *.py) ;; + *) exit 0 ;; +esac + +case "$file_path" in + *__pycache__*|*/.venv/*|*/venv/*) exit 0 ;; +esac + +project_dir="${CLAUDE_PROJECT_DIR:-$(pwd)}" +cd "$project_dir" || exit 0 + +if ! command -v ruff >/dev/null 2>&1; then + printf '[post-edit-ruff] ruff not on PATH — skipping format/check on %s\n' "$file_path" >&2 + exit 0 +fi + +rel="${file_path#${project_dir}/}" + +ruff format "$rel" >&2 || true +ruff check --fix "$rel" >&2 || true +exit 0 diff --git a/.claude/scripts/session-start-context.sh b/.claude/scripts/session-start-context.sh new file mode 100755 index 0000000..022d64e --- /dev/null +++ b/.claude/scripts/session-start-context.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# SessionStart hook. +# +# Emits current git branch, last 5 commits, and short status as +# hookSpecificOutput.additionalContext so the new session knows +# exactly where it's resuming. +# +# Read-only. Exits 0 always — SessionStart cannot block on exit 2. + +set -euo pipefail + +cd "${CLAUDE_PROJECT_DIR:-$(pwd)}" + +if ! command -v git >/dev/null 2>&1 || ! git rev-parse --git-dir >/dev/null 2>&1; then + exit 0 +fi + +branch=$(git branch --show-current 2>/dev/null || echo "(detached)") +log=$(git log --oneline -5 2>/dev/null || echo "(no commits)") +status=$(git status --short 2>/dev/null | head -10 || echo "") +status_count=$(printf '%s\n' "$status" | sed '/^$/d' | wc -l | tr -d ' ') + +context=$(printf 'BFL git context (auto-injected at session start):\n\nBranch: %s\n\nRecent commits:\n%s\n\nWorking tree (%s files changed):\n%s\n' \ + "$branch" "$log" "$status_count" "${status:-(clean)}") + +if command -v jq >/dev/null 2>&1; then + jq -n --arg ctx "$context" '{ + hookSpecificOutput: { + hookEventName: "SessionStart", + additionalContext: $ctx + } + }' +else + escaped=$(printf '%s' "$context" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()), end="")' 2>/dev/null || printf '"%s"' "$(printf '%s' "$context" | sed 's/\\/\\\\/g; s/"/\\"/g; s/$/\\n/' | tr -d '\n')") + printf '{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":%s}}\n' "$escaped" +fi + +exit 0 diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..fbf6714 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,38 @@ +{ + "worktree": { "bgIsolation": "none" }, + "hooks": { + "SessionStart": [ + { + "matcher": "startup|resume|clear|compact", + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PROJECT_DIR}/.claude/scripts/session-start-context.sh" + } + ] + } + ], + "PreToolUse": [ + { + "matcher": "Edit|Write", + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PROJECT_DIR}/.claude/scripts/block-api-key-commit.sh" + } + ] + } + ], + "PostToolUse": [ + { + "matcher": "Edit|Write", + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PROJECT_DIR}/.claude/scripts/post-edit-ruff.sh" + } + ] + } + ] + } +} diff --git a/.claude/skills/bfl-api-pattern/SKILL.md b/.claude/skills/bfl-api-pattern/SKILL.md new file mode 100644 index 0000000..3a67942 --- /dev/null +++ b/.claude/skills/bfl-api-pattern/SKILL.md @@ -0,0 +1,90 @@ +--- +name: bfl-api-pattern +description: Reusable recipe for calling any BFL FLUX endpoint that follows the async POST → poll pattern. Use when integrating a new endpoint not covered by the existing BaseFlux methods, or when explaining how the existing flow works. +when_to_use: User asks "how does the BFL polling work?", "I want to call a new BFL endpoint that BaseFlux doesn't cover", "explain the request flow", or is debugging a 4xx/5xx response from BFL. +allowed-tools: Read, Grep, Glob +--- + +# BFL API request pattern + +Every BFL FLUX generation / edit / fill / expand endpoint follows the same async shape. This skill documents it, and points to the existing implementation in `BaseFlux` (`nodes/base.py`). + +**You should not write a new copy of this pattern**. The base class already implements it. This skill exists to (a) explain the existing flow to humans and (b) handle the rare case where a new endpoint needs a one-off variant (e.g. multipart upload, streaming response). + +## The flow + +1. **POST** to `/` with body JSON. Headers: `x-key: `, `Content-Type: application/json`. +2. **Response**: `{ "id": "" }` (or an error JSON if BFL rejects). +3. **Poll**: GET `/get_result?id=` every **5 s**, max **40 attempts** (~200 s ceiling). Headers: same `x-key`. +4. **Status enum** (`nodes/status.py`): + - `"Pending"` → retry. + - `"Ready"` → response contains `result.sample` URL — download, convert to PIL, then tensor. + - `"Error"` / `"Content Moderated"` / `"Request Moderated"` → terminal failure; return blank 512×512 black image. + - `"Task not found"` → shouldn't happen mid-flow; treat as terminal. +5. **Image download & convert** (`BaseFlux.process_result`): + - GET the `result.sample` URL (no auth needed; it's a signed S3-style URL). + - Open with PIL. + - Re-encode in the requested `output_format` (`jpeg` / `png`). + - Convert to numpy float32 array `/ 255.0`. + - Wrap in torch tensor with leading batch dim: `torch.from_numpy(arr)[None,]`. + - Return as a 1-tuple matching `RETURN_TYPES = ("IMAGE",)`. + +## Where this lives + +| Step | Location | +|---|---| +| Build URL from base + path | `nodes/config.py:44-65` (`ConfigLoader.create_url`) | +| POST + curl print | `nodes/base.py:50-73` (`BaseFlux.post_request`) | +| Poll loop | `nodes/base.py:75-133` (`BaseFlux.get_result`) | +| Image → tensor | `nodes/base.py:19-38` (`BaseFlux.process_result`) | +| Graceful failure tensor | `nodes/base.py:40-44` (`BaseFlux.create_blank_image`) | +| Dimension check | `nodes/base.py:46-48` (`BaseFlux.check_multiple_of_32`) | +| Status enum | `nodes/status.py` | + +## Headers + +```python +headers = {"x-key": config_loader_instance.get_x_key()} +``` + +That's it. No `Authorization: Bearer`, no signed tokens. The `x-key` value is whatever's in `config.ini` `X_KEY` (or the override from `FluxConfig_BFL`). + +## curl-equivalent print + +Every outbound POST prints a copy-pasteable curl command before sending. Pattern from `nodes/base.py:60-62`: + +```python +headers_str = " \\\n ".join(f"-H '{k}: {v}'" for k, v in prepared.headers.items()) +print(f"[BFL] curl -X POST '{prepared.url}' \\\n {headers_str} \\\n -d '{body}'") +``` + +This is intentional. Users who hit BFL errors paste the curl into a terminal to confirm whether the issue is the request or the response handling. + +**Caveat**: this prints the `x-key` header. That's a known trade-off — the user is the only one looking at their own console. Do not log this header value to a file or send it anywhere. + +## Regional endpoints (finetune only) + +Default `BASE_URL` is `https://api.bfl.ai/v1/` and does NOT serve finetune routes. Finetune endpoints live at: +- `https://api.us.bfl.ai/v1/` +- `https://api.eu.bfl.ai/v1/` + +`ConfigLoader.create_url(path, region=...)` (`nodes/config.py:44-65`) picks the right base. Region comes from `FluxConfig_BFL`'s `region` field or from the per-call argument. + +## When you'd need to deviate from `BaseFlux` + +You should reuse `BaseFlux` unless the endpoint: +- Uses multipart/form-data (not currently a thing in BFL). +- Streams the response (not currently a thing in BFL). +- Returns a non-image result that doesn't fit the `IMAGE` tensor (see `FluxCredits` in `nodes/api_node.py` — utility node, doesn't inherit BaseFlux, calls `requests.get` directly). +- Polls at a different interval (BFL doesn't currently require this). + +If you really do need a one-off, mirror the existing patterns: +1. Print `[BFL] curl ...` before any outbound request. +2. Print `[BFL] ` lines. +3. On any exception or terminal status, return `self.create_blank_image()` (if `BaseFlux` subclass) or an equivalent safe value. + +## What this skill does NOT do + +- Does NOT write a new copy of `post_request` / `get_result`. The base class is the single source of truth. +- Does NOT change polling timing without explicit user ask. +- Does NOT log the `x-key` value to anywhere other than the user's console. diff --git a/.claude/skills/bfl-new-node/SKILL.md b/.claude/skills/bfl-new-node/SKILL.md new file mode 100644 index 0000000..087d124 --- /dev/null +++ b/.claude/skills/bfl-new-node/SKILL.md @@ -0,0 +1,141 @@ +--- +name: bfl-new-node +description: Scaffold a new ComfyUI node that wraps a BFL FLUX endpoint — picks the right base class, writes INPUT_TYPES + generate_image, registers the class in NODE_CLASS_MAPPINGS / NODE_DISPLAY_NAME_MAPPINGS, and updates the README. Follow this when adding a Flux model, finetune variant, or utility node. +when_to_use: User says "new node", "add a Flux N model", "wrap the X endpoint", "scaffold a BFL node", "add a finetune node", or invokes the /new-node command. +allowed-tools: Read, Edit, Write, Grep, Glob, Bash(rg:*), Bash(grep:*) +argument-hint: [generation|finetune|utility] [endpoint-name] +--- + +# BFL new-node scaffold + +Adds a single ComfyUI node that follows every convention in `CLAUDE.md`. This skill **writes files**. + +## Decisions you make up front + +If `$ARGUMENTS` doesn't already say: + +1. **ClassName** (Python identifier, bare, no `_BFL` suffix). Example: `Flux3`, `FluxPro2Ultra`. +2. **Kind**: + - `generation` → inherits `BaseFlux`, category `"BFL"`, lives in `nodes/api_node.py`. + - `finetune` → inherits `BaseFinetuneFlux`, category `"BFL/Finetune"`, lives in `nodes/finetune.py`. + - `utility` → inherits `object`, category `"BFL/Utility"` or `"BFL/Utils"`, lives in `nodes/utils.py` or a new file. +3. **Endpoint name** (generation/finetune only): the BFL API path segment, e.g. `flux-pro-1.1`, `flux-dev`, `flux-2-pro`. Used in `super().generate_image("", arguments, config)`. +4. **Dimension style** (generation only): + - `dims` → required `width` / `height` ints (multiples of 32). Older Flux models. + - `aspect_ratio` → required `aspect_ratio` enum string. Newer Flux 2 / Ultra family. + - Pick one. Do not mix. + +If any of those is missing, ask once at the top before scaffolding. + +## What you write + +### Class body + +For a `generation` node with `dims`: + +```python +class (BaseFlux): + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "prompt": ("STRING", {"default": "", "multiline": True}), + "width": ("INT", {"default": 1024, "min": 256, "max": 1440}), + "height": ("INT", {"default": 1024, "min": 256, "max": 1440}), + "prompt_upsampling": ("BOOLEAN", {"default": False}), + "safety_tolerance": ("INT", {"default": 2, "min": 0, "max": 6}), + "output_format": (["jpeg", "png"], {"default": "jpeg"}), + }, + "optional": { + "seed": ("INT", {"default": -1}), + "image_prompt": ("STRING", {"default": ""}), + "webhook_url": ("STRING", {"default": ""}), + "webhook_secret": ("STRING", {"default": ""}), + "config": ("BFL_CONFIG",), + }, + } + + def generate_image(self, prompt, width, height, prompt_upsampling, safety_tolerance, output_format, + seed=-1, image_prompt="", webhook_url="", webhook_secret="", config=None): + arguments = { + "prompt": prompt, + "width": width, + "height": height, + "prompt_upsampling": prompt_upsampling, + "safety_tolerance": safety_tolerance, + "output_format": output_format, + } + if seed != -1: + arguments["seed"] = seed + if image_prompt: + arguments["image_prompt"] = image_prompt + if webhook_url: + arguments["webhook_url"] = webhook_url + if webhook_secret: + arguments["webhook_secret"] = webhook_secret + return super().generate_image("", arguments, config) +``` + +For `aspect_ratio` instead of dims, replace the dims block in `INPUT_TYPES` with: + +```python +"aspect_ratio": (["16:9", "4:3", "1:1", "3:2", "21:9", "9:16", "3:4", "2:3", "9:21"], {"default": "16:9"}), +``` + +…and in `generate_image`, replace the `"width"`, `"height"` lines with `"aspect_ratio": aspect_ratio`. + +**Always prefer `super().generate_image(...)`** over inlining `self.post_request` + `self.get_result` — the base class runs `check_multiple_of_32` for you when dims are present. (Some Flux 2 nodes inline both, but for new code the super call is shorter and safer.) + +### Registration + +At the bottom of the target module, in the existing dicts: + +```python +NODE_CLASS_MAPPINGS = { + ..., + "_BFL": , +} + +NODE_DISPLAY_NAME_MAPPINGS = { + ..., + "_BFL": " (BFL)", +} +``` + +The `_BFL` suffix on the dict key is mandatory. The ` (BFL)` suffix on the display name is mandatory. The Python class name itself is bare — no `_BFL`. + +### README update + +Append a row to the right table in `README.md`: +- Generation → "### Generation" table +- Finetune → "### Finetune" table +- Utility → "### Utils" or "### Config" table (whichever matches CATEGORY) + +Format: `| (BFL) | |` + +### `__init__.py` (only if new module file) + +If you create a new file under `nodes/`, append its module name (without `.py`) to `node_list` in `__init__.py:4-9`. If you're adding to an existing module (`api_node.py`, `finetune.py`, `utils.py`, `config_node.py`), do nothing. + +## Verification before declaring done + +1. `ruff check nodes/.py` — clean (the post-edit hook also runs this). +2. The new entry appears in BOTH `NODE_CLASS_MAPPINGS` AND `NODE_DISPLAY_NAME_MAPPINGS` with **matching keys**. +3. The registry key ends in `_BFL` and the display name ends in ` (BFL)`. +4. The new class inherits from the right base (`BaseFlux` / `BaseFinetuneFlux` / `object`). +5. `seed` is in `optional` with default `-1`, and the request body only includes it when `!= -1`. +6. `config: BFL_CONFIG` is in `optional` and is passed through to `super().generate_image(...)`. +7. If new module file was created, it's in `__init__.py`'s `node_list`. +8. README node table includes the new row. + +## What this skill does NOT do + +- Does NOT commit. The user runs `/commit` after. +- Does NOT bump version. The user runs `/version-bump` when releasing. +- Does NOT touch `web/js/` — the default ComfyUI UI renders the new node automatically. +- Does NOT add dependencies. +- Does NOT modify `nodes/base.py` — the base class is stable; do not "improve" it while here. + +## Argument hint + +`$ARGUMENTS` may carry: `ClassName`, kind (`generation` / `finetune` / `utility`), and endpoint name. Missing fields → ask once before scaffolding. diff --git a/.gitignore b/.gitignore index 82f9275..518cfe8 100644 --- a/.gitignore +++ b/.gitignore @@ -160,3 +160,8 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ + +# Claude Code runtime artifacts +.claude/cache/ +.claude/artifacts/ + diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..089931a --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,115 @@ + + +# CLAUDE.md + +> **Attribution.** The four behavioral rules below ("Think Before Coding", "Simplicity First", "Surgical Changes", "Goal-Driven Execution") are reproduced **verbatim** from Andrej Karpathy's [`CLAUDE.md`](https://raw.githubusercontent.com/forrestchang/andrej-karpathy-skills/main/CLAUDE.md), distributed via the `forrestchang/andrej-karpathy-skills` repository (transferred to [`multica-ai/andrej-karpathy-skills`](https://github.com/multica-ai/andrej-karpathy-skills) in May 2026). Used verbatim with attribution; no edits to wording. Project-specific rules follow under **`## BFL Project Rules`**. + +Behavioral guidelines to reduce common LLM coding mistakes. Merge with project-specific instructions as needed. + +**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment. + +## 1. Think Before Coding + +**Don't assume. Don't hide confusion. Surface tradeoffs.** + +Before implementing: +- State your assumptions explicitly. If uncertain, ask. +- If multiple interpretations exist, present them - don't pick silently. +- If a simpler approach exists, say so. Push back when warranted. +- If something is unclear, stop. Name what's confusing. Ask. + +## 2. Simplicity First + +**Minimum code that solves the problem. Nothing speculative.** + +- No features beyond what was asked. +- No abstractions for single-use code. +- No "flexibility" or "configurability" that wasn't requested. +- No error handling for impossible scenarios. +- If you write 200 lines and it could be 50, rewrite it. + +Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify. + +## 3. Surgical Changes + +**Touch only what you must. Clean up only your own mess.** + +When editing existing code: +- Don't "improve" adjacent code, comments, or formatting. +- Don't refactor things that aren't broken. +- Match existing style, even if you'd do it differently. +- If you notice unrelated dead code, mention it - don't delete it. + +When your changes create orphans: +- Remove imports/variables/functions that YOUR changes made unused. +- Don't remove pre-existing dead code unless asked. + +The test: Every changed line should trace directly to the user's request. + +## 4. Goal-Driven Execution + +**Define success criteria. Loop until verified.** + +Transform tasks into verifiable goals: +- "Add validation" → "Write tests for invalid inputs, then make them pass" +- "Fix the bug" → "Write a test that reproduces it, then make it pass" +- "Refactor X" → "Ensure tests pass before and after" + +For multi-step tasks, state a brief plan: +``` +1. [Step] → verify: [check] +2. [Step] → verify: [check] +3. [Step] → verify: [check] +``` + +Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification. + +--- + +**These guidelines are working if:** fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, and clarifying questions come before implementation rather than after mistakes. + +--- + +## BFL Project Rules + +This is a **ComfyUI custom-nodes package** that wraps the **Black Forest Labs (BFL) FLUX** image-generation API. Single Python package, ~1000 LOC, no test framework, ruff for lint/format. + +### Always do + +- **Inheritance**: every new generation node inherits from `BaseFlux` (`nodes/base.py`). Every finetune node inherits from `BaseFinetuneFlux` (same file). The base class owns `post_request` / `get_result` / `process_result` / `create_blank_image` / `check_multiple_of_32` — do **not** reimplement those. +- **Node registration**: + - Python class name is bare (e.g., `FluxPro11`, `FluxDev`). The one exception is `FluxConfig_BFL` where the class name matches its registry key for historical reasons. + - ComfyUI **registry key** in `NODE_CLASS_MAPPINGS` ends with `_BFL`: `"FluxPro11_BFL": FluxPro11`. + - ComfyUI **display name** in `NODE_DISPLAY_NAME_MAPPINGS` ends with ` (BFL)`: `"FluxPro11_BFL": "Flux Pro 1.1 (BFL)"`. + - `CATEGORY` is one of `"BFL"` (generation), `"BFL/Finetune"`, `"BFL/Utility"`, `"BFL/Utils"`, `"BFL/Config"`. Match the existing module's choice. + - A brand-new module file under `nodes/` is invisible until its name is appended to `node_list` in `__init__.py:4-9`. +- **Logging**: every outbound request and every status line is prefixed `[BFL] ` so it's grep-able in the ComfyUI console. +- **curl-equivalent**: every POST prints the full curl-equivalent (URL, headers, body) before sending. Matches the pattern in `nodes/base.py:60-62`. Useful for users debugging the BFL API directly. +- **Width / height validation**: when a node takes `width` / `height` ints, the request path runs `self.check_multiple_of_32(width, height)` (already called by `BaseFlux.generate_image` if they're in `arguments`). Newer Flux 2 / Ultra models use `aspect_ratio` strings (`"16:9"`, `"1:1"`, …) and bypass dimension validation. +- **Seed handling**: `seed = -1` means *random*. The node parameter defaults to `-1`. Only insert `arguments["seed"] = seed` into the request body when `seed != -1` — never send `-1` to BFL. +- **Optional argument inclusion**: build the request `arguments` dict with required keys, then conditionally add optional ones only when non-default / non-empty (`if webhook_url:`, `if image_prompt:`, …). Matches every node in `nodes/api_node.py`. +- **Graceful failure**: on any unhandled exception, on terminal API status (`Error`, `Content Moderated`, `Request Moderated`), or on polling exhaustion, return `self.create_blank_image()` — a black 512×512 IMAGE tensor. ComfyUI workflows must never crash because of a BFL failure. +- **Polling**: 5-second interval, max 40 attempts (~200 s ceiling). Already implemented in `BaseFlux.get_result` — do not write a second polling loop in a subclass. +- **Config resolution**: every generation node accepts an optional `config` input of type `BFL_CONFIG` (the output of `FluxConfig_BFL`). Pass it through to `super().generate_image(url_path, arguments, config)`; the base class hands it to `get_config_loader(config)` from `nodes/config_node.py:57-67`, which decides whether to use the `config.ini`-backed default loader or build one from the override. **Both paths must work** — `config=None` (use file) and `config=` (override). +- **Conventional Commits 1.0.0**: `[scope][!]: `. Types: `feat`, `fix`, `refactor`, `chore`, `docs`, `style`, `perf`, `test`, `build`, `ci`. Scopes: `api`, `finetune`, `config`, `utils`, `web`, `docs`, `release`, `tooling`. Description: lowercase, imperative ("add", not "added"/"adds"), no trailing period, ≤ 72 chars. Body explains *why*, wrapped at 72. Use `!` after type/scope for breaking changes. No Jira prefix. **No `Co-Authored-By: Claude` / "Generated with…" footer.** Do NOT inspect `git log` to derive style — historical commits used `update:` and similar non-standard types; the new standard starts now. +- **Version bumps**: edit `pyproject.toml` `version` field only. Commit message: `chore(release): bump version to X.Y.Z`. + +### Never do + +- **Never** commit a real `X_KEY` in `config.ini`. The tracked placeholder must stay `your-key`. A PreToolUse hook (`block-api-key-commit.sh`) blocks Edit/Write to `config.ini` with any other value. If the user needs a real key locally, they edit it after clone and the placeholder stays in the repo. +- **Never** log the full `x-key` header value, webhook secret, or BFL API response body that contains either. +- **Never** call BFL endpoints from `nodes/utils.py` or other helper modules. Network I/O lives inside generation / finetune / credits node classes only. +- **Never** bypass hooks with `--no-verify`. Fix the underlying issue. +- **Never** auto-commit. The user runs `/commit`. +- **Never** add a dependency to `requirements.txt` / `pyproject.toml` without an explicit user ask — the package currently depends only on `torch` (transitively on `requests`, `Pillow`, `numpy` via ComfyUI). Keep it that way. +- **Never** edit `web/js/` "while I'm here". The ComfyUI frontend extensions are sensitive to ComfyUI version drift; touch them only when the user asks for a UI change. + +### Gotchas + +- `ConfigLoader` in `nodes/config.py:85-86` constructs a module-level singleton `config_loader` at import time. Some nodes still reference that singleton; new code should use `get_config_loader(config)` from `nodes/config_node.py:57-67` (which respects the per-call override) instead. +- `BASE_URL` in `config.ini` may or may not end with `/`. `ConfigLoader.create_url` uses `urljoin` — pass the `path` argument **without** a leading slash, otherwise `urljoin` will strip the existing prefix path. +- **Finetune endpoints are regional**: `api.us.bfl.ai`, `api.eu.bfl.ai`. The default `api.bfl.ai/v1/` does **not** serve finetune routes. Region selection is via `FluxConfig_BFL`'s `region` field or per-call. See `nodes/finetune.py`. +- BFL's async pattern: POST to `flux-pro-1.1` (etc.) returns `{"id": ""}`. Polling GET goes to `get_result?id=` — note: **query parameter `id`**, not a path segment. `BaseFlux.get_result` already builds this correctly; if you write a one-off polling call, match it. +- The newer Flux 2 / Klein nodes (`nodes/api_node.py` lower half) sometimes inline `self.post_request` + `self.get_result` instead of calling `super().generate_image(...)`. Both styles exist in the codebase; if you're scaffolding a new node, **prefer `super().generate_image(...)`** — it's shorter and runs the dimension check for you. +- `__init__.py` does dynamic import of every entry in `node_list`. A new node file in `nodes/.py` is invisible to ComfyUI until you append `""` to that list. +- `WEB_DIRECTORY = "./web"` in `__init__.py:21` exposes `web/js/*` to ComfyUI's frontend. Renaming or moving `web/` breaks the frontend extensions silently — they just won't load. diff --git a/pyproject.toml b/pyproject.toml index 1bf5a79..8340a8b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,6 +5,9 @@ version = "1.0.13" license = {file = "LICENSE"} dependencies = ["torch"] +[project.optional-dependencies] +dev = ["ruff>=0.5.0"] + [project.urls] Repository = "https://github.com/gelasdev/ComfyUI-FLUX-BFL-API" # Used by Comfy Registry https://comfyregistry.org @@ -12,4 +15,14 @@ Repository = "https://github.com/gelasdev/ComfyUI-FLUX-BFL-API" [tool.comfy] PublisherId = "gelas" DisplayName = "ComfyUI-FLUX-BFL-API" -Icon = "" \ No newline at end of file +Icon = "" + +[tool.ruff] +line-length = 120 +target-version = "py310" + +[tool.ruff.lint] +select = ["E", "F", "I", "W", "UP", "B"] + +[tool.ruff.lint.per-file-ignores] +"__init__.py" = ["F401"]