@koda-sl/baker-cli 0.421.0-dev.8ef74f67e → 0.421.0

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 (28) hide show
  1. package/README.md +35 -10
  2. package/dist/{chunk-BGAU2ZQF.js → chunk-EXWIIEZP.js} +4 -4
  3. package/dist/{chunk-6YAPMFMC.js → chunk-FKFJKE75.js} +43 -4
  4. package/dist/chunk-FKFJKE75.js.map +1 -0
  5. package/dist/{chunk-PFN3A4VQ.js → chunk-GLPQQP5C.js} +780 -408
  6. package/dist/chunk-GLPQQP5C.js.map +1 -0
  7. package/dist/{chunk-KBIEU3CK.js → chunk-LRRWIOJG.js} +3 -3
  8. package/dist/{chunk-QUFIEBEP.js → chunk-YZLLC7DL.js} +25 -4
  9. package/dist/chunk-YZLLC7DL.js.map +1 -0
  10. package/dist/{chunk-EXXI274U.js → chunk-ZADYWHL5.js} +3 -3
  11. package/dist/cli.js +3105 -1532
  12. package/dist/cli.js.map +1 -1
  13. package/dist/{client-LYDB4ONI.js → client-CUHMSNK5.js} +4 -4
  14. package/dist/engine/index.js +3 -3
  15. package/dist/{env-37XB6PGZ.js → env-PJTJTW3N.js} +2 -2
  16. package/dist/{output-DYBL2XBN.js → output-A2ASA36U.js} +5 -5
  17. package/dist/{shared-XHOQLVMN.js → shared-LV6PIKFE.js} +6 -6
  18. package/package.json +1 -1
  19. package/dist/chunk-6YAPMFMC.js.map +0 -1
  20. package/dist/chunk-PFN3A4VQ.js.map +0 -1
  21. package/dist/chunk-QUFIEBEP.js.map +0 -1
  22. /package/dist/{chunk-BGAU2ZQF.js.map → chunk-EXWIIEZP.js.map} +0 -0
  23. /package/dist/{chunk-KBIEU3CK.js.map → chunk-LRRWIOJG.js.map} +0 -0
  24. /package/dist/{chunk-EXXI274U.js.map → chunk-ZADYWHL5.js.map} +0 -0
  25. /package/dist/{client-LYDB4ONI.js.map → client-CUHMSNK5.js.map} +0 -0
  26. /package/dist/{env-37XB6PGZ.js.map → env-PJTJTW3N.js.map} +0 -0
  27. /package/dist/{output-DYBL2XBN.js.map → output-A2ASA36U.js.map} +0 -0
  28. /package/dist/{shared-XHOQLVMN.js.map → shared-LV6PIKFE.js.map} +0 -0
package/README.md CHANGED
@@ -865,7 +865,7 @@ baker ads x targeting-constants --constant tv_shows --locale en-US
865
865
 
866
866
  ### ChatGPT Ads (`baker ads chatgpt`)
867
867
 
868
- Read the company's ChatGPT Ads account (OpenAI's ads inside ChatGPT conversations) and how it performs, and stage changes to it that apply when the chat is published. The company connects one account in the dashboard (Brain → Integrations → ChatGPT Ads) with an API key from its Ads Manager; the key stays in Baker and never reaches the CLI, so there is no account id to pass.
868
+ Read the company's ChatGPT Ads account (OpenAI's ads inside ChatGPT conversations) and how it performs, and stage changes to it that apply when the chat is published. The company connects one account with an API key from its Ads Manager, pasted in the chat's connect card (`request_connection` `{ platform: "chatgpt-ads" }`) or in Brain → Integrations → ChatGPT Ads; the key stays in Baker and never reaches the CLI, so there is no account id to pass.
869
869
 
870
870
  | Subcommand | What it returns |
871
871
  |---|---|
@@ -3177,7 +3177,7 @@ baker testimonials tags
3177
3177
 
3178
3178
  ### Avatars (`baker avatars`)
3179
3179
 
3180
- Reusable AI presenters a company casts into images and video. An avatar holds one settled identity: an **identity sheet** (`sheetUrl`, a generated multi-view turnaround) plus the **subject description** (`subjectDescription`) that person is described with. Every render grounds on the sheet via `--reference` and reuses the subject description verbatim — that pairing is what keeps the same face across a whole campaign.
3180
+ Reusable AI presenters a company casts into images and video — for an actor who has to come back across ads; a person in a single ad needs no avatar. An avatar holds one settled identity: an **identity sheet** (`sheetUrl`, four candid phone photos of the same person in their own setting) plus the **subject description** (`subjectDescription`) that person is described with. Every render grounds on the sheet via `--reference` and reuses the subject description verbatim — that pairing is what keeps the same face across a whole campaign.
3181
3181
 
3182
3182
  `status` is `generating | ready | error | archived`; only `ready` can be cast. `create` returns immediately with `generating` because the identity sheet renders in the background (~30s). `likeness` is `synthetic` (invented from a description) or `licensed` (built from photographs of a real person, which requires `--likeness-confirmed`).
3183
3183
 
@@ -3740,12 +3740,13 @@ Permissions enforced server-side:
3740
3740
 
3741
3741
  ---
3742
3742
 
3743
- ### `baker mcp connected | list | add | remove`
3743
+ ### `baker mcp connected | catalog | list | add | remove`
3744
3744
 
3745
3745
  See everything a chat can reach — managed integrations, custom servers, and connected platforms — and manage **custom MCP servers** by pointing the agent at any remote HTTPS MCP endpoint. Registered tools appear as `mcp__<name>__*` on the next chat message.
3746
3746
 
3747
3747
  ```bash
3748
3748
  baker mcp connected
3749
+ baker mcp catalog --search notion
3749
3750
  baker mcp list
3750
3751
  baker mcp add --name weather --url https://mcp.example.com/mcp
3751
3752
  baker mcp add --name acme --url https://acme.dev/mcp --header "Authorization: Bearer sk-…"
@@ -3754,6 +3755,7 @@ baker mcp remove --name weather
3754
3755
  ```
3755
3756
 
3756
3757
  - `connected` returns three lists. `managed` and `custom` are MCP tools the agent calls directly; `platforms` are the accounts the company signed in to (HubSpot, Google Ads, GA4, Search Console, Tag Manager, LinkedIn Ads), each with its login and the resources it may read. A platform has **no** `mcp__*` tools — it is read through its own command family — so it is listed here to keep the agent from concluding a working connection is missing.
