@homie-rocks/studio 0.51.4 → 0.51.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +10 -29
  3. package/apps/APPS.md +16 -1
  4. package/bin/homie-studio.mjs +28 -30
  5. package/guides/app/NEEDS.md +160 -0
  6. package/guides/app/SKILL.md +20 -4
  7. package/guides/art/SKILL.md +68 -140
  8. package/guides/art/scripts/art.mjs +6 -79
  9. package/guides/models/SKILL.md +73 -177
  10. package/guides/models/references/RIGHTS.md +1 -1
  11. package/guides/models/scripts/models.mjs +7 -288
  12. package/guides/music/SKILL.md +74 -183
  13. package/guides/music/references/COMPOSITION.md +21 -26
  14. package/guides/music/references/GAME-AUDIO.md +3 -3
  15. package/guides/music/references/RIGHTS.md +12 -15
  16. package/guides/music/scripts/music.mjs +11 -202
  17. package/guides/parts/SKILL.md +1 -1
  18. package/guides/shop/SKILL.md +68 -77
  19. package/guides/sound/SKILL.md +70 -1
  20. package/guides/sound/scripts/player/sound.js +233 -0
  21. package/guides/studio-setup/SKILL.md +11 -27
  22. package/guides/studio-setup/references/OFFICIAL-MEDIA.md +66 -0
  23. package/guides/style/SKILL.md +66 -3
  24. package/guides/video/SKILL.md +83 -0
  25. package/guides/video/references/DELIVERY.md +73 -0
  26. package/guides/video/references/HONESTY.md +40 -0
  27. package/guides/video/references/METHOD.md +144 -0
  28. package/guides/video/references/RECORD.md +137 -0
  29. package/guides/video/references/TRAILER.md +154 -0
  30. package/guides/video/references/examples/site-walkthrough.json +14 -0
  31. package/guides/video/references/examples/studio-play-phone.json +15 -0
  32. package/guides/video/references/examples/studio-play.json +17 -0
  33. package/guides/video/scripts/capture-game.mjs +175 -0
  34. package/guides/video/scripts/clock.js +136 -0
  35. package/guides/video/scripts/drawover/film.js +58 -0
  36. package/guides/video/scripts/drawover/index.html +9 -0
  37. package/guides/video/scripts/drawover/look.js +115 -0
  38. package/guides/video/scripts/drawover/shots.json +12 -0
  39. package/guides/video/scripts/lib/browser.mjs +87 -0
  40. package/guides/video/scripts/lib/frames.mjs +170 -0
  41. package/guides/video/scripts/lib/remix.mjs +159 -0
  42. package/guides/video/scripts/qa.mjs +127 -0
  43. package/guides/video/scripts/record-fixed.mjs +403 -0
  44. package/guides/video/scripts/record-page.mjs +477 -0
  45. package/guides/video/scripts/tap.js +108 -0
  46. package/guides/video/scripts/video.mjs +888 -0
  47. package/lib/app-needs.mjs +86 -18
  48. package/lib/app-ready.mjs +21 -0
  49. package/lib/build.mjs +12 -9
  50. package/lib/check.mjs +16 -7
  51. package/lib/cloudflare.mjs +6 -11
  52. package/lib/codex.mjs +7 -5
  53. package/lib/dev.mjs +1 -2
  54. package/lib/doctor.mjs +10 -54
  55. package/lib/mcp-tools.mjs +42 -21
  56. package/lib/mcp.mjs +1 -1
  57. package/lib/media-import.mjs +41 -0
  58. package/lib/media-workflows.mjs +13 -0
  59. package/lib/needs-headless.mjs +34 -0
  60. package/lib/official-media.mjs +15 -0
  61. package/lib/rules-build.mjs +13 -5
  62. package/lib/scaffold.mjs +2 -2
  63. package/lib/shop-links.mjs +81 -19
  64. package/lib/shop-listen.mjs +43 -0
  65. package/lib/shop-local.mjs +1 -1
  66. package/lib/shop-mcp.mjs +58 -0
  67. package/lib/stripe-connect.mjs +36 -15
  68. package/lib/template-history.json +87 -0
  69. package/lib/upgrade-media.mjs +13 -0
  70. package/lib/upgrade.mjs +10 -0
  71. package/package.json +1 -1
  72. package/parts/PARTS.md +1 -1
  73. package/rules/view.ts +4 -0
  74. package/shop/SHOP.md +110 -34
  75. package/worker/app-records.mjs +1 -0
  76. package/worker/build-id.mjs +1 -1
  77. package/worker/event-resources.mjs +9 -3
  78. package/worker/index.mjs +5 -1
  79. package/worker/local-test.mjs +4 -0
  80. package/worker/mcp-events.mjs +2 -2
  81. package/worker/mcp-tools.mjs +19 -1
  82. package/worker/mcp.mjs +1 -1
  83. package/worker/purchase-reconciliation.mjs +16 -1
  84. package/worker/shop-links.mjs +1 -1
  85. package/worker/shop-rules.mjs +3 -0
  86. package/worker/shop-subscriptions.mjs +2 -0
  87. package/worker/shop.mjs +58 -8
  88. package/worker/stripe-ownership.mjs +7 -0
  89. package/worker/version.mjs +1 -1
  90. package/guides/models/references/models.json +0 -175
  91. package/guides/music/scripts/lib/eleven.mjs +0 -260
  92. package/lib/projects-env.mjs +0 -68
  93. package/lib/projects.mjs +0 -311
  94. package/lib/provider-key.mjs +0 -21
