@koda-sl/baker-cli 0.305.0-dev.582739c3e → 0.305.0-dev.881741ff3

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.
package/README.md CHANGED
@@ -352,6 +352,7 @@ The same lines also go to stderr. A read with nothing to report prints none of i
352
352
  | Preset | Description | Default date range |
353
353
  |------------------------|------------------------------------------|--------------------|
354
354
  | `campaign-performance` | Campaign metrics overview | LAST_30_DAYS |
355
+ | `conversions-by-action` | Which conversion action each campaign's conversions actually are | LAST_30_DAYS |
355
356
  | `keyword-analysis` | Keyword performance per ad group | LAST_30_DAYS |
356
357
  | `keyword-serving` | Why a keyword is limited — status reasons, first-page bid estimate vs max CPC, quality score | ALL_TIME |
357
358
  | `positive-keywords` | Positive (targeting) keywords only | ALL_TIME |
@@ -366,6 +367,8 @@ The same lines also go to stderr. A read with nothing to report prints none of i
366
367
 
367
368
  Every preset except the negatives lists and `account-summary` filters to **actually-serving entities** by default — the full status chain (`campaign.status = 'ENABLED' AND ad_group.status = 'ENABLED' AND ad_group_ad.status = 'ENABLED'`, as applicable) — and selects the status columns so you can see them. Pass `--include-paused` to widen to `!= 'REMOVED'` (keeps paused, still drops removed). Raw GAQL (no `--preset`) has no such default: a query over a serving-hierarchy resource without a status filter emits a `SERVING_SCOPE` warning reminding you to add the chain, because Google Ads has no single serving flag.
368
369
 
370
+ **Conversion rates across campaigns are not comparable by default.** `metrics.conversions` is a sum over whatever conversion actions are primary for each campaign, and those differ campaign by campaign — a webinar registration in one, an info request in the next. A read that selects a conversion metric, is not segmented by conversion action, and is not pinned to one campaign (`WHERE campaign.id = …`) comes back with a `CONVERSION_MIX` warning: a total across those rows adds different events, and one campaign's rate is not comparable with another's. `--preset conversions-by-action` is the read that settles it. Like every other warning it travels in `--output csv|jsonl|md` and on the `--out` file summary.
371
+
369
372
  **"Eligible (Limited)" keywords:** an enabled keyword that barely shows is invisible in the performance presets — near-zero impressions look the same whether it is outbid, low quality, or rarely searched. `--preset keyword-serving` reads the signals behind Google Ads' status column: `ad_group_criterion.primary_status` and `primary_status_reasons` (the status enum has no `LIMITED` value — "(Limited)" lives in the reasons while the status stays `ELIGIBLE`), `position_estimates.first_page_cpc_micros` (the number in "Below first page bid (€3.63)"), the max CPC actually in force and where it comes from, quality score split into ad relevance / landing page / expected CTR, approval status, and `system_serving_status`. Any read whose rows carry those fields — preset or raw GAQL — comes back with a `KEYWORD_SERVING_LIMITED` warning naming the affected keywords (each by its ad group, since one keyword's copies share a criterion id) and the bid each would need. Below-first-page-bid and low-quality want opposite fixes (raise the bid vs. improve relevance), so the warning reports them separately. On `--out` the verdict is folded across pages as they stream to disk and returned on the file summary; like every other warning it also travels in `--output csv|jsonl|md`.
370
373
 
371
374
  The warning defers to Google: when the query selects `primary_status_reasons`, a keyword Google did not flag is not reported, even if its bid sits under the first-page estimate — the estimate is advisory and the bid may not be the criterion's to set under a portfolio strategy. Only an estimates-only read (no reasons selected) falls back to inferring the limit from the bid gap.
@@ -1342,6 +1345,8 @@ baker experiment fold # make the page
1342
1345
 