3758
+ - `catalog --search <name> [--limit 1-20]` searches the managed apps a user can connect when a vendor has no MCP server of its own. Each app reports its slug (`app`), how the user connects it (`sign-in` or `key`; apps that need no sign-in cannot be connected and are left out) — never a managed copy of one of Baker's own connections: those come back under `platforms`, to ask for with `{ platform }` and whether this chat already reaches it (`connected`).
3757
3759
  - `--url` must be the **full MCP endpoint** (commonly ending in `/mcp`), HTTPS and public; loopback / private / cloud-metadata hosts are rejected. A bare domain frequently accepts the OAuth sign-in and then serves no tools, so every address is handshake-checked after it changes: `connected` and `list` report `usable: false` plus a `problem` (and a `suggestedAddress` when the server advertises a better endpoint) for a server that isn't serving tools, or `lastCheck: failed` when the last check only hit an outage — that one may work anyway, and a later turn re-checks it automatically. Repointing a server at a different host clears its saved sign-in: a token belongs to the host that issued it, so the server has to be connected again.
3758
3760
  - Scope is two independent choices — audience (just you / everyone) × level (this company / whole org):
3759
3761
  - `--scope company` (default) — everyone in this company.
@@ -3800,6 +3802,23 @@ baker history list --category ads --full # ad writes with raw metadata
3800
3802
 
3801
3803
  ---
3802
3804
 
3805
+ ### `baker credentials list | put | delete`
3806
+
3807
+ Credentials are a company's stored API keys, tokens and passwords. The CLI never returns a value: each credential has a placeholder, `{{credential:NAME}}`, that a request inside a Baker Session can carry in any header, URL or body. The Session's HTTPS proxy fills in the value only for the hosts the credential is bound to, and puts the placeholder back in responses.
3808
+
3809
+ ```bash
3810
+ baker credentials list # name, placeholder, allowedHosts, label
3811
+ baker credentials list --full # + source, lastUsedAt
3812
+ printf %s "$KEY" | baker credentials put stripe-key --host api.stripe.com --label "Stripe key"
3813
+ baker credentials delete stripe-key
3814
+ ```
3815
+
3816
+ - `put` reads the value from **stdin only** — a `--value` flag or a positional value is refused, because argv is visible to other processes. One trailing newline is trimmed; empty stdin is refused. `--host` is repeatable (or comma-separated) and accepts `*.example.com` wildcards.
3817
+ - `put` and `delete` act only on credentials the agent created. Credentials a person added are managed in the dashboard (Settings → Credentials).
3818
+ - The company must have credentials switched on; otherwise every command answers `FORBIDDEN`.
3819
+
3820
+ ---
3821
+
3803
3822
  ### `baker hubspot forms list | view | submissions` · `baker hubspot workflows list | view` · `baker hubspot pipelines list` · `baker hubspot contacts summary | lookup | properties | fields` · `baker hubspot deals properties | fields` · `baker hubspot meetings list | view`
3804
3823
 
3805
3824
  Read-only view of the company's **connected HubSpot account** — the forms and meeting links (calendars) that live there, the leads those forms received, and the workflows, deal pipelines, and contact records a lead runs through afterwards. Fetched from HubSpot live on every call; nothing is cached and nothing is ever written back.
@@ -4106,6 +4125,8 @@ baker canvas run my-canvas.json
4106
4125
  # Advisory warnings print as `[warn] <CODE>: …` before anything bills.
4107
4126
  # Only nodes reachable from the declared output execute (orphaned nodes are skipped,
4108
4127
  # not billed). Add --keep-runs N to prune old r_* run dirs, keeping the N newest.
4128
+ # The JSON ends with `verdict` (static ads), `run_id` and `files` (each delivered size's
4129
+ # local file by its label), so `| tail` keeps them.
4109
4130
  # A failed run reports `error.code: "input_invalid"` when the backend refused a node's
4110
4131
  # own input (it fails the same way until the input changes), `"runtime"` otherwise.
4111
4132
 
@@ -4218,7 +4239,7 @@ because a video model garbles a wordmark every time:
4218
4239
  every beat they appear in on that avatar's identity sheet and copies its subject
4219
4240
  description verbatim, so the ad shows the same face as the rest of the company's work.
4220
4241
  This is the whole journey: `baker avatars create` → `cast.avatar` in the spec → render.
4221
- - **`cast.description`** — a person described in words, when there is no avatar.
4242
+ - **`cast.description`** — a person described in words, when there is no avatar: a candid portrait is generated from it and every beat grounds on that one invented person.
4222
4243
 
4223
4244
  Each of these also removes a generation: the closing beat costs no image and no clip.
4224
4245
 
@@ -4477,6 +4498,10 @@ Open record keyed by the composition's `meta.json` inputs.
4477
4498
 