@@ -1,154 +1,82 @@
1
1
  ---
2
2
  name: art
3
- description: Make a studio's game look like something at build time — a cover from a real frame of the game (free), painted covers, backdrops, textures and character plates from image models through the creator's OWN fal account under a hard budget with a receipt for every call, checked (does the texture tile, is the file small enough for a phone, does the cover promise only what the game contains) and loaded by the game — plus the art-direction method (a style sheet and paintovers of real frames as the target the game's own rendering is changed to reach). Use when someone asks for cover art, key art, a thumbnail, a hero image, backgrounds, textures, sprites, or says the game looks rough, flat or unfinished.
4
- compatibility: Node 22, ffmpeg and Chrome. Generated images use the creator's own fal account (FAL_KEY); fal's own MCP server (https://mcp.fal.ai/mcp-relay) finds models and reads prices.
3
+ description: "Make game and app art using the official fal MCP, studio art direction, local capture and asset checks."
4
+ compatibility: Node 22 and the official fal plugin in the AI client. Local media processing uses ffmpeg and Chrome where needed.
5
5
  metadata:
6
6
  providers: fal
7
7
  ---
8
8
 
9
- # Art for a studio's game
10
-
11
- **Build time, never a frame of play.** Everything here happens once, before anybody plays, and costs
12
- nothing at runtime: never call an image model from a game's loop or from anything a person waits on.
13
- If you want one there, you want a baked asset instead.
14
-
15
- One script: `scripts/art.mjs` in this skill's folder (Claude Code:
16
- `node "${CLAUDE_PLUGIN_ROOT}/skills/art/scripts/art.mjs" <command>`), from inside the studio. Node 22,
17
- ffmpeg and Chrome; `--json` on every command. Art jobs live in `art/<slug>/` (the images, the
18
- `budget.json`, the `.request` and `.json` receipts beside each generated file); every paid call is
19
- also a line in `art/receipts.jsonl`.
20
-
21
- ```sh
22
- node <art.mjs> check # ffmpeg, Chrome, the studio; the fal key checked for free (only generated images need it)
23
- ```
24
-
25
- ## 1. The cover, first, always
26
-
27
- The cover is what a stranger sees before pressing anything: the studio's game page, the directory
28
- card, a link preview. Start from the game itself:
29
-
30
- ```sh
31
- npm run dev # background task
32
- node <art.mjs> frame <game> --url http://127.0.0.1:8787 # real game frames, the most detailed picked
33
- node <art.mjs> cover <game> --from art/frames/frame-04.png [--focus 0.6,0.4]
34
- ```
35
-
36
- `frame` uses the play view for server rules so the camera's seat keeps the room
37
- running, and the big screen for older games. It hides the platform's furniture
38
- (join QR, status chip, result cards) and keeps the frame with the most visible detail.
39
- For `--view tv` in a rules game, keep a real player in that same room: a TV alone
40
- does not keep a server match running. A paused-room message means use `--view play`
41
- or open a player, not change hosting mode.
42
- `cover` crops it to 16:9 around the focus, writes `cover.jpg` (1600x900, under 400 KB) into the game
43
- and names it in `game.json` (`"cover": "cover.jpg"`): the site and the directory use it until the game's
44
- landing has a still of its own (`hero/wide.jpg`), which then is its picture everywhere.
45
-
46
- **Open the cover and look.** A cover is a picture of the GAME: no QR code, no share panel, no debug
47
- text, not the waiting screen (a picture of nothing happening). Dark or featureless frames get a
48
- warning: a black card on a page reads as a black box.
49
-
50
- **A painted cover** from that frame, when the person wants one (paid, below): write the prompt from
51
- the game you just played, never from its title. Describe what is on screen: the palette, the time of
52
- day, what the player is doing, the shapes. A cover that promises something the game does not contain
53
- is the one failure here that matters: a person presses Play expecting that picture and meets
54
- something else. Use the real frame as the image reference (`"@file:art/frames/frame-04.png"` in the
55
- input) so the painting keeps the game's layout.
56
-
57
- ## 2. Paid images: price, budget, one call at a time
58
-
59
- Use an already connected fal account for paid generation; never ask the person to find or set a key.
60
- The local paid script discovers an existing fal key using the lookup below. If none is available, continue
61
- with local assets or captured footage and explain that this paid-script connection is not ready.
62
- Do not make account configuration a prerequisite for a live studio. **fal's own MCP server** is how you find a
63
- model and read its input schema and price (`search_models`, `recommend_model`, `get_model_schema`,
64
- `get_pricing`): use it rather than memory, because endpoints retire and reprice. Not connected? Offer
65
- it, and the person signs in on fal's own page (no key): in Claude Code `claude mcp add --transport http
66
- --scope user fal https://mcp.fal.ai/mcp-relay`, then `/mcp`; in Codex `codex mcp add fal --url
67
- https://mcp.fal.ai/mcp-relay`, then `codex mcp login fal`. **Paid calls go through `art.mjs gen`,
68
- never the MCP's `run_model` or `submit_job`**: `gen` prices, caps, receipts and resumes; the MCP's run
69
- does none of that (the Homie mod in Claude Code, and Homie's hooks in Codex, hold one as a call whose cost cannot be read first).
70
-
71
- ```sh
72
- node <art.mjs> price --model fal-ai/flux/dev --input art/cover/input.json # free: fal's unit price x this input
73
- node <art.mjs> budget cover --cap <dollars the person agreed to>
74
- node <art.mjs> gen cover --model fal-ai/flux/dev --input art/cover/input.json --out cover-painted.jpg --dry-run
75
- node <art.mjs> gen cover --model fal-ai/flux/dev --input art/cover/input.json --out cover-painted.jpg --yes
9
+ # Art for this studio
10
+
11
+ Read the game's or app's style, codex and existing scene before writing a prompt.
12
+ Use its palette, shape language, materials, lighting and camera. Keep approved references
13
+ and the style revision. Covers must describe what the playable experience really offers.
14
+ A frame from the running game is free and often the best cover.
15
+
16
+ For generated art use fal's `recommend_model` or `search_models`, `get_model_schema`
17
+ and `get_pricing`, then `run_model`. Continue a pending request with `check_job` and
18
+ `get_job_result`. Use `upload_file` only as documented by the official plugin for references.
19
+ Set an explicit image count. Inspect the first result before any dependent generation.
20
+
21
+ Local `scripts/art.mjs` keeps `check`, `frame`, `cover`, `tile`, `fit` and `sheet`.
22
+ Use `cover <game> --from <image>` for a 1600×900 cover and game.json reference;
23
+ `fit <image> --out <file>` for phone-sized delivery. Check tile seams with a repeated
24
+ 2×2 preview, transparency against light and dark, readability at phone size, and
25
+ compression after fitting. Do not replace an engine effect with a large image by default.
26
+
27
+ ## Official generation workflow
28
+
29
+ The owner's AI uses the provider's official plugin/MCP in its own client. Homie never
30
+ asks for, stores, reads or prints provider credentials. Read the current provider
31
+ skill and discover its available tools and input schemas; host prefixes vary.
32
+ Connection instructions and verified sources: [official integrations](../studio-setup/references/OFFICIAL-MEDIA.md).
33
+ If tools are missing, connect that official plugin once in the client's browser flow.
34
+ Do not fall back to a Homie client, key file, environment key or CLI generation wrapper.
35
+
36
+ Before spending, state the model, count/duration, estimated cost and remaining approved
37
+ budget. Use the owner's existing authorization; ask only if the spend is outside it.
38
+ Record the request ID as soon as accepted. Poll that same request; never resubmit to
39
+ retry a slow job. Unknown cost is unknown, never zero. Provider billing is authoritative;
40
+ Homie's local receipt cannot enforce spending in another client.
41
+
42
+ ## Place and prove the result
43
+
44
+ Download the completed tool's output using the AI client's file/download facilities.
45
+ Keep temporary signed URLs out of committed records. Keep the original in the studio's
46
+ art/job folder; put the optimized delivered asset beside its game/app, or in music/ or
47
+ videos/ for a published work. Record a receipt with these fields:
48
+
49
+ ```json
50
+ {
51
+ "provider": "fal",
52
+ "tool": "run_model",
53
+ "model": "exact endpoint returned by the provider",
54
+ "requestId": "actual request ID",
55
+ "prompt": "the actual prompt, including style constraints",
56
+ "styleRevision": "revision from the studio's style",
57
+ "rights": {"source": "provider/model terms URL", "checkedAt": "YYYY-MM-DD", "commercial": false, "note": "record the actual plan and permitted use"},
58
+ "cost": {"status": "unknown"}
59
+ }
76
60
  ```
77
61
 
78
- - Tell the person the price and ask for a cap before the first call. `gen` refuses with no budget,
79
- refuses past the cap, and asks again without `--yes`.
80
- - The receipt is written the moment fal accepts the job. Running the same `gen` again resumes it from
81
- `<out>.request`: it never pays twice. A cut-off call is resumed, not re-sent.
82
- - **Never run paid generation in a loop** to "try until it is good". Look at what came back, change
83
- the prompt, run it once more. Spend the riskiest image first.
84
- - No real people, no real brands, no copyrighted characters, no "in the style of" a living artist, in
85
- prompts or references.
86
- - **Every prompt starts from the game's derived style prompt** (`homie-studio style prompt <id>`: medium,
87
- shape, palette hexes, light, framing; computed from the decisions, never edited by hand) and uses the golden
88
- images as references on a multi-reference model. A picture made under an older palette is listed stale when
89
- the palette changes (the `style` skill's blast radius).
90
- - What the provider's terms say about the pictures, with read dates: `references/RIGHTS.md`. Read the live page
91
- again before anything commercial is published.
92
- - 3D models (props, characters) are the `models` skill's; mood images for the style board are too
93
- (`models.mjs mood`).
62
+ For ElevenLabs use its actual discovered tool and model, and its flow/session or request
63
+ identifier. A measured cost uses `{"status":"actual","amount":1,"unit":"credits"}`;
64
+ a quote uses `estimate`. Never claim commercial rights from the fact that generation
65
+ succeeded. Read the provider and model terms for the account's plan; record attribution,
66
+ reference-image permissions and any restrictions. Mark test fixtures `"fixture": true`.
94
67
 
95
- ## 3. Backdrops, textures, plates
96
-
97
- A procedural gradient sky is the tell that a game was generated; a painted one costs one image at build
98
- time and loads like any other asset. Good candidates: a skybox or far matte, a ground or wall texture,
99
- a character plate, a title card. Bad: anything the game generates procedurally for a reason.
68
+ Wire the delivered path into the scene or manifest, then run:
100
69
 
101
70
  ```sh
102
- node <art.mjs> tile art/floor/floor.png # does it tile? seam numbers and a 2x2 preview
103
- node <art.mjs> fit art/floor/floor.png --out games/<id>/public/art/floor.jpg --width 1024 --max-kb 250
104
- node <art.mjs> sheet art/floor # everything in a job, labelled, to look at
71
+ npx --no-install homie-studio media import --file <downloaded-file> --receipt <receipt.json> --out games/<id>/assets/<name> --used-by games/<id>/view.ts
105
72
  ```
106
73
 
107
- - **Models are bad at tiling.** Ask for a texture, then check it: a seam over 2 shows as a grid of
108
- lines in the game. Mirror-blend the edges or make it tile yourself before it ships.
109
- - **Size is a feature.** A phone downloads every byte before the first round. Fit backdrops to the
110
- size they are drawn at, JPEG (or WebP where ffmpeg has it) for pictures, PNG only for hard-edged
111
- sprites with transparency. Then play it again: the `playtest` skill's first row times the load; an
112
- asset that doubled the load time made the game worse.
113
- - Put art where the game's own build puts assets (`games/<id>/public/` for a bundled game, the
114
- game's folder for a static one) and load it with a relative path.
115
-
116
- ## 4. Art direction: paintovers, then change the game
117
-
118
- Images do not fix a game that looks unfinished; the best-loved browser games often ship no texture
119
- files at all. What makes a frame look finished is light, contrast between the subject and the ground,
120
- detail at more than one scale, and a clear horizon or depth. The method:
121
-
122
- 1. **The style bible is the game's decisions** (the `style` skill): render style, palette with its jobs,
123
- light, shape language, camera, fonts, drawn in the codex's Art direction tab. An older studio's
124
- `art/STYLE.md` retires into it: carry what it says into `style set` / `style steer`, then keep only the
125
- decisions. Paint with the LOCKED palette and light.
126
- 2. **Paintovers**: real frames of the game (`frame`), painted over by an image model with the derived style
127
- prompt (`npx --no-install homie-studio style prompt <id>`) and the frame as the reference, plus the golden
128
- images (`style golden`) where there are any. These are the TARGET, never shipped as the game.
129
- 3. **Change the game's own rendering** (lights, materials, fog, the floor, the camera) until frames of
130
- the running game match the paintover; compare them side by side.
131
- 4. **Judge blind**: frames of the old build, the new one and a reference game the person admires,
132
- shuffled under random labels, scored by a fresh reviewer before the labels are revealed (the
133
- `playtest` skill's review). A reference is a bar, not a template.
134
-
135
- ## Tell the person
136
-
137
- What was made and where it is used, the file sizes, what it cost (from the receipts) against the cap,
138
- what is a real frame and what is generated, and the pictures themselves.
139
-
140
- ## Never
141
-
142
- - Never ship generated art as a screenshot of gameplay, or a cover that shows what the game does not have.
143
- - Never spend without a budget, past it, or without a receipt; never print or store a key.
144
- - Never replace art a person made without asking.
145
-
146
- ### Existing provider credentials
74
+ This copies bytes, verifies a static reference, hashes the asset and records allowlisted
75
+ provenance in `media/provenance.json`. Reimporting the same output is idempotent; it
76
+ never generates or charges. Include each generation step's receipt for derived assets.
77
+ Run the scene and look/listen: a file and static reference are not proof that it renders
78
+ or plays. Report the placed path, usage evidence, rights and actual/estimated cost.
147
79
 
148
- The tools read `FAL_KEY` or `ELEVENLABS_API_KEY` first, then the plain single-line
149
- key in `~/.homie/keys/fal-key` or `~/.homie/keys/elevenlabs-key`. They also accept
150
- `~/.homie/workspace/fal-key` and `~/.homie/workspace/elevenlabs-key` from earlier
151
- installs. Checks name the source, never the key. Keep these files private (mode
152
- 0600); never copy them into a studio. ElevenLabs also accepts its CLI browser
153
- sign-in and the existing Stripe Projects connection. Run the tool’s `check`
154
- before declaring a provider disconnected.
80
+ The toolkit’s `lib/media-workflows.mjs` declares this skill’s verified tool references.
81
+ Validate them against the host’s current tool inventory before execution; tool discovery
82
+ is required for generation capabilities that have no stable documented name.
@@ -1,33 +1,13 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * art.mjs: a game's cover, painted backdrops and textures, made at build time and never while
4
- * anyone plays. Real frames come from the running game for free; generated images come from fal
5
- * through the creator's own account, priced first, capped by a budget, receipted the moment fal
6
- * accepts them.
7
- *
8
- * check ffmpeg, Chrome, the studio, the fal key (a free check)
9
- * frame <game> --url <site> [--view tv|play] [--seconds 12] [--slug <art job>]
10
- * real frames of the game playing, the busiest one picked
11
- * cover <game> --from <image> [--focus x,y]
12
- * the game's cover: 16:9, 1600x900 JPEG, into the game, named in game.json
13
- * budget <slug> --cap <usd> what the person agreed to spend on this art job
14
- * price --model <fal model> --input <input.json>
15
- * gen <slug> --model <fal model> --input <input.json> --out <file> [--dry-run | --yes]
16
- * one paid image (or a few), inside the cap, resumable, receipted
17
- * tile <image> does a texture tile without a seam? numbers and a 2x2 preview
18
- * fit <image> --out <file> [--width 1024] [--max-kb 300]
19
- * a texture or backdrop at a size a phone can download
20
- * sheet <folder> a labelled contact sheet of every image in a folder
21
- *
22
- * Every command takes --json. Run it from inside the studio (the folder with studio.json).
23
- */
2
+ /** art.mjs: local asset preparation only. Generation, schemas and pricing belong to the official provider MCP.
3
+ * Read ../SKILL.md for the current workflow. */
24
4
  import { spawnSync } from 'node:child_process';
25
5
  import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
26
6
  import { dirname, extname, join, relative, resolve } from 'node:path';
27
7
  import { fileURLToPath } from 'node:url';
28
8
  import { experienceDir, experienceJson, isServerRules, SLUG, checkBudget, findStudio, readBudget, readJson, setBudget, writeJson } from '../../music/scripts/lib/studio.mjs';
29
9
  import { GPU_FLAGS, chromePath, htmlToPng, loadPuppeteer } from '../../video/scripts/lib/browser.mjs';
30
- import { checkKey, falKey, priceOf, run as falRun } from '../../video/scripts/lib/fal.mjs';
10
+
31
11
  import { decode, stats } from '../../playtest/scripts/lib/pixels.mjs';
32
12
 
33
13
  const argv = process.argv.slice(2);
@@ -61,29 +41,15 @@ function gameDir(root, game) {
61
41
  }
62
42
 
63
43
  /** A receipt line: the job's budget (running total), art/receipts.jsonl, and HOMIE_SPEND_LEDGER when set. */
64
- function receipt(root, dir, line) {
65
- const at = new Date().toISOString();
66
- const b = readBudget(dir);
67
- if (b) {
68
- b.spent = +(b.spent + (Number(line.cost) || 0)).toFixed(6);
69
- b.calls.push({ at, provider: line.provider, model: line.model, cost: line.cost, unit: line.unit, requestId: line.requestId ?? null, artifact: line.artifact ?? null });
70
- writeJson(join(dir, 'budget.json'), b);
71
- }
72
- mkdirSync(join(root, 'art'), { recursive: true });
73
- appendFileSync(join(root, 'art', 'receipts.jsonl'), `${JSON.stringify({ schema: 'homie.media-receipt/1', at, ...line })}\n`);
74
- const ledger = process.env.HOMIE_SPEND_LEDGER;
75
- if (ledger) {
76
- try { appendFileSync(ledger, `${JSON.stringify({ schema: 'homie.brand-spend/1', at, provider: line.provider, model: line.model, jobId: line.requestId ?? null, units: line.units ?? null, unit: line.unitName ?? line.unit, usd: line.cost, credits: null, unpriced: null, brand: process.env.HOMIE_SPEND_BRAND ?? null, attribution: process.env.HOMIE_SPEND_ATTRIBUTION ?? null, artifact: line.artifact ?? null })}\n`); } catch (e) { process.stderr.write(`could not write the ledger: ${e.message}\n`); }
77
- }
78
- }
44
+
79
45
 
80
46
  /* ---------------------------------------------------------------- check */
81
47
 
82
48
  async function check() {
83
49
  const ff = spawnSync('ffmpeg', ['-version']).status === 0;
84
50
  const root = findStudio();
85
- const key = falKey() ? await checkKey() : { ok: false, why: 'FAL_KEY is not set (only generated images need it; frames, covers, tiles and fitting are free)' };
86
- return { ok: ff, command: 'check', ffmpeg: ff, chrome: chromePath(), puppeteer: Boolean(loadPuppeteer(root)), studio: root, fal: key.ok ? `the key works (checked free; from ${key.source})` : key.why };
51
+ return { ok: ff, command: 'check', ffmpeg: ff, chrome: chromePath(), puppeteer: Boolean(loadPuppeteer(root)), studio: root, generation: 'Use the official fal MCP in your AI client' };
52
+
87
53
  }
88
54
 
89
55
  /* ---------------------------------------------------------------- frame */
@@ -175,43 +141,6 @@ function budget(root) {
175
141
  return { ok: true, command: 'budget', slug: pos[1], cap: b.cap, spent: b.spent };
176
142
  }
177
143
 
178
- function inputOf(file) {
179
- const j = readJson(resolve(String(file ?? '')), null);
180
- if (!j) throw new Error('--input <file.json>: the model\'s input (its schema is on the model\'s fal page, or the fal MCP get_model_schema)');
181
- return j;
182
- }
183
-
184
- async function price() {
185
- const model = String(flags.get('model') ?? '');
186
- if (!model) throw new Error('--model <fal model id, e.g. fal-ai/flux/dev>');
187
- const p = await priceOf(model, inputOf(flags.get('input')));
188
- return { ok: true, command: 'price', ...p };
189
- }
190
-
191
- async function gen(root) {
192
- const slug = pos[1];
193
- const dir = artDir(root, slug);
194
- const model = String(flags.get('model') ?? '');
195
- if (!model) throw new Error('--model <fal model id>');
196
- if (!flags.get('out')) throw new Error('--out <file in this art job, e.g. cover-painted.png>');
197
- const input = inputOf(flags.get('input'));
198
- const out = resolve(dir, String(flags.get('out')));
199
- if (!out.startsWith(`${dir}/`)) throw new Error('--out must stay inside the art job\'s folder');
200
- if (existsSync(out)) return { ok: true, command: 'gen', slug, already: rel(root, out) };
201
- const p = await priceOf(model, input);
202
- if (flags.has('dry-run')) return { ok: true, command: 'gen', dryRun: true, slug, price: p };
203
- const resuming = existsSync(`${out}.request`);
204
- if (!resuming) {
205
- const b = checkBudget(dir, p.usd, { provider: 'fal', unit: 'usd' });
206
- if (!flags.has('yes')) return { ok: false, command: 'gen', needs: 'approval', slug, price: p, budget: { cap: b.cap, spent: b.spent }, why: `This call costs about US$${p.usd.toFixed(3)} (${p.basis}); US$${b.spent.toFixed(2)} of US$${b.cap.toFixed(2)} spent so far. Run it with --yes once that is fine.` };
207
- }
208
- const r = await falRun(model, input, {
209
- out, base: dir, uploads: join(dir, 'uploads.json'), price: p, log: (m) => { if (!JSON_OUT) process.stderr.write(`${m}\n`); },
210
- onAccepted: async (q) => { receipt(root, dir, { provider: 'fal', model, requestId: q.request_id, cost: p.usd, unit: 'usd', units: p.units, unitName: p.unit, basis: p.basis, artifact: rel(root, out) }); },
211
- });
212
- return { ok: true, command: 'gen', slug, files: r.files.map((f) => rel(root, f)), requestId: r.requestId, usd: p.usd, spent: readJson(join(dir, 'budget.json'), {}).spent, next: 'look at it before anything else; a second call only with a changed prompt, never a loop' };
213
- }
214
-
215
144
  /* ---------------------------------------------------------------- tile / fit / sheet */
216
145
 
217
146
  function tile() {
@@ -281,14 +210,12 @@ async function main() {
281
210
  return { ok: true, command: 'help', usage: head.replace(/^#!.*\n\/\*\*?/, '').replace(/^ \* ?/gm, '').trim() };
282
211
  }
283
212
  if (cmd === 'check') return check();
284
- if (cmd === 'price') return price();
285
213
  if (cmd === 'tile') return tile();
286
214
  if (cmd === 'fit') return fit();
287
215
  const root = studioRoot();
288
216
  if (cmd === 'frame') return frame(root);
289
217
  if (cmd === 'cover') return cover(root);
290
218
  if (cmd === 'budget') return budget(root);
291
- if (cmd === 'gen') return gen(root);
292
219
  if (cmd === 'sheet') return sheet(root);
293
220
  return { ok: false, command: cmd, why: `unknown command "${cmd}" (art.mjs help)` };
294
221
  }