1343
1346
  **The goal is one event, named by its key.** `--goal submit:book-a-call` — the same vocabulary the event stream uses (`form:<flow>:<node>:<trigger>`, `submit:<flow>`, `page:<name>`, `exit:<host>`), and `baker analytics conversions --candidates` lists the keys these pages produce. Not a conversion *name*, which groups several keys: a variant that swaps the page's Form for a shorter one converts on a different key, and a name would either count both endings as one outcome or count the new version as never converting at all — `--variant-goal` gives the new version its own event when it has one. The goal is fixed for the life of the test and the verdict is read on it alone. Every other conversion the company counts comes back under `secondary` on every result, with the same interval and a one-word reading, and decides nothing. A conversion counts for a visitor wherever it happens after they were shown a version — on the page, on another page, in a delivered lead, or in a deal the CRM posted a week later by email.
1344
1347
 
1348
+ **`start` checks that both versions still fire a conversion on the same action.** A conversion is a name matched against each page's own `data-baker-*` declarations, so the two versions agree about what an event means only because a person made them agree — and nothing downstream can tell when they stop. `start` compares the sections the two versions do not share and names any conversion whose markup differs, because a step moved from a pop-up to the open page keeps its name and starts counting a scroll instead of a click. A forked section is supposed to change, so most of these are fine; the one that is not is the one nobody looked at. Give the new version's step its own name when it is a different action.
1349
+
1345
1350
  **Nothing ends a test on its own.** `status` returns a verdict; `finish --keep original|variant` is the decision, and it takes effect within seconds with nothing to publish. Ending a `keep_running` test records `inconclusive`. Going against the verdict is allowed and recorded. `pause` and `resume` switch the split off and on without losing the test; `cancel` is `finish --keep original` for a live test and takes a staged one back off the session. `update` changes what a test *says* — `--because`, `--change`, `--evidence` (repeatable, `source:what it showed`, optionally `|link`) — at any time, writes down what a finished one taught (`--learning`), and changes what it *measures* — `--goal`, `--lift`, `--expect` — only while it is still staged.
1346
1351
 
1347
1352
  **A finished test is a record.** Its counts are frozen when it ends, so it reads the same next year as it did that day; its `learning` is what it taught, in a person's words. `history` returns every finished test — the claim, what was different, the numbers with their interval and confidence, what was kept and whether that went against the numbers, whether the belief held, and the learning — and `plan` returns the same entries as `pastTests` for the page it sizes, so a hypothesis is chosen against the page's record rather than in ignorance of it.