4478
4499
  Local; no credits (one box-reader call per variant, about $0.005, which rides on the run record's review spend). Ranks a static ad's variants before the judge by the deterministic checks (blocking defects, then every line of the ad spec's copy read back with its box, the brand's logo found, the button's words, every word and the logo inside the master's band) and passes on only the best-ranked, so the judge only breaks an exact tie. A variant the reader could not read ties with the best. **Inputs:** `images` (the variants' whole list), `spec` (the ad spec, `$ref:prompt.asset`). **Params:** `formats` (the delivered sizes). **Outputs:** `images` (the best-ranked variants, in an order seeded by the pictures rather than the render order; never empty: when every variant fails, the one with the fewest defects passes as `flagged`), `order` (which variant each of `images` is), `dropped` (each variant set aside, with its index and reasons), `flagged` (the variant kept when all failed) and `usage` (the reader's spend). Scaffolded by `scaffold-static-ad` between the variants and the judge.
4479
4500
 
4501
+ ##### `static_safe_fit`
4502
+
4503
+ Local; no credits (one box-reader call, about $0.005, which rides on the run record's review spend). Brings a finished 9:16's words and logo inside the band Reels or Stories leave clear without rendering again: reads where every line and the logo sit and, when any reaches past the band, shrinks the whole picture a little (to 85% at most) and shifts it, filling the uncovered margin by mirroring the picture beside it, blurred under a feathered seam. The image model cannot move an element by a few points on request, so this replaces re-rendering for small misses. **Inputs:** `image`. **Params:** `format`, `placements` (the ad set's, as the sizes were planned). **Outputs:** `image` (same pixel size), `fit` (`{ scale, x, y }`, or null when passed on unchanged), `reason` (`not_read`, `inside_band`, or `too_far` when clearing the band needs more than the 85% shrink: the layout itself has to change) and `usage`. Scaffolded by `scaffold-static-ad` after every 9:16 an interface covers (`safe_9x16`, tagged `static_safe_fit`).
4504
+
4480
4505
  ##### `font_specimen`
4481
4506
 
4482
4507
  Render specimen text in a given font file to a **PNG** — black `#000` text on a white `#fff` background — via headless Chromium at 2× device-scale. Produces a **typeface reference image**: wire `image` into `image_generate`'s `reference` input so the model replicates the font when generating images that contain text.
@@ -5722,7 +5747,7 @@ A completed render the review calls ready (`verdict` `ready` or `ready_with_note
5722
5747
 
5723
5748
  **Resuming an interrupted run.** A long `baker canvas run` (multi-clip video) that is killed mid-render — session end, sandbox pause — leaves a marker under the outputs dir. The next `baker canvas run` of the same canvas automatically **resumes** that run: it reuses the run id so still-running billed jobs re-attach instead of being abandoned and re-billed, and completed nodes come from the cache. A live render keeps its marker's heartbeat fresh, so a second `run` of the same canvas is refused while it is alive (checked with `kill(pid, 0)` plus the heartbeat, on Linux and macOS alike). Resume also works from a **different workspace or a fresh sandbox**: when no local marker survives, the run history is consulted for a resumable run of the same source (canvas, `prompt.json`, scenes, compositions and formats), and it is adopted when the backend says it may be: interrupted, stopped at a checkpoint, or a `running` row whose keepalive is older than the backend's staleness threshold. Ctrl-C / SIGTERM aborts gracefully — no new nodes dispatch, a resumable snapshot is flushed, and the marker survives. A clean completion (or a handled failure) clears the marker, so a normal re-run starts a fresh generation. Force a new run with `--fresh`, or pin a specific run with `--run-id <id>` (also the escape hatch to adopt a run that is reported as concurrently live). Independent same-layer nodes (e.g. video clips) fan out in parallel up to `--parallel`/`--concurrency` (default 8; env `BAKER_CANVAS_CONCURRENCY`).
5724
5749
 
5725
- **Static ads: correct a version, or use another of its variants.** Correct a finished version with `--fix "<what to change>"`: one render takes that version (the last one, or `--from <run_id>`) as its first reference and changes only what you said, keeping the picture, the layout and every other word; it replaces the variants and the judge, every size is made again from the corrected picture, and it counts as one render of the round. Use another variant with `--use-variant <sha256>` (`--from <run_id>`, default the last render): when the person chose another of a version's variants over the judge's pick, every size is made again from that picture, and the only requests re-lay it out for each other size; `--regenerate adapt_9x16` lays out one size again from the same chosen picture when only that size came out wrong; `baker creatives feedback --slug <slug>` prints the choice (`variantChoice`) and the exact command.
5750
+ **Static ads: correct a version, or use another of its variants.** Correct a finished version with `--fix "<what to change>"`: one render takes that version (the last one, or `--from <run_id>`) as its first reference and changes only what you said, keeping the picture, the layout and every other word; it replaces the variants and the judge, every size is made again from the corrected picture, and it counts as one render of the round. Use another variant with `--use-variant <sha256>` (`--from <run_id>`, default the last render): when the person chose another of a version's variants over the judge's pick, every size is made again from that picture, and the only requests re-lay it out for each other size; `--regenerate adapt_9x16` lays out one size again from the same chosen picture when only that size came out wrong (after a `--fix` or `--use-variant`, from that corrected or chosen picture, so the size never brings back the uncorrected one; `--from <run_id>` names another version); `baker creatives feedback --slug <slug>` prints the choice (`variantChoice`) and the exact command.
5726
5751
 
5727
5752
  #### `baker canvas scaffold-video <video> [flags]`
5728
5753
 
@@ -5737,7 +5762,7 @@ Before the deconstruct it runs a **local shot-cut pass** on the source file with
5737
5762
 
5738
5763
  A shot longer than the video model's per-clip ceiling (Seedance's 15s, passed as `video_deconstruct`'s `max_clip_s`) is split into equal **continuation sub-scenes** that share their splice boundary exactly — so a long shot is reproduced in **full** (no truncation) and joins seamlessly. Each sub-scene carries `continues_previous`.
5739
5764
 
5740
- It then scaffolds the full pipeline like an **editing timeline**: each clip gets a **static-ad-grade start AND end keyframe** (`image_generate`, each with its **own `params.prompt`** regenerated from that scene's `scenes/sNN.json` — edit the scene file to change that frame, never the node prompt by hand; the slim `prompt.style.json` wired as the **shared `target_blueprint`** style reference, plus a per-element reference legend). Each keyframe is **fully recast** to the dropped `el_*` reference images. The original extracted frame is kept LAST as a **pure composition anchor** (framing / camera angle / shot size / pose) only in an ad with **nobody in it** — no deconstructed cast and no recurring person. In any ad with people, no generated frame takes a source frame as a reference: an image model copies the face of whoever is in its references, whatever the prompt says, and which scenes a person is in is itself a model's guess — so framing is carried by the frame prompt instead. **People are cast from company avatars** (`--avatar <handle>`): each recurring person grounds on that avatar's identity sheet, used as-is (no sheet is built from it); a person with no avatar gets a `[TODO]` asking for one, never for a found photo. Both keyframes feed `video_generate` (`first_frame`+`last_frame`, so Seedance interpolates real in-shot motion; ultra-detailed motion brief; duration snapped to the nearest allowed clip length). Every keyframe grounds **only on its own extracted frame + `el_*` slots** — no reference to any other generated frame — so all images render **in parallel** (no cascade). Source-frame URLs are **deduped** (each ingested once). `--frames reuse` wires the real source frame straight in — only on a video with nobody in it: when the deconstruct found a cast or a recurring person, reuse is refused (`frames_reuse_refused`) and every frame is generated, so the source's actors never reach the ad.
5765
+ It then scaffolds the full pipeline like an **editing timeline**: each clip gets a **static-ad-grade start AND end keyframe** (`image_generate`, each with its **own `params.prompt`** regenerated from that scene's `scenes/sNN.json` — edit the scene file to change that frame, never the node prompt by hand; the slim `prompt.style.json` wired as the **shared `target_blueprint`** style reference, plus a per-element reference legend). Each keyframe is **fully recast** to the dropped `el_*` reference images. The original extracted frame is kept LAST as a **pure composition anchor** (framing / camera angle / shot size / pose) only in an ad with **nobody in it** — no deconstructed cast and no recurring person. In any ad with people, no generated frame takes a source frame as a reference: an image model copies the face of whoever is in its references, whatever the prompt says, and which scenes a person is in is itself a model's guess — so framing is carried by the frame prompt instead. **People are invented or cast**: a person with no avatar gets a generated candid portrait from their description (an `image_generate` node at the `el_*` slot, fused into a sheet), so every frame shows one realistic invented person; with `--avatar <handle>` each recurring person grounds on that avatar's identity sheet, used as-is. Never a found photo. Both keyframes feed `video_generate` (`first_frame`+`last_frame`, so Seedance interpolates real in-shot motion; ultra-detailed motion brief; duration snapped to the nearest allowed clip length). Every keyframe grounds **only on its own extracted frame + `el_*` slots** — no reference to any other generated frame — so all images render **in parallel** (no cascade). Source-frame URLs are **deduped** (each ingested once). `--frames reuse` wires the real source frame straight in — only on a video with nobody in it: when the deconstruct found a cast or a recurring person, reuse is refused (`frames_reuse_refused`) and every frame is generated, so the source's actors never reach the ad.
5741
5766
 
5742
5767
  **Composited scenes (split-screen / picture-in-picture / keyed presenter).** Real ads aren't always one full-frame shot — a frame can be **persistently divided** (b-roll on top, a presenter talking on the bottom) or **layer a presenter** over background footage (boxed in a corner, or green-screen keyed). The deconstruct now reports this per scene as `scene.composition` (`layout: split_screen | pip | keyed_overlay`, with one `region` per stream — each its own clean-plate frame + motion brief, the talking-head region flagged `is_presenter`). The scaffold reproduces a composited scene by building **one clip per region** (`s<i>_r0_*`, `s<i>_r1_*`, …) and compositing them with ffmpeg: a split-screen `vstack`/`hstack` (stack direction read from the region **panels**, so a top/bottom split always stacks vertically), or a picture-in-picture `overlay` of the presenter inset at its corner. A **keyed** presenter is first cut to transparency by `video_background_remove` (`s<i>_key`), then overlaid. The presenter region carries the native lip-synced voice; b-roll/render panels stay silent. To change a layout, edit `composition` in that scene's `scenes/sNN.json` and re-scaffold, or hand-edit the `s<i>_composite` ffmpeg args. Plain full-frame scenes (the default) are unaffected.
5743
5768
 
@@ -5838,11 +5863,11 @@ Turn a source/inspiration image into a **runnable, self-validated static-ad canv
5838
5863
 
5839
5864
  It then scaffolds a canvas on which **the image model designs the finished ad**, holding only what `prompt.json` holds; no element is mandatory:
5840
5865
 
5841
- - Up to **three variants** (`--takes`, default 3) render on the model the router picks for the concept (today `openai/gpt-image-2.5-sunburst` at quality `max`; `--gen-model` overrides), each a finished ad: the picture and every word of the copy. Each variant is **its own request** (tagged `static_variant_call`), run in parallel on its own direction (the scene as written; another picture of the same message, by the scene's look; another line leading, named by number so no word changes), and a `collect` tagged `static_variants` gathers them, on every model: one request with `n` costs the same (`n` is billed per picture) but gave fewer distinct options, so the graph never uses it. The brief quotes every line of `copy` verbatim and asks for each once; it is compiled again from `prompt.json` on every `validate` and `run`, so an edit re-keys exactly the renders it touches. The brand's **typeface** is shown to the model as a `font_specimen` set on the `designed` lines: a file in `src/brand/fonts/` (via `src/brand/tokens.json` when present), else the heading family `tokens.json` or `BRAND.md` names, fetched as a web font file; an `in_scene` line keeps its own lettering, up to three words on one surface (longer in-scene copy is set as designed type). Each line's `role` sets its size: a price or stat (else the headline) leads, the `cta` is a large solid button, fine print is smallest. **The logo is optional**: the real file in `src/brand/logos/` goes to the model as a reference to reproduce once, at least a fifth of the frame wide, where it contrasts or on a small brand-colour plate, only when `prompt.json` has `"logo": true` (an older blueprint: an `advertiser_brand` entry in `brands_logos`) and the file exists; no logo is never an error, never a slot to fill and never a stand-in. Setting it false drops the logo on the next run; adding one to a canvas scaffolded without the file needs the file in `src/brand/logos/` and a re-scaffold. `prompt.json` also carries a **`brand`** block (ground, ink, accent, CTA and palette, from `src/brand/tokens.json` roles or `BRAND.md`); a colour outside it never reaches the model. The brief describes a complete ad at the master's shape and asks only for its edge margin (see below). `baker canvas set-prompt` refuses these nodes and names `prompt.json`.
5866
+ - Up to **three variants** (`--takes`, default 3) render on the model the router picks for the concept (today `openai/gpt-image-2.5-sunburst` at quality `max`; `--gen-model` overrides), each a finished ad: the picture and every word of the copy. Each variant is **its own request** (tagged `static_variant_call`), run in parallel on its own direction (the scene as written; another picture of the same message, by the scene's look; another line leading, named by number so no word changes), and a `collect` tagged `static_variants` gathers them, on every model: one request with `n` costs the same (`n` is billed per picture) but gave fewer distinct options, so the graph never uses it. The brief quotes every line of `copy` verbatim and asks for each once; it is compiled again from `prompt.json` on every `validate` and `run`, so an edit re-keys exactly the renders it touches. The brand's **typeface** is shown to the model as a `font_specimen` set on the `designed` lines: a file in `src/brand/fonts/` (via `src/brand/tokens.json` when present), else the heading family `tokens.json` or `BRAND.md` names, fetched as a web font file; an `in_scene` line keeps its own lettering only on the real packaging, signage or screen its `on` names (handwriting on post-its, whiteboards or chalkboards, and a line naming no surface, are set as designed type). Each line's `role` sets its size: a price or stat (else the headline) leads, the `cta` is a large solid button, fine print is smallest. **The logo is optional**: the real file in `src/brand/logos/` goes to the model as a reference to reproduce once, at least a fifth of the frame wide, where it contrasts or on a small brand-colour plate, only when `prompt.json` has `"logo": true` (an older blueprint: an `advertiser_brand` entry in `brands_logos`) and the file exists; no logo is never an error, never a slot to fill and never a stand-in. Setting it false drops the logo on the next run; adding one to a canvas scaffolded without the file needs the file in `src/brand/logos/` and a re-scaffold. `prompt.json` also carries a **`brand`** block (ground, ink, accent, CTA and palette, from `src/brand/tokens.json` roles or `BRAND.md`); a colour outside it never reaches the model. The brief describes a complete ad at the master's shape and asks only for its edge margin (see below). `baker canvas set-prompt` refuses these nodes and names `prompt.json`.
5842
5867
  - A **`static_prefilter`** node (tagged `variant_prefilter`) drops variants that fail the deterministic checks before the judge sees them, never all of them; an `image_select` **judge** (tagged `pick`) compares the rest pairwise against the compiled brief (one variant passes through unjudged).
5843
- - **Every placement format is made from the chosen picture, and no size is a crop or a paste of another.** The pick goes to exact pixels as the master (an `imagemagick` node tagged `static_master`) at the lead size: the 4:5 when a feed size is delivered, else the square, else the first portrait size, rendered at the model's nearest ratio (3:4 on GPT Image) and filled to Meta's pixels. It is delivered as it is. Every other size is **re-laid out** from it by one request (`image_generate` node `adapt_<size>`, tagged `static_adapt_format`, on the router's `relayout` model, GPT Image 2.5 at quality `max`): the master and the logo file as references and a brief compiled from `prompt.json` for that frame on every run (`static-adapt.ts`): the exact lines and their hierarchy, the same people, product and logo and none the chosen ad does not show (an ad that is its words on a ground fills the extra room with that ground and the brand's colours), the words and the button re-scaled for the frame, the frame stated as zones inside its safe area (on a 9:16, the scene alone above and below the Reels band), never the master shrunk onto a background or cut. A `size_<size>` node (`static_size`) brings it to Meta's exact pixels (9:16 at 1080×1920, 1:1 at 1080×1080; a landscape size the same way). The master shape is one decision in the code (`DEFAULT_MASTER_SHAPE`: `relayout`; the `9x16_band` and `4x5_adapt` shapes, kept for the bench, make the sizes by cutting or reframing the master, and the person rating them rejected every such way; a canvas from the retired band master, which extended a square master into the 9:16, is upgraded to the re-layout graph on its next run). Placing the logo file on every size instead of having the model draw it (`logo_composite` nodes) is a scaffold option, off until it is proven on the bench.
5868
+ - **Every placement format is made from the chosen picture, and no size is a crop or a paste of another.** The pick goes to exact pixels as the master (an `imagemagick` node tagged `static_master`) at the lead size: the 4:5 when a feed size is delivered, else the square, else the first portrait size, rendered at the model's nearest ratio (3:4 on GPT Image) and filled to Meta's pixels. It is delivered as it is. Every other size is **re-laid out** from it by one request (`image_generate` node `adapt_<size>`, tagged `static_adapt_format`, on the router's `relayout` model, GPT Image 2.5 at quality `max`): the master, the variants' own product and people pictures and the logo file as references and a brief compiled from `prompt.json` for that frame on every run (`static-adapt.ts`): the exact lines and their hierarchy, the same people (same face, same clothes), product and label, logo and none the chosen ad does not show, the words and the button re-scaled for the frame, the frame stated as zones inside its safe area (on a 9:16, the words set large through the Reels band, the headline across most of its width, lists restacked rather than shrunk, and the ad's own picture larger above and below it, never flat colour), never the master shrunk onto a background, set as a square or cut. A `size_<size>` node (`static_size`) brings it to Meta's exact pixels (9:16 at 1080×1920, 1:1 at 1080×1080; a landscape size the same way). A 9:16 that Reels or Stories covers then passes a free `static_safe_fit` node (`safe_9x16`), which shrinks and shifts the picture just enough (to 85% at most) to bring a word or the logo a few points past the band back inside it; a canvas scaffolded before it gets the node on its next run, every other node kept. The master shape is one decision in the code (`DEFAULT_MASTER_SHAPE`: `relayout`; the `9x16_band` and `4x5_adapt` shapes, kept for the bench, make the sizes by cutting or reframing the master, and the person rating them rejected every such way; a canvas from the retired band master, which extended a square master into the 9:16, is upgraded to the re-layout graph on its next run). Placing the logo file on every size instead of having the model draw it (`logo_composite` nodes) is a scaffold option, off until it is proven on the bench.
5844
5869
  - Readers go by these roles, never by node ids: the run review, `--fix`, `--use-variant`, `--regenerate static_variants`, the render budget and the dashboard. A static ad scaffolded before this graph (`ad_a`/`ad_b`/`ad_c`, `pick`, re-lays `ad_4x5`/`ad_9x16`, `fit_4x5`) is **upgraded in place** on its next `validate` or `run`: prompt.json, every asset slot, the model and the formats are kept, the render part becomes this graph, and the creative is not reported as changed by the upgrade. Its old runs stay readable.
5845
- - Detected elements get **one `[TODO]` ingest slot** each (never the logo), labelled by role (`el_product_1`, `el_person_1`), never by the source's brand. The original image stays on the canvas as the record of the source and is **never given to the render**: the generator copies what it is shown, whatever the prompt says. Each **animal hero** is fused into a generated **multi-view reference sheet** (`image_reference_sheet`) that the render grounds on; pass `--skip-actor-sheets` to ground on the dropped photo. A **person hero** is cast from a company Avatar with `--avatar <handle>`: the slot ingests its identity sheet and `prompt.json` describes that person in the Avatar's own words.
5870
+ - Detected elements get **one `[TODO]` ingest slot** each (never the logo), labelled by role (`el_product_1`, `el_person_1`), never by the source's brand. The original image stays on the canvas as the record of the source and is **never given to the render**: the generator copies what it is shown, whatever the prompt says. Each **animal hero** is fused into a generated **multi-view reference sheet** (`image_reference_sheet`) that the render grounds on; pass `--skip-actor-sheets` to ground on the dropped photo. A **person** with no avatar has no slot: the render draws an invented, realistic person from the element's description. With `--avatar <handle>` (for an actor who recurs across ads) the slot ingests the Avatar's identity sheet and `prompt.json` describes that person in the Avatar's own words.
5846
5871
 
5847
5872
  The canvas is validated before it is written. stdout returns `{ ok, canvas_path, prompt_path, models, aspect_ratio, placements, output_formats, stats, checklist }`; the **checklist** carries `summary` (what a run does: candidates, judge, type, logo, formats), `font`, `logo`, `in_scene_text`, `formats` and every real asset to drop in.
5848
5873
 
@@ -5850,7 +5875,7 @@ The canvas is validated before it is written. stdout returns `{ ok, canvas_path,
5850
5875
 
5851
5876
  **Who sees the ad.** `--target-adset <id>` names the Meta ad set the ad will run in. Before anything is billed, the viewer its targeting reaches is read exactly as `baker ads meta adsets --audience` reads it (`temperature`, what they already `knows`, `placements`) and written to `_definition.md` as `targetAdSet` and `viewer` (an existing definition keeps every other field). The sizes, the brief and the prefilter keep clear of that viewer's placements (`staticSafeArea`; `automatic`, a bare `facebook`/`instagram` or no placements is Advantage+, Reels included), and every `baker canvas run` checks the ad against that viewer. With no ad account, give the viewer from the Knowledge: `--viewer-temperature cold|warm|hot [--viewer-knows brand,offer,product] [--viewer-placements <placement,…>]`, recorded with `source: knowledge`; next to `--target-adset` they correct the read, and its `reasons` say so. A bad value exits 2 (`invalid_viewer`) before any billed pass; an ad set that cannot be read never stops the scaffold (a hint says why, and the sizes keep clear of every placement). With neither flag, a `viewer` (or `targetAdSet`) already in `_definition.md` is used as it is. stdout carries `target_adset`, `viewer` and a `checklist.viewer` line.
5852
5877
 
5853
- **After a run** the CLI reads the finished ad back on what makes a static win, not a checklist of elements: every delivered image's pixel shape against its format; **every line of `prompt.json`'s copy (in-scene and designed alike) read back with its box by a box reader and compared** (a line more than 2% off, missing, or joined by words nobody asked for is blocking; case and punctuation fold, accents do not); **every word and the logo measured against the format's safe area** (`staticSafeArea`, the one owner of those areas: on a 9:16, the Reels and Stories interface unless the ad set's placements rule them out); designed type in a colour far from the brand palette is a warning. When the reader cannot answer, the vision pass transcribes the copy instead and judges the safe zones by eye. Then one vision pass for clipping, overlaps, artifacts, one dominant idea, the brand with the logo covered and the concept's mechanisms, with every format shown a second time at phone-feed thumbnail size (240px wide, same call): a lead message unreadable at that size is blocking. A missing logo, CTA or copy is never a finding; a logo that is drawn and broken is. stdout carries `verdict` (`ready` | `ready_with_notes` | `needs_fixes` | `not_reviewed`), `judge` (the per-candidate scores) and hints that end with what the run made; the findings are stored on the run as its `review`. `needs_fixes` means the ad is not done even though every node completed, and the run is recorded with `presentable: false` so it is never shown as a version; every fix is an edit to `prompt.json` and a re-run (wrong words: the lines in `copy`; wrong picture: the `scene`; a bad roll: `--regenerate static_variants`; one re-laid-out size wrong while the chosen picture is right: `--regenerate adapt_<size>`, named in the hints). The checks rank the variants before the pick and the judge only breaks an exact tie, comparing pairwise against the compiled brief, knowing the brand and the viewer; every variant stays on offer to the person in Baker. **Render budget:** a static ad gets 3 renders per round (`RENDERS_PER_ROUND`), and a round is one version the person sees; the backend counts them (`GET /api/canvas/render-budget`), with a local ledger only when it cannot be reached. The fourth exits 2 with `RENDER_BUDGET` without rendering. The person's reaction opens a new round: their answer in Baker, or `baker creatives feedback record` / `baker creatives select` from the chat. Resuming an interrupted run does not count again, and a finished run that billed nothing (an outage: timeout, network, a provider that says retry) gives its render back. When the round is spent and the verdict is still `needs_fixes`, the hints say to show the best version and name the defect in plain words rather than render again.
5878
+ **After a run** the CLI reads the finished ad back on what makes a static win, not a checklist of elements: every delivered image's pixel shape against its format; **every line of `prompt.json`'s copy (in-scene and designed alike) read back with its box by a box reader and compared** (a line more than 2% off, missing, or joined by words nobody asked for is blocking; case and punctuation fold, accents do not); **every word and the logo measured against the format's safe area** (`staticSafeArea`, the one owner of those areas: on a 9:16, the Reels and Stories interface unless the ad set's placements rule them out, which is blocking; on a feed image such as a 4:5, only a design margin, so words close to the edge are a warning and only words the frame cuts are blocking); a copy line written over two lines (`"15%\noff"`) is matched part by part wherever the reader finds each; designed type in a colour far from the brand palette is a warning. When the reader cannot answer, the vision pass transcribes the copy instead and judges the safe zones by eye. Then one vision pass for clipping, overlaps, artifacts, one dominant idea, the brand with the logo covered and the concept's mechanisms, with every format shown a second time at phone-feed thumbnail size (240px wide, same call): a lead message unreadable at that size is blocking. A missing logo, CTA or copy is never a finding; a logo that is drawn and broken is. stdout carries `verdict` (`ready` | `ready_with_notes` | `needs_fixes` | `not_reviewed`), `judge` (the per-candidate scores) and hints that end with what the run made; the findings are stored on the run as its `review`. `needs_fixes` means the ad is not done even though every node completed, and the run is recorded with `presentable: false` so it is never shown as a version; every fix is an edit to `prompt.json` and a re-run (wrong words: the lines in `copy`; wrong picture: the `scene`; a bad roll: `--regenerate static_variants`; one re-laid-out size wrong while the chosen picture is right: `--regenerate adapt_<size>`, named in the hints). The checks rank the variants before the pick and the judge only breaks an exact tie, comparing pairwise against the compiled brief, knowing the brand and the viewer; every variant stays on offer to the person in Baker. **Render budget:** a static ad gets 3 renders per round (`RENDERS_PER_ROUND`), and a round is one version the person sees; the backend counts them (`GET /api/canvas/render-budget`), with a local ledger only when it cannot be reached. The fourth exits 2 with `RENDER_BUDGET` without rendering. The person's reaction opens a new round: their answer in Baker, or `baker creatives feedback record` / `baker creatives select` from the chat. Resuming an interrupted run does not count again, and a finished run that billed nothing (an outage: timeout, network, a provider that says retry) gives its render back. When the round is spent and the verdict is still `needs_fixes`, the hints say to show the best version and name the defect in plain words rather than render again.
5854
5879
 
5855
5880
  The render prompt is **model-aware**: each model's profile (`src/engine/lib/prompt-profiles.ts`) sets its shape, word budget, picture order and request params. gpt-image gets short labelled sections with the constraints last; Gemini gets sentences that give the reason and a positive closing list of the only words allowed. A profile change, or a new model on the router (`src/engine/lib/image-model-router.ts`), is re-run on the static bench (`scripts/creative-evals`, `--job static`) before it ships. Run `baker canvas critique` on the scaffolded canvas for an advisory read before you spend.
5856
5881
 
@@ -2,13 +2,13 @@ import {
2
2
  handleConnectionError,
3
3
  needsConnectionFix,
4
4
  writeAdsJson
5
- } from "./chunk-KBIEU3CK.js";
5
+ } from "./chunk-LRRWIOJG.js";
6
6
  import {
7
7
  ApiError
8
- } from "./chunk-EXXI274U.js";
8
+ } from "./chunk-ZADYWHL5.js";
9
9
  import {
10
10
  getEnv
11
- } from "./chunk-QUFIEBEP.js";
11
+ } from "./chunk-YZLLC7DL.js";
12
12
 
