chore(tooling): add claude code config and ruff setup

introduces project-level claude code tooling — CLAUDE.md behavioral
rules and BFL conventions, agents/commands/skills under .claude/, and
hooks that block accidental config.ini API-key commits and run ruff
after edits. wires ruff into pyproject.toml as a dev extra with the
project's lint/format config, and ignores claude runtime artifacts.
This commit is contained in:
Çağan Kiraz
2026-05-25 15:44:38 +03:00
parent bdd256d5c9
commit ba3a4964db
16 changed files with 979 additions and 1 deletions
+92
View File
@@ -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 `<RegistryKey>`?"
- "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: <restated in one line>
Target: <identifier / pattern / scope>
### 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)
- <e.g., "newer Flux 2 nodes inline post_request/get_result instead of calling super().generate_image">
```
## 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 <field>: arguments[<field>] = <field>` 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.
+115
View File
@@ -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["<ClassName>_BFL"] = <ClassName>`
- `NODE_DISPLAY_NAME_MAPPINGS["<ClassName>_BFL"] = "<Human Readable Name> (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("<endpoint>", 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 `<type>[scope][!]: <subject>` with valid type. Lowercase, imperative, ≤ 72 chars, no period.
## Output format
```
## BFL Node Review
Scope: <line — what you reviewed>
### Findings (N)
1. [CRITICAL|HIGH|MEDIUM|LOW] file:line — description
Fix: concrete suggestion
2. ...
### Verdict
<Ready to Merge | Needs Attention | Needs Work> — 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.
+31
View File
@@ -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 <sha> -- 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.
+80
View File
@@ -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'
<type>[scope][!]: <subject>
<optional body explaining why>
<optional BREAKING CHANGE: footer>
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.
+23
View File
@@ -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.
+38
View File
@@ -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: <ClassName> [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("<endpoint>", 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("<endpoint>", 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.
+35
View File
@@ -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 = "<old>"` → `version = "<new>"`.
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 <new>
```
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.
+75
View File
@@ -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 = <value>.
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 = <value>` (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 <<EOF
Refusing to write a real X_KEY value to config.ini.
Tracked value must stay the placeholder \`your-key\`. Real BFL API keys are
user-local — set them after clone, never commit them.
Detected value (truncated): ${value:0:6}...
To override locally (NOT committed), edit config.ini after the file is
written and keep that change off git. To bypass this hook for a legitimate
non-key edit, write the file with X_KEY = your-key in the new content.
See CLAUDE.md "Never commit a real X_KEY in config.ini".
EOF
exit 2
+49
View File
@@ -0,0 +1,49 @@
#!/usr/bin/env bash
# PostToolUse(Edit|Write) hook — auto-ruff after Python edits.
#
# Runs `ruff format` + `ruff check --fix` on the edited file using the
# project's pyproject.toml configuration. Skips __pycache__ and .venv.
# Advisory: ruff diagnostics surface back via stderr, but we exit 0
# either way (post-edit lint feedback should not re-block a successful
# write). If ruff is not installed, exit 0 with a one-line notice.
set -euo pipefail
input=$(cat)
file_path=$(printf '%s' "$input" | python3 -c '
import json, sys
try:
data = json.loads(sys.stdin.read())
ti = data.get("tool_input", {}) or {}
print(ti.get("file_path") or ti.get("path") or "")
except Exception:
print("")
' 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
+38
View File
@@ -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
+38
View File
@@ -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"
}
]
}
]
}
}
+90
View File
@@ -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 `<BASE_URL>/<endpoint>` with body JSON. Headers: `x-key: <API_KEY>`, `Content-Type: application/json`.
2. **Response**: `{ "id": "<task_id>" }` (or an error JSON if BFL rejects).
3. **Poll**: GET `<BASE_URL>/get_result?id=<task_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/<path>`
- `https://api.eu.bfl.ai/v1/<path>`
`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] <status>` 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.
+141
View File
@@ -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: <ClassName> [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("<endpoint>", 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 <ClassName>(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("<endpoint>", 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 = {
...,
"<ClassName>_BFL": <ClassName>,
}
NODE_DISPLAY_NAME_MAPPINGS = {
...,
"<ClassName>_BFL": "<Human Readable Name> (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: `| <Display Name> (BFL) | <one-sentence description> |`
### `__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/<modified-file>.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.
+5
View File
@@ -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/
+115
View File
@@ -0,0 +1,115 @@
<!-- DO NOT REGENERATE via /init — manually maintained -->
# 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=<dict>` (override).
- **Conventional Commits 1.0.0**: `<type>[scope][!]: <description>`. 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": "<task_id>"}`. Polling GET goes to `get_result?id=<task_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/<name>.py` is invisible to ComfyUI until you append `"<name>"` 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.
+14 -1
View File
@@ -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 = ""
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"]