@@ -4073,7 +4078,7 @@ Pick a `source` discriminator and declare the kind you expect. See [Ingestion](#
4073
4078
 
4074
4079
  **Params** — discriminated on `source`:
4075
4080
 
4076
- `source: "url"` (yt-dlp / direct fetch / Handinger):
4081
+ `source: "url"` (yt-dlp / direct fetch / Firecrawl):
4077
4082
 
4078
4083
  | Name | Type | Required | Constraint |
4079
4084
  |---|---|---|---|
@@ -4093,7 +4098,7 @@ Pick a `source` discriminator and declare the kind you expect. See [Ingestion](#
4093
4098
 
4094
4099
  **Path-source notes:** the canvas is **not portable** to another machine without the file. Cache key folds the file's `mtime:size`, so editing the file invalidates the cache automatically. Supported extensions: `png`, `jpg`/`jpeg`, `webp`, `gif`, `avif`, `svg`, `mp4`, `webm`, `mov`, `m4v`, `mp3`, `wav`, `m4a`, `ogg`, `flac`, `json`, `txt`, `md`, `markdown`, `html`/`htm`, `csv`, `ttf`, `otf`, `woff`, `woff2`. Unknown extensions fall back to magic-byte sniffing for common image formats (and an SVG content sniff), else `kind_mismatch`. **Any `expect: "image"` in a format image-generation models can't read (SVG, AVIF, HEIC, TIFF, BMP) is normalized to PNG on ingest** — model-safe rasters (`jpeg`/`png`/`gif`/`webp`) pass through untouched, everything else is transcoded so a reference can never 400 a generation. This applies to **both `source: "path"` and `source: "url"`** (URL images are fetched and normalized locally, since the backend can't run the rasterizer). SVG gets density-aware upscaling (longest edge near 2048px, transparency preserved). The normalized asset carries `metadata.rasterized_from` set to the source format (e.g. `"svg"`, `"avif"`). **Video (`expect: "video"`) duration is probed from the file's ISO-BMFF (`mp4`/`mov`/`m4v`) header** and stamped as the canonical `duration_ms` (and `metadata.duration_ms`); other containers (e.g. `webm`) leave it unset. Downstream `video_deconstruct` uses this declared duration to size its ingest-poll timeout and preflight — without it those fall back to worst-case budgets and a single deconstruct step can hit the action time limit.
4095
4100
 
4096
- **Cost:** 0 engine credits for direct fetch + yt-dlp + local file. Handinger charges per scrape.
4101
+ **Cost:** 0 engine credits for direct fetch + yt-dlp + local file. Firecrawl charges per scrape.
4097
4102
 
4098
4103
  ---
4099
4104
 
@@ -5407,10 +5412,10 @@ baker canvas catalog | jq '.compositions[].id'
5407
5412
  // URL source — YouTube audio-only (yt-dlp -x)
5408
5413
  { "id": "src", "type": "ingest", "params": { "source": "url", "url": "https://youtu.be/dQw4w9WgXcQ", "expect": "audio" } }
5409
5414
 
5410
- // URL source — blog post → clean markdown (Handinger)
5415
+ // URL source — blog post → clean markdown (Firecrawl)
5411
5416
  { "id": "src", "type": "ingest", "params": { "source": "url", "url": "https://example.com/blog/post", "expect": "text" } }
5412
5417
 
5413
- // URL source — PDF → markdown (Handinger handles PDFs)
5418
+ // URL source — PDF → markdown (Firecrawl handles PDFs)
5414
5419
  { "id": "src", "type": "ingest", "params": { "source": "url", "url": "https://example.com/whitepaper.pdf", "expect": "text" } }
5415
5420
 
5416
5421
  // URL source — raw markdown file (direct fetch)
@@ -5438,7 +5443,7 @@ baker canvas catalog | jq '.compositions[].id'
5438
5443
  | url | video / audio | any | `yt_dlp` (works on platform URLs *and* direct file URLs via the generic extractor) |
5439
5444
  | url | image / json / font | any | `direct_fetch` (HTTP GET) |
5440
5445
  | url | text | ends in `.txt` or `.md` | `direct_fetch` |
5441
- | url | text | anything else (HTML, PDF, …) | `handinger` `/markdown` (extracted markdown — HTML/PDF made readable) |
5446
+ | url | text | anything else (HTML, PDF, …) | `firecrawl` `/v2/scrape` (extracted markdown — HTML/PDF made readable) |
5442
5447
 
5443
5448
  If the bytes' mime contradicts `expect` (e.g. `expect: "image"` but the URL serves `text/html`, or a `.pdf` file is passed as `expect: "image"`), the node fails with a clear `kind_mismatch` error instead of forwarding bad bytes downstream.
5444
5449
 
@@ -5481,7 +5486,7 @@ If the bytes' mime contradicts `expect` (e.g. `expect: "image"` but the URL serv
5481
5486
  file_size?: number, // bytes
5482
5487
  original_filename?: string, // basename of the source path
5483
5488
 
5484
- strategy: "direct_fetch" | "handinger" | "yt_dlp" | "local_file",
5489
+ strategy: "direct_fetch" | "firecrawl" | "yt_dlp" | "local_file",
5485
5490
  ingested_at: string, // ISO 8601 timestamp
5486
5491
 
5487
5492
  // yt-dlp adds (when available from --dump-json):
@@ -5492,7 +5497,7 @@ If the bytes' mime contradicts `expect` (e.g. `expect: "image"` but the URL serv
5492
5497
  width?: number,
5493
5498
  height?: number,
5494
5499
 
5495
- // Handinger adds:
5500
+ // Firecrawl adds:
5496
5501
  word_count?: number,
5497
5502
  },
5498
5503
 
@@ -5511,8 +5516,8 @@ The `url` is a stable R2 URL — remote downstream nodes (e.g. `video_lipsync`)
5511
5516
  |-----------------------|----------------|------------------------------------------------------------------------------|
5512
5517
  | `INVALID_PARAMS` | engine validator | Missing/malformed fields, mixing `url` + `path`, unknown `source`, unknown `expect`. |
5513
5518
  | `kind_mismatch` | url + direct_fetch | URL returned bytes whose mime doesn't match `expect`. Message: `ingest expect=<X> but <url> returned <mime> (kind=<Y>)`. |
5514
- | `kind_mismatch` | path + local_file | File mime (from extension or magic-byte sniff) doesn't match `expect`, *or* mime doesn't map to any of our kinds (e.g. local PDFs aren't supported as `text` — host them via URL for Handinger to extract). |
5515
- | `provider_error` | url + handinger / direct_fetch | Handinger `/markdown` non-2xx, or upstream `fetch` non-ok. |
5519
+ | `kind_mismatch` | path + local_file | File mime (from extension or magic-byte sniff) doesn't match `expect`, *or* mime doesn't map to any of our kinds (e.g. local PDFs aren't supported as `text` — host them via URL for Firecrawl to extract). |
5520
+ | `provider_error` | url + firecrawl / direct_fetch | Firecrawl `/v2/scrape` non-2xx or no markdown in the response, **or the target site answering 4xx/5xx** (Firecrawl reports that inside a 200 carrying the error page, so it is refused rather than stored as the page), or upstream `fetch` non-ok. Terminal: a bad host, a 404 and a block page fail identically on a retry and each retry is billed. Retried: a 429 from either side, a Firecrawl-side 5xx, a dropped connection. |
5516
5521
  | `file_not_found` | path + local_file | `stat()` returned `ENOENT`. Error names the absolute resolved path. |
5517
5522
  | `not_a_file` | path + local_file | Path exists but is a directory / block device / socket. |
5518
5523
  | `permission_denied` | path + local_file | `stat()` or `readFile()` returned `EACCES`. |
@@ -5529,7 +5534,7 @@ All execution-time failures flow through `NodeExecutionError` so they appear in
5529
5534
  - **Engine-level cache** (`canvas/.cache/index/`) keys by node params + node version + node-supplied cache extras. Re-running the same canvas with the same params is a cache hit and skips the network/yt-dlp/local-read entirely.
5530
5535
  - **yt-dlp version is folded into the cache key** for `source: "url"` with `expect: video | audio`. Upgrading yt-dlp busts the cache automatically, since new versions can produce different bitstreams.
5531
5536
  - **Local file `mtime + size` is folded into the cache key** for `source: "path"`. Editing the file (any save bumps mtime) invalidates the cache so you get fresh ingestion. mtime+size is cheap to read at validate time; for cases where mtime is unreliable (`cp -p`, content-only changes), force-bypass with `--cache-policy bypass` or change the canvas's `cache_salt`.
5532
- - **No TTL.** Handinger and direct fetch don't cache-bust on page changes — if the source page updates, use `--cache-policy bypass` or change `cache_salt`.
5537
+ - **No TTL.** Firecrawl and direct fetch don't cache-bust on page changes — if the source page updates, use `--cache-policy bypass` or change `cache_salt`.
5533
5538
 
5534
5539
  **Local files (`source: "path"`):**
5535
5540
 
@@ -5552,10 +5557,10 @@ ingest { source: "path", expect: "video" } → video_transcribe # Local
5552
5557
  **Requirements:**
5553
5558
 
5554
5559
  - `yt-dlp` and `ffmpeg` on PATH for `source: "url"` with `expect: "video" | "audio"` (already in the E2B sandbox image; install locally with `pip install yt-dlp` and `brew install ffmpeg` / equivalent).
5555
- - `HANDINGER_API_KEY` on the Convex backend for `source: "url"` with `expect: "text"` on non-`.md`/`.txt` URLs.
5560
+ - `FIRECRAWL_API_KEY` on the Convex backend for `source: "url"` with `expect: "text"` on non-`.md`/`.txt` URLs.
5556
5561
  - No additional dependencies for `source: "path"` — just filesystem access.
5557
5562
 
5558
- **Cost:** zero engine credits for direct_fetch + yt_dlp. Handinger charges per scrape on your Handinger account (configured outside Baker).
5563
+ **Cost:** zero engine credits for direct_fetch + yt_dlp. Firecrawl charges per scrape on your Firecrawl account (configured outside Baker).
5559
5564
 
5560
5565
  ---
5561
5566
 
@@ -6088,6 +6093,12 @@ Pushing to `main` with changes in `packages/cli/` triggers the GitHub Actions wo
6088
6093
 
6089
6094
  - **0.271.0**: **`--format demandGenVideo` — the Demand Gen ad most Demand Gen spend actually runs through can be edited.** A `DEMAND_GEN_VIDEO_RESPONSIVE_AD` keeps its copy in `demandGenVideoResponsiveAd`, which has a `longHeadlines` list no other ad format has and takes its business name as a text asset rather than a string. Baker modelled only the multi-asset Demand Gen ad and labelled all four Demand Gen types as one format, so every served video ad was uneditable: the strict content schema refused `longHeadlines` and named a field instead of a missing format, and the only route left was the Google Ads interface. On the account that surfaced it, Demand Gen was 76% of the spend. New flags on `ads create|update`: `--long-headlines` (up to 5 × 90 chars, this format only), `--video-assets`, `--logo-image-assets`, `--call-to-action-assets`; a create with no video is refused with the `assets create` call to make first, and an omitted media list on an update leaves that media on the ad. Two more gates ship with it. **A content flag a format does not read is now refused instead of dropped** — `--long-headlines` on `--format demandGen` staged the copy without them and reported success, the same silent-drop the backend's strict schemas exist to stop. And **the format is checked against the live `ad_group_ad.ad.type` when it stages**: a mismatch is refused with the right format named, and the two Demand Gen types Baker writes no format for (`DEMAND_GEN_CAROUSEL_AD`, `DEMAND_GEN_PRODUCT_AD`) are refused by name, so "Baker cannot edit this ad" arrives at stage time rather than as a publish failure a Session too late.
6090
6095
 
6096
+ - **0.293.0**: an `assets detach` whose asset link is not on the account is refused **at stage time**, and an asset-link read that leaves the link's own status unscoped is refused in preflight. Both come from one publish: 74 of 85 staged ops came back `RESOURCE_NOT_FOUND` — 67 ad-group sitelinks and 7 campaign ones — twelve hours after the agent reported them staged, against a Session that had already merged. Nothing checked them: `google.assetLink.detach` had no entry in the stage-time snapshot map, so its target read back as "read failed" (silent) instead of "not found" (refused), exactly the gap that was closed for `audienceCriterion`. The read that produced the list had the same hole from the other side — it scoped `campaign.status` and `ad_group.status` and said nothing about `campaign_asset.status` / `ad_group_asset.status`, and Google returns a removed link like any other row, so already-detached extensions read as live. The snapshot query excludes `REMOVED` for that reason: a retired link is zero rows, which is the refusal. A bare-id target still only warns — it names no level, so there is no row to read and a refusal would be a guess.
6097
+
6098
+ Two rules the new chains brought with them. **`campaign_asset` and `ad_group_asset` segment on their owners**, so a `campaign.*` / `ad_group.*` field used in WHERE must also be SELECTed or Google refuses the whole statement (`EXPECTED_REFERENCED_FIELD_IN_SELECT_CLAUSE`) — the rule that already applied to `campaign_budget`, now keyed off a table rather than one resource, and applied to the query `INCOMPLETE_STATUS_CHAIN` hands back so the corrected command is one you can actually run. Checked live on v23 against 3865083115: `ad_group_ad`, `ad_group_criterion` and `asset_group_asset` filter their ancestors freely, these two do not. And **`asset_group_asset` carries the same status** its campaign and asset group do not, so its chain gained `asset_group_asset.status` too — the Performance Max `asset-performance` preset scopes and shows it.
6099
+
6100
+ Underneath both: **`query` now returns a `.resource_name` you selected.** Google injects `resource_name` into every row whether or not it was asked for, and the backend stripped all of them as noise — including the one the SELECT clause named. For an asset link that column *is* the identity (`customers/{cid}/adGroupAssets/{adGroupId}~{assetId}~{FIELD_TYPE}`; there is no id to detach by), so the read the docs prescribe — `SELECT ad_group_asset.resource_name …` — came back without it and composing the string by hand was the only route left. Auto-injected resource names are still stripped; a selected one is kept. This also repairs the `asset-groups detach` instruction, which pointed at `asset_group_asset.resource_name` and could not have worked either.
6101
+
6091
6102
  - **0.291.0**: **the image handover Task no longer depends on having uploaded the images.** It was built only from staged `asset.create` image ops, so a Session that audited a live account — found the brand campaign carrying 22 image extensions and the generic one carrying none, and reached for `assets attach` on assets that were *already in the library* — got Google's refusal and nothing else: no upload to hang a Task on, and a CLI hint telling the agent not to raise one by hand because "Baker stages it itself". The one step only the user can take reached them as a sentence in a chat that scrolls away. An image `assets attach` is now **skipped rather than refused**: it still never reaches Google (re-verified live with `validate_only` on v23 *and* v25 against 3865083115, against a control op that validates clean in the same request — `AD_IMAGE` → `UNSUPPORTED_FIELD_TYPE` at customer and campaign level, `MARKETING_IMAGE` → `FIELD_TYPE_INCOMPATIBLE_WITH_CAMPAIGN_TYPE`, `BUSINESS_LOGO` → `CUSTOMER_NOT_VERIFIED`, which is what proves the field type is the objection; **we are not on Google's allowlist for it**), but the skip carries the image and campaign names, recorded on the draft so the next staged op cannot recompute the Task away, and a batch that also carries sitelinks keeps them instead of failing whole. The schema stopped refusing the field type for the same reason: a boundary refusal has no chat to record a handover against.
6092
6103
 
6093
6104
  ### Testing a pre-release in sandboxes
@@ -17,9 +17,9 @@ import {
17
17
  shouldEscalate
18
18
  } from "./chunk-WFWU3CHS.js";
19
19
 
20
- // ../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/88b6d0c76c4dff408de4044be85791896193f2435af7c280bc59312c3fb9649c/node_modules/safe-stable-stringify/index.js
20
+ // ../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/509a695c9237c6e3727f1f1f99169917f329e3ff3f7877be290b684e24bf153d/node_modules/safe-stable-stringify/index.js
21
21
  var require_safe_stable_stringify = __commonJS({
22
- "../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/88b6d0c76c4dff408de4044be85791896193f2435af7c280bc59312c3fb9649c/node_modules/safe-stable-stringify/index.js"(exports, module) {
22
+ "../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/509a695c9237c6e3727f1f1f99169917f329e3ff3f7877be290b684e24bf153d/node_modules/safe-stable-stringify/index.js"(exports, module) {
23
23
  "use strict";
24
24
  var { hasOwnProperty } = Object.prototype;
25
25
  var stringify = configure2();
@@ -758,6 +758,19 @@ async function parseErrorBody(res) {
758
758
  }
759
759
  return classifyHttpError(res.status, errObj, message2);
760
760
  }
761
+ function providerVerdict(status, errObj, message2) {
762
+ if (errObj.provider === void 0 && errObj.code !== "provider_error") return null;
763
+ return {
764
+ kind: "provider",
765
+ status,
766
+ provider: errObj.provider,
767
+ code: errObj.code ?? "provider_error",
768
+ message: message2,
769
+ // A 4xx is terminal unless the backend says otherwise — the opposite
770
+ // default from the 502 branch, where the status itself means "try again".
771
+ retryable: errObj.retryable ?? false
772
+ };
773
+ }
761
774
  function classifyHttpError(status, errObj, message2) {
762
775
  if (errObj.code === CONTENT_POLICY_CODE) {
763
776
  return { kind: "content_policy", status, provider: errObj.provider, message: message2 };
@@ -766,7 +779,7 @@ function classifyHttpError(status, errObj, message2) {
766
779
  return { kind: "unauthorized", status, message: message2 };
767
780
  }
768
781
  if (status === 400 || status === 422) {
769
- return { kind: "validation", status, message: message2, details: errObj.details };
782
+ return providerVerdict(status, errObj, message2) ?? { kind: "validation", status, message: message2, details: errObj.details };
770
783
  }
771
784
  if (status === 502 || status === 504) {
772
785
  if (errObj.code === "provider_timeout" || status === 504) {
@@ -1098,7 +1111,7 @@ function resolveAdaptFormats(params) {
1098
1111
  return params.formats ?? [];
1099
1112
  }
1100
1113
 
1101
- // ../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/88b6d0c76c4dff408de4044be85791896193f2435af7c280bc59312c3fb9649c/node_modules/safe-stable-stringify/esm/wrapper.js
1114
+ // ../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/509a695c9237c6e3727f1f1f99169917f329e3ff3f7877be290b684e24bf153d/node_modules/safe-stable-stringify/esm/wrapper.js
1102
1115
  var import__ = __toESM(require_safe_stable_stringify(), 1);
1103
1116
  var configure = import__.default.configure;
1104
1117
  var wrapper_default = import__.default;
@@ -1139,6 +1152,21 @@ function normalizeForCanonical(value) {
1139
1152
  return void 0;
1140
1153
  }
1141
1154
 
1155
+ // ../canvas-contract/src/castName.ts
1156
+ function withoutCastName(text, names) {
1157
+ let out = text;
1158
+ for (const name of names) {
1159
+ const token = name.trim();
1160
+ if (token.length < 3) continue;
1161
+ const pattern = new RegExp(`(?<![\\p{L}\\p{N}])${escapeForPattern(token)}(?![\\p{L}\\p{N}])`, "giu");
1162
+ out = out.replace(pattern, "the person in the reference");
1163
+ }
1164
+ return out;
1165
+ }
1166
+ function escapeForPattern(value) {
1167
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1168
+ }
1169
+
1142
1170
  // ../canvas-contract/src/clipWeight.ts
1143
1171
  var MB_PER_SECOND_720P = 2.13;
1144
1172
  var RESOLUTION_WEIGHT = {
@@ -6237,7 +6265,7 @@ function resolveStrategy(params) {
6237
6265
  case "text": {
6238
6266
  const p = safePathname(params.url);
6239
6267
  const isRawText = RAW_TEXT_EXTENSIONS.some((ext) => p.endsWith(ext));
6240
- return isRawText ? "direct_fetch" : "handinger";
6268
+ return isRawText ? "direct_fetch" : "firecrawl";
6241
6269
  }
6242
6270
  default: {
6243
6271
  params.expect;
@@ -6254,10 +6282,10 @@ function safePathname(rawUrl) {
6254
6282
  }
6255
6283
  var ingestNode = defineNode({
6256
6284
  id: "ingest",
6257
- version: "1.2.0",
6285
+ version: "1.3.0",
6258
6286
  category: "io",
6259
6287
  location: "local",
6260
- summary: "Ingest an external URL or a local file into the asset store. Declare the kind you expect (image/video/audio/text/json/font); the node picks the strategy. For source=url: yt-dlp for video/audio (YouTube/TikTok/Vimeo/etc. and direct file URLs), Handinger for HTML/PDF pages \u2192 markdown, direct HTTP fetch for binary URLs (images, fonts) and raw .txt/.md. For source=path: read from the local filesystem and upload to R2.",
6288
+ summary: "Ingest an external URL or a local file into the asset store. Declare the kind you expect (image/video/audio/text/json/font); the node picks the strategy. For source=url: yt-dlp for video/audio (YouTube/TikTok/Vimeo/etc. and direct file URLs), Firecrawl for HTML/PDF pages \u2192 markdown, direct HTTP fetch for binary URLs (images, fonts) and raw .txt/.md. For source=path: read from the local filesystem and upload to R2.",
6261
6289
  when_to_use: 'Use as the on-ramp for any external asset a canvas needs. `expect` makes the output port statically typed so downstream wiring is checked before any byte is fetched. Pick `source: "url"` for portable canvas; pick `source: "path"` for local files (canvas is not portable across machines without the file). Compose with other nodes for richer pipelines (e.g. `ingest expect=video` \u2192 `video_transcribe`).',
6262
6290
  inputs: IngestInputs,
6263
6291
  params: IngestParams,
@@ -6284,8 +6312,8 @@ function runStrategy(strategy, params, ctx) {
6284
6312
  switch (strategy) {
6285
6313
  case "direct_fetch":
6286
6314
  return execDirectFetch(params, ctx);
6287
- case "handinger":
6288
- return execHandinger(params, ctx);
6315
+ case "firecrawl":
6316
+ return execFirecrawl(params, ctx);
6289
6317
  case "yt_dlp":
6290
6318
  return execYtDlp(params, ctx);
6291
6319
  case "local_file":
@@ -6338,11 +6366,11 @@ async function ingestImageUrl(url, ctx) {
6338
6366
  ctx
6339
6367
  });
6340
6368
  }
6341
- async function execHandinger(params, ctx) {
6369
+ async function execFirecrawl(params, ctx) {
6342
6370
  const result = await callBackendExec({
6343
6371
  nodeType: "ingest",
6344
6372
  nodeVersion: ingestNode.version,
6345
- params: { strategy: "handinger", url: params.url },
6373
+ params: { strategy: "firecrawl", url: params.url },
6346
6374
  inputs: {},
6347
6375
  outputKinds: { asset: "text" },
6348
6376
  ctx
@@ -8228,9 +8256,9 @@ async function substituteCompositionFiles(tmp, values) {
8228
8256
  }
8229
8257
  await writeFile6(entryPath, output, "utf-8");
8230
8258
  }
8231
- var MAX_WORKERS = 4;
8259
+ var MAX_WORKERS = 8;
8232
8260
  function workerCount() {
8233
- return Math.max(1, Math.min(MAX_WORKERS, cpus().length));
8261
+ return Math.max(1, Math.min(MAX_WORKERS, cpus().length - 1));
8234
8262
  }
8235
8263
  async function runRender(opts) {
8236
8264
  const { tmp, outputPath, params, meta, ctx } = opts;
@@ -9570,6 +9598,7 @@ export {
9570
9598
  describeFailureReason,
9571
9599
  AD_FORMAT_PLATFORMS,
9572
9600
  platformFormats,
9601
+ withoutCastName,
9573
9602
  frameRealismDirection,
9574
9603
  SEEDANCE_DURATIONS,
9575
9604
  ELEVENLABS_MAX_MUSIC_LENGTH_MS,
@@ -9633,4 +9662,4 @@ export {
9633
9662
  defaultRegistry,
9634
9663
  createEngineFromEnv
9635
9664
  };
9636
- //# sourceMappingURL=chunk-JI6QIAD3.js.map
9665
+ //# sourceMappingURL=chunk-ZILPAKE6.js.map