13
13
  // src/commands/ads/meta/shared.ts
14
14
  var DAY_MS = 864e5;
@@ -133,4 +133,4 @@ export {
133
133
  resolveEffectiveStatus,
134
134
  statusFilterHints
135
135
  };
136
- //# sourceMappingURL=chunk-BGAU2ZQF.js.map
136
+ //# sourceMappingURL=chunk-EXWIIEZP.js.map
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  debugLogSetting
3
- } from "./chunk-QUFIEBEP.js";
3
+ } from "./chunk-YZLLC7DL.js";
4
4
 
5
5
  // src/debugLog.ts
6
6
  import { appendFileSync, mkdirSync, renameSync, statSync } from "fs";
@@ -33,6 +33,45 @@ function redactSecrets(input) {
33
33
  }
34
34
  return out;
35
35
  }
36
+ var CREDENTIAL_VALUE = "[credential value]";
37
+ var CREDENTIAL_PUT_PATH = "/api/credentials/put";
38
+ function withoutCredentialValue(url, body) {
39
+ let path;
40
+ try {
41
+ path = new URL(url).pathname;
42
+ } catch {
43
+ path = url;
44
+ }
45
+ if (path !== CREDENTIAL_PUT_PATH || body === null || typeof body !== "object" || !("value" in body)) {
46
+ return body;
47
+ }
48
+ return { ...body, value: CREDENTIAL_VALUE };
49
+ }
50
+ var PUT_FLAGS_WITH_VALUES = /* @__PURE__ */ new Set(["--host", "--label"]);
51
+ function argvWithoutCredentialValue(argv) {
52
+ if (argv[0] !== "credentials" || argv[1] !== "put") {
53
+ return argv;
54
+ }
55
+ let nameSeen = false;
56
+ let keepNext = false;
57
+ let redactNext = false;
58
+ return argv.map((arg, index) => {
59
+ if (index < 2) return arg;
60
+ if (keepNext || redactNext) {
61
+ const kept = keepNext ? arg : CREDENTIAL_VALUE;
62
+ keepNext = false;
63
+ redactNext = false;
64
+ return kept;
65
+ }
66
+ if (arg.startsWith("--value=")) return `--value=${CREDENTIAL_VALUE}`;
67
+ redactNext = arg === "--value";
68
+ keepNext = PUT_FLAGS_WITH_VALUES.has(arg);
69
+ if (arg.startsWith("-")) return arg;
70
+ const isName = !nameSeen;
71
+ nameSeen = true;
72
+ return isName ? arg : CREDENTIAL_VALUE;
73
+ });
74
+ }
36
75
  function resolveTarget() {
37
76
  const raw = debugLogSetting();
38
77
  if (!raw) {
@@ -98,7 +137,7 @@ function errorToPlain(err) {
98
137
  var invocationStartedAt;
99
138
  function logInvocation(argv) {
100
139
  invocationStartedAt = Date.now();
101
- append({ kind: "invocation", argv, cwd: process.cwd() });
140
+ append({ kind: "invocation", argv: argvWithoutCredentialValue(argv), cwd: process.cwd() });
102
141
  }
103
142
  function logExit(code) {
104
143
  append({
@@ -127,7 +166,7 @@ function debugLogHttp(entry) {
127
166
  method: entry.method,
128
167
  url: entry.url,
129
168
  durationMs: entry.durationMs,
130
- request: entry.requestBody,
169
+ request: withoutCredentialValue(entry.url, entry.requestBody),
131
170
  status: entry.status,
132
171
  ok: entry.ok,
133
172
  response: entry.responseBody,
@@ -173,4 +212,4 @@ export {
173
212
  readBodyForLog,
174
213
  installStreamTaps
175
214
  };
176
- //# sourceMappingURL=chunk-6YAPMFMC.js.map
215
+ //# sourceMappingURL=chunk-FKFJKE75.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/debugLog.ts"],"sourcesContent":["// Full-fidelity debug capture for the whole CLI.\n//\n// Set BAKER_DEBUG_LOG to turn it on:\n// BAKER_DEBUG_LOG=1 -> appends to ./baker-cli-debug.jsonl\n// BAKER_DEBUG_LOG=/tmp/baker.jsonl -> appends to that file\n// BAKER_DEBUG_LOG=0|false|off|no -> disabled (same as unset)\n//\n// When enabled, every HTTP request/response (both the REST client and the\n// canvas engine client), every byte written to stdout/stderr, and the command\n// invocation are appended as JSONL entries. Known secret shapes are redacted\n// before anything is written (best-effort — see redactSecrets). Logging is\n// always fail-open: a logging error is swallowed so it can never break a\n// command. The file rotates once past MAX_BYTES so an always-on capture cannot\n// fill the disk.\n\nimport { appendFileSync, mkdirSync, renameSync, statSync } from \"node:fs\";\nimport { dirname, isAbsolute, resolve } from \"node:path\";\nimport { debugLogSetting } from \"./env.ts\";\n\nconst DEFAULT_FILE = \"baker-cli-debug.jsonl\";\nconst BOOLEAN_ON = new Set([\"1\", \"true\", \"on\", \"yes\"]);\nconst BOOLEAN_OFF = new Set([\"0\", \"false\", \"off\", \"no\"]);\n\n// Rotate once the active file passes this size (one older generation, `<file>.1`,\n// is kept). Bounds growth for the always-on sandbox capture without a per-write stat.\nconst MAX_BYTES = 64 * 1024 * 1024;\n\n// Best-effort redaction. Response/request bodies are stored as escaped strings, so\n// key/value matching is unreliable there; instead we scrub the token *values*\n// themselves (quote-agnostic) plus the auth header and Baker keys. This is not\n// exhaustive — a secret in an unrecognized shape can still be written. See README.\nconst REDACTIONS: ReadonlyArray<readonly [RegExp, string]> = [\n [/Bearer\\s+[A-Za-z0-9._-]+/g, \"Bearer [redacted]\"], // Authorization headers\n // Both prefixes, captured rather than listed twice: `bkr_` does not contain\n // the literal `bk_`, so the company-key rule matched no part of the read-only\n // credential a Session review runs with.\n [/\\b(bkr?)_[A-Za-z0-9]+/g, \"$1_[redacted]\"], // Baker API keys\n [/eyJ[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+/g, \"[redacted-jwt]\"], // JWTs / id tokens\n [/ya29\\.[A-Za-z0-9._-]+/g, \"[redacted-google-token]\"], // Google OAuth access tokens\n [/1\\/\\/[A-Za-z0-9._-]+/g, \"[redacted-google-refresh]\"], // Google refresh tokens\n [/EAA[A-Za-z0-9]{20,}/g, \"[redacted-meta-token]\"], // Meta/Facebook access tokens\n];\n\nexport function redactSecrets(input: string): string {\n let out = input;\n for (const [pattern, replacement] of REDACTIONS) {\n out = out.replace(pattern, replacement);\n }\n return out;\n}\n\nconst CREDENTIAL_VALUE = \"[credential value]\";\nconst CREDENTIAL_PUT_PATH = \"/api/credentials/put\";\n\n/**\n * `baker credentials put` carries a credential's value, the one payload this\n * log must never hold whatever its shape: the Session review reads this file.\n * Keyed on the route, not on a secret pattern, so a value no rule recognises\n * is still dropped.\n */\nfunction withoutCredentialValue(url: string, body: unknown): unknown {\n let path: string;\n try {\n path = new URL(url).pathname;\n } catch {\n path = url;\n }\n if (path !== CREDENTIAL_PUT_PATH || body === null || typeof body !== \"object\" || !(\"value\" in body)) {\n return body;\n }\n return { ...body, value: CREDENTIAL_VALUE };\n}\n\nconst PUT_FLAGS_WITH_VALUES = new Set([\"--host\", \"--label\"]);\n\n/** A value typed after `credentials put` (refused by the command, but already in argv) never reaches the log. */\nfunction argvWithoutCredentialValue(argv: string[]): string[] {\n if (argv[0] !== \"credentials\" || argv[1] !== \"put\") {\n return argv;\n }\n let nameSeen = false;\n let keepNext = false;\n let redactNext = false;\n return argv.map((arg, index) => {\n if (index < 2) return arg;\n if (keepNext || redactNext) {\n const kept = keepNext ? arg : CREDENTIAL_VALUE;\n keepNext = false;\n redactNext = false;\n return kept;\n }\n if (arg.startsWith(\"--value=\")) return `--value=${CREDENTIAL_VALUE}`;\n redactNext = arg === \"--value\";\n keepNext = PUT_FLAGS_WITH_VALUES.has(arg);\n if (arg.startsWith(\"-\")) return arg;\n const isName = !nameSeen;\n nameSeen = true;\n return isName ? arg : CREDENTIAL_VALUE;\n });\n}\n\nfunction resolveTarget(): string | undefined {\n const raw = debugLogSetting();\n if (!raw) {\n return undefined;\n }\n const lower = raw.toLowerCase();\n if (BOOLEAN_OFF.has(lower)) {\n return undefined;\n }\n const target = BOOLEAN_ON.has(lower) ? DEFAULT_FILE : raw;\n return isAbsolute(target) ? target : resolve(process.cwd(), target);\n}\n\nexport function isDebugLogEnabled(): boolean {\n return resolveTarget() !== undefined;\n}\n\nlet sessionId: string | undefined;\nfunction getSessionId(): string {\n if (!sessionId) {\n sessionId = `${process.pid}-${Date.now()}`;\n }\n return sessionId;\n}\n\n// Per-resolved-path bookkeeping so the hot path skips redundant syscalls: the\n// directory is ensured once and the current size is read once (via statSync),\n// then tracked in-process. Resets when the resolved path changes.\nlet trackedPath: string | undefined;\nlet bytesInFile = 0;\n\nfunction ensureReady(filePath: string): void {\n if (trackedPath !== filePath) {\n trackedPath = filePath;\n mkdirSync(dirname(filePath), { recursive: true });\n try {\n bytesInFile = statSync(filePath).size;\n } catch {\n bytesInFile = 0;\n }\n }\n}\n\nfunction append(entry: Record<string, unknown>): void {\n const filePath = resolveTarget();\n if (!filePath) {\n return;\n }\n try {\n const payload = { ts: new Date().toISOString(), session: getSessionId(), ...entry };\n const line = `${redactSecrets(JSON.stringify(payload))}\\n`;\n ensureReady(filePath);\n const size = Buffer.byteLength(line);\n if (bytesInFile + size > MAX_BYTES) {\n renameSync(filePath, `${filePath}.1`);\n bytesInFile = 0;\n }\n appendFileSync(filePath, line);\n bytesInFile += size;\n } catch {\n // Fail-open: debug logging must never break a command.\n }\n}\n\nfunction errorToPlain(err: unknown): Record<string, unknown> {\n if (err instanceof Error) {\n const code = (err as { code?: unknown }).code;\n return { name: err.name, message: err.message, ...(typeof code === \"string\" ? { code } : {}) };\n }\n return { message: String(err) };\n}\n\nlet invocationStartedAt: number | undefined;\n\nexport function logInvocation(argv: string[]): void {\n invocationStartedAt = Date.now();\n append({ kind: \"invocation\", argv: argvWithoutCredentialValue(argv), cwd: process.cwd() });\n}\n\n/**\n * Close an invocation with its outcome and its wall time.\n *\n * Until this existed the log could say what was called and what every HTTP hop\n * cost, and nothing at all about whether the command succeeded or how long the\n * whole thing took — so \"this verb is slow\" and \"this verb fails half the time\"\n * were both unanswerable from the one record that sees every call. `durationMs`\n * is measured from `logInvocation`, which runs before any command does.\n */\nexport function logExit(code: number): void {\n append({\n kind: \"exit\",\n code,\n ...(invocationStartedAt === undefined ? {} : { durationMs: Date.now() - invocationStartedAt }),\n });\n}\n\nlet exitHookInstalled = false;\n\n/**\n * Write the exit record however the process ends.\n *\n * `process.on(\"exit\")` is the only hook that sees all of them — a clean return, a\n * `process.exit` from the error handler, an unhandled rejection — and it is\n * synchronous-only, which is exactly what `appendFileSync` needs. Installed once,\n * and only when logging is on, so the disabled path adds no listener at all.\n */\nexport function installExitRecord(): void {\n if (exitHookInstalled || !isDebugLogEnabled()) {\n return;\n }\n exitHookInstalled = true;\n process.on(\"exit\", (code) => {\n logExit(typeof code === \"number\" ? code : 0);\n });\n}\n\nexport type HttpLogEntry = {\n source: \"cli\" | \"canvas\";\n method: string;\n url: string;\n requestBody?: unknown;\n status?: number;\n ok?: boolean;\n responseBody?: string;\n durationMs?: number;\n error?: unknown;\n};\n\nexport function debugLogHttp(entry: HttpLogEntry): void {\n if (!isDebugLogEnabled()) {\n return;\n }\n append({\n kind: \"http\",\n source: entry.source,\n method: entry.method,\n url: entry.url,\n durationMs: entry.durationMs,\n request: withoutCredentialValue(entry.url, entry.requestBody),\n status: entry.status,\n ok: entry.ok,\n response: entry.responseBody,\n error: entry.error === undefined ? undefined : errorToPlain(entry.error),\n });\n}\n\n// Reads a response body for logging without consuming it for the real caller.\nexport async function readBodyForLog(response: Response): Promise<string | undefined> {\n if (!isDebugLogEnabled()) {\n return undefined;\n }\n try {\n return await response.clone().text();\n } catch {\n return undefined;\n }\n}\n\n// Method syntax (bivariant) so a real `process.stdout`/`stderr` and a plain\n// test double both satisfy it.\ntype Writable = { write(...args: unknown[]): boolean };\n\n// Wraps a stream's `write` so every chunk is mirrored into the debug log while\n// the original write semantics (args + return value) are preserved. A single\n// streaming TextDecoder per stream decodes byte chunks so a multi-byte character\n// split across two writes is reassembled instead of garbled.\nexport function tapStream(name: string, stream: Writable): void {\n const original = stream.write.bind(stream) as (...args: unknown[]) => boolean;\n const decoder = new TextDecoder(\"utf-8\");\n stream.write = ((chunk: unknown, ...rest: unknown[]): boolean => {\n try {\n const text =\n typeof chunk === \"string\"\n ? chunk\n : chunk instanceof Uint8Array\n ? decoder.decode(chunk, { stream: true })\n : String(chunk);\n append({ kind: \"output\", stream: name, text });\n } catch {\n // ignore capture failures\n }\n return original(chunk, ...rest);\n }) as typeof stream.write;\n}\n\nlet tapsInstalled = false;\n\n// Mirrors everything written to stdout/stderr into the debug log. Installed once,\n// only when enabled, so the common (disabled) path leaves the streams untouched.\nexport function installStreamTaps(): void {\n if (tapsInstalled || !isDebugLogEnabled()) {\n return;\n }\n tapsInstalled = true;\n tapStream(\"stdout\", process.stdout);\n tapStream(\"stderr\", process.stderr);\n}\n"],"mappings":"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","names":[]}