@koda-sl/baker-cli 0.114.1 → 0.115.0-dev.3bcc79f9c
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 +83 -0
- package/dist/{chunk-ZWMMCLJ4.js → chunk-OCMOQOIJ.js} +241 -57
- package/dist/chunk-OCMOQOIJ.js.map +1 -0
- package/dist/cli.js +2753 -652
- package/dist/cli.js.map +1 -1
- package/dist/engine/index.d.ts +30 -0
- package/dist/engine/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-ZWMMCLJ4.js.map +0 -1
package/README.md
CHANGED
|
@@ -653,6 +653,56 @@ baker ads google library search-competitors "crm software" --location uk
|
|
|
653
653
|
|
|
654
654
|
---
|
|
655
655
|
|
|
656
|
+
### Staged writes (`baker ads google budgets|campaigns|...`)
|
|
657
|
+
|
|
658
|
+
Write commands **never touch the Google Ads API at stage time**. Each command stages a create/update/pause/resume/remove op against the current chat's draft (`BAKER_CHAT_ID`); the dashboard shows it as a pending "Google Ads" change, and the whole draft applies as one atomic `GoogleAdsService.Mutate` when the chat is published. Feature-flagged per company (`companies.googleAdsWriteEnabled`) — off by default = a fully simulated publish with zero real API calls.
|
|
659
|
+
|
|
660
|
+
Reference not-yet-created resources with `g_temp_*` refs returned by earlier `create` commands (they map to Google's temporary negative-ID resource names inside one atomic batch).
|
|
661
|
+
|
|
662
|
+
```bash
|
|
663
|
+
# Build a Search campaign bottom-up, chaining temp refs
|
|
664
|
+
baker ads google budgets create --customer-id 1234567890 --name "Search budget" --amount 50
|
|
665
|
+
# → { ref: "g_temp_ab12", ... }
|
|
666
|
+
baker ads google campaigns create --customer-id 1234567890 --name "Brand — Search" \
|
|
667
|
+
--channel-type SEARCH --budget-ref g_temp_ab12 --bidding-strategy MANUAL_CPC
|
|
668
|
+
baker ads google ad-groups create --customer-id 1234567890 --name "Brand terms" --campaign-ref g_temp_<campaign>
|
|
669
|
+
baker ads google keywords add --customer-id 1234567890 --ad-group-ref g_temp_<adgroup> --text "brand name" --match-type EXACT
|
|
670
|
+
# Shared negative keyword list → attach to the campaign
|
|
671
|
+
baker ads google keyword-lists create --customer-id 1234567890 --name "Brand exclusions"
|
|
672
|
+
baker ads google keyword-lists add --customer-id 1234567890 --list-ref g_temp_<list> --text "free" --match-type BROAD
|
|
673
|
+
baker ads google keyword-lists attach --customer-id 1234567890 --campaign-ref g_temp_<campaign> --list-ref g_temp_<list>
|
|
674
|
+
|
|
675
|
+
# Batch keywords — one command stages the whole set as ONE request (up to 500)
|
|
676
|
+
baker ads google keywords add --customer-id 1234567890 --ad-group-ref g_temp_<adgroup> \
|
|
677
|
+
--text "brand name, brand shop:PHRASE, brand store" --match-type EXACT
|
|
678
|
+
baker ads google keyword-lists add --customer-id 1234567890 --list-ref <list-id> --file negatives.txt
|
|
679
|
+
# negatives.txt: one keyword[:MATCH_TYPE] per line; blank lines and "# comments" are skipped
|
|
680
|
+
# Responsive search ad
|
|
681
|
+
baker ads google ads create --customer-id 1234567890 --ad-group-ref g_temp_<adgroup> \
|
|
682
|
+
--headlines "Fast Widgets,Buy Online,Free Shipping" --descriptions "Best widgets around.,Ships tomorrow." \
|
|
683
|
+
--final-url https://example.com
|
|
684
|
+
# Responsive display ad — images are fields on the ad; register each image as an asset first (returns a g_temp_* ref)
|
|
685
|
+
baker ads google assets create --customer-id 1234567890 --file '{"type":"image","imageId":"<baker_image_id>","name":"Hero"}'
|
|
686
|
+
baker ads google ads create --customer-id 1234567890 --ad-group-ref g_temp_<adgroup> --format responsiveDisplay \
|
|
687
|
+
--headlines "Come back and save" --long-headline "Finish signing up and get 20% off" \
|
|
688
|
+
--descriptions "Fast setup, no card required" --business-name "Acme" --final-url https://example.com \
|
|
689
|
+
--marketing-images g_temp_<img_landscape> --square-marketing-images g_temp_<img_square> --logo-images g_temp_<img_logo>
|
|
690
|
+
|
|
691
|
+
# Review / undo staged changes
|
|
692
|
+
baker ads google draft list # readable campaign ▸ ad group ▸ ad tree + completeness advisories
|
|
693
|
+
baker ads google draft list --json # raw JSON envelope for scripting
|
|
694
|
+
baker ads google draft remove g_temp_ab12 # cascades to dependents
|
|
695
|
+
baker ads google draft clear
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
Command groups: `budgets`, `campaigns`, `ad-groups`, `keywords` (add/update/remove), `negative-keywords`, `keyword-lists`, `ads`, `assets`, `audiences`, `conversions`, `bidding-strategies`, `labels`, `campaign-criteria`, and `draft`. Amounts are in major currency units (converted to micros). Money/bids: `--amount`, `--cpc-bid`, `--target-cpa` take major units; `--target-roas` a ratio. Less-common ops accept a `--file <payload.json>` (flags override file keys). Updates target a resource name or bare id as the positional argument; a target that names an op staged earlier **amends it in place**.
|
|
699
|
+
|
|
700
|
+
**Reviewing the draft** — `baker ads google draft list` renders everything you've staged as a grouped campaign ▸ ad group ▸ ad tree (with the simulated/live mode banner and non-blocking completeness advisories), the CLI counterpart to the dashboard's Google Ads tab. Pass `--json` for the raw envelope. Aim for a fully built campaign — 2–4 ad groups, ≥5 keywords each, 2–4 RSAs with 8–12 headlines, ≥4 sitelinks / ≥3 callouts / ≥1 structured snippet, and ≥1 shared negative list; the advisories flag what's still thin.
|
|
701
|
+
|
|
702
|
+
**Batch keyword adds** — `keywords add`, `negative-keywords add`, and `keyword-lists add` take a whole batch in one command: comma-separate `--text` entries and/or pass `--file <list.txt>` (one keyword per line). A `:EXACT`/`:PHRASE`/`:BROAD` suffix per entry overrides the `--match-type` default. Batches stage all-or-nothing as one request (limit 500); each keyword still lands as its own draft op, so it stays individually removable/amendable.
|
|
703
|
+
|
|
704
|
+
---
|
|
705
|
+
|
|
656
706
|
### Caching
|
|
657
707
|
|
|
658
708
|
Google Ads data is cached at two levels:
|
|
@@ -2549,6 +2599,17 @@ baker canvas run my-canvas.json
|
|
|
2549
2599
|
# single node — that old serial workaround is obsolete.
|
|
2550
2600
|
baker canvas run my-canvas.json --parallel 8
|
|
2551
2601
|
|
|
2602
|
+
# 2c. Runs persist across sandboxes/sessions by default: node results sync to a
|
|
2603
|
+
# company-scoped remote cache (small JSON pointers; bytes stay in R2), so a FRESH
|
|
2604
|
+
# sandbox re-runs an already-computed canvas at zero credits — assets rehydrate
|
|
2605
|
+
# from R2, sha-verified. Every run also posts a durable history record (per-node
|
|
2606
|
+
# outputs, credits, cached/fresh) that powers the dashboard's Creatives
|
|
2607
|
+
# generations timeline. Opt out with --remote-cache off (env
|
|
2608
|
+
# BAKER_CANVAS_REMOTE_CACHE=off) and --no-record. With --remote-cache off,
|
|
2609
|
+
# assets are not uploaded, so a recorded run keeps its stats but has no
|
|
2610
|
+
# browsable outputs — pass --no-record too if you want nothing persisted.
|
|
2611
|
+
baker canvas run my-canvas.json --remote-cache off --no-record
|
|
2612
|
+
|
|
2552
2613
|
# 3. Inspect a finished run (per-node timing, file list, optional video thumbs)
|
|
2553
2614
|
baker canvas inspect <run_id>
|
|
2554
2615
|
|
|
@@ -3852,6 +3913,7 @@ baker canvas run ./reference-ad.video.canvas.json
|
|
|
3852
3913
|
| Flag | Default | Effect |
|
|
3853
3914
|
|---|---|---|
|
|
3854
3915
|
| `--out <path>` | `<video-dir>/<name>.video.canvas.json` | Where to write the canvas (composition is copied alongside). |
|
|
3916
|
+
| `--slug <slug>` | — | Creative slug (lowercase kebab): writes the canvas to `src/creatives/<slug>/<slug>.canvas.json` — the repo convention that attaches every run to the creative's dashboard generation history. `--out` wins over `--slug`. |
|
|
3855
3917
|
| `--frames <mode>` | `generate` | `generate` emits ONE recast keyframe per scene (the original frame is dropped so the dropped `el_*` assets drive identity); `reuse` wires the real extracted first+last frames straight into the clips (faithful, cheaper, no recast). |
|
|
3856
3918
|
| `--ambient` | off | Give silent **b-roll** scenes native diegetic ambient (Seedance `generate_audio`), mixed deep under the music bed. Talking scenes already carry voice; check levels don't muddy the mix before keeping it. |
|
|
3857
3919
|
| `--max-scenes <n>` | all source scenes | **Cost lever that reduces fidelity** — caps the deconstruct, MERGING away every scene beyond the cap (fewer cuts, lost beats). Prints a warning when set; omit it to reproduce every scene. |
|
|
@@ -3893,6 +3955,7 @@ baker canvas run ./static-ad.canvas.json
|
|
|
3893
3955
|
|---|---|---|
|
|
3894
3956
|
| `--context <text>` | — | Known provenance (advertiser, category, market) to ground the describe. |
|
|
3895
3957
|
| `--out <path>` | `<image-dir>/static-ad.canvas.json` (cwd when `<image>` is a URL) | Where to write the canvas (`prompt.json` is written alongside). |
|
|
3958
|
+
| `--slug <slug>` | — | Creative slug (lowercase kebab): writes the canvas to `src/creatives/<slug>/<slug>.canvas.json` — the repo convention that attaches every run to the creative's dashboard generation history. `--out` wins over `--slug`. |
|
|
3896
3959
|
| `--describe-model <id>` | registry default (`~google/gemini-pro-latest`) | Override the `image_describe` model. |
|
|
3897
3960
|
| `--select-model <id>` | registry default (`~google/gemini-flash-latest`) | Override the element-selection `text_generate` model. |
|
|
3898
3961
|
| `--layout-model <id>` | registry default (`~google/gemini-flash-latest`) | Override the global-layout `text_generate` model. |
|
|
@@ -4364,6 +4427,26 @@ import {
|
|
|
4364
4427
|
} from "@koda-sl/baker-cli/engine";
|
|
4365
4428
|
```
|
|
4366
4429
|
|
|
4430
|
+
## Creatives
|
|
4431
|
+
|
|
4432
|
+
Publish an approved canvas render as a first-class Baker creative. The image uploads to the Baker image library (tagged `creative`), a creative record is created/updated, and the command prints the creative reference JSON the dashboard renders in chat.
|
|
4433
|
+
|
|
4434
|
+
```bash
|
|
4435
|
+
baker creatives publish ./canvas/<run_id>/<final>.png --title "Spring Offer 4x5" \
|
|
4436
|
+
--slug spring-offer-4x5 --run-id r_01JXYZ... \
|
|
4437
|
+
--source-reference-url "https://www.facebook.com/ads/library/?id=..."
|
|
4438
|
+
```
|
|
4439
|
+
|
|
4440
|
+
| Flag | Effect |
|
|
4441
|
+
|---|---|
|
|
4442
|
+
| `--title <text>` | Required. Human title for the creative. |
|
|
4443
|
+
| `--slug <slug>` | Creative slug (`src/creatives/<slug>/`) — attaches the image to that creative's row, marks it `published`. |
|
|
4444
|
+
| `--run-id <r_…>` | Pins the approved generation from the creative's run history as the published one. |
|
|
4445
|
+
| `--source-reference-url <url>` | Original reference ad URL, recorded on the creative. |
|
|
4446
|
+
| `--context <text>` | Optional describe context for the uploaded image asset. |
|
|
4447
|
+
|
|
4448
|
+
Without `--slug` the command behaves as before (one creative record per published image). With `--slug` it upserts the repo-convention row — the same one the dashboard's Creatives tab and the `src/creatives/{slug}/` folder describe — so publish, repo sync, and run history all land on a single record regardless of order.
|
|
4449
|
+
|
|
4367
4450
|
## Help & Discovery
|
|
4368
4451
|
|
|
4369
4452
|
Every command supports `--help` for usage info:
|
|
@@ -24,9 +24,9 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
24
24
|
mod
|
|
25
25
|
));
|
|
26
26
|
|
|
27
|
-
//
|
|
27
|
+
// ../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/810146e81bae4e3a061fe487864f2fde80c4b03b886877dc0f1fffbc6480b67e/node_modules/safe-stable-stringify/index.js
|
|
28
28
|
var require_safe_stable_stringify = __commonJS({
|
|
29
|
-
"
|
|
29
|
+
"../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/810146e81bae4e3a061fe487864f2fde80c4b03b886877dc0f1fffbc6480b67e/node_modules/safe-stable-stringify/index.js"(exports, module) {
|
|
30
30
|
"use strict";
|
|
31
31
|
var { hasOwnProperty } = Object.prototype;
|
|
32
32
|
var stringify = configure2();
|
|
@@ -159,9 +159,9 @@ var require_safe_stable_stringify = __commonJS({
|
|
|
159
159
|
}
|
|
160
160
|
if (value) {
|
|
161
161
|
return (value2) => {
|
|
162
|
-
let
|
|
163
|
-
if (typeof value2 !== "function")
|
|
164
|
-
throw new Error(
|
|
162
|
+
let message2 = `Object can not safely be stringified. Received type ${typeof value2}`;
|
|
163
|
+
if (typeof value2 !== "function") message2 += ` (${value2.toString()})`;
|
|
164
|
+
throw new Error(message2);
|
|
165
165
|
};
|
|
166
166
|
}
|
|
167
167
|
}
|
|
@@ -673,6 +673,9 @@ var HttpClient = class {
|
|
|
673
673
|
async postJson(path16, body, signal) {
|
|
674
674
|
return await this.requestJson("POST", path16, body, signal);
|
|
675
675
|
}
|
|
676
|
+
async putJson(path16, body, signal) {
|
|
677
|
+
return await this.requestJson("PUT", path16, body, signal);
|
|
678
|
+
}
|
|
676
679
|
async getJson(path16, signal) {
|
|
677
680
|
return await this.requestJson("GET", path16, void 0, signal);
|
|
678
681
|
}
|
|
@@ -700,8 +703,8 @@ var HttpClient = class {
|
|
|
700
703
|
try {
|
|
701
704
|
const res = await this.fetchFn(url, {
|
|
702
705
|
method,
|
|
703
|
-
headers: method === "
|
|
704
|
-
body: method === "
|
|
706
|
+
headers: method === "GET" ? { Authorization: `Bearer ${this.apiKey}` } : { "Content-Type": "application/json", Authorization: `Bearer ${this.apiKey}` },
|
|
707
|
+
body: method === "GET" ? void 0 : JSON.stringify(body),
|
|
705
708
|
signal: controller.signal
|
|
706
709
|
});
|
|
707
710
|
if (res.ok) return { kind: "value", value: await res.json() };
|
|
@@ -738,33 +741,33 @@ async function parseErrorBody(res) {
|
|
|
738
741
|
const errObj = body.error ?? {};
|
|
739
742
|
return classifyHttpError(res.status, errObj, errObj.message ?? `HTTP ${res.status}`);
|
|
740
743
|
}
|
|
741
|
-
function classifyHttpError(status, errObj,
|
|
744
|
+
function classifyHttpError(status, errObj, message2) {
|
|
742
745
|
if (errObj.code === CONTENT_POLICY_CODE) {
|
|
743
|
-
return { kind: "content_policy", status, provider: errObj.provider, message };
|
|
746
|
+
return { kind: "content_policy", status, provider: errObj.provider, message: message2 };
|
|
744
747
|
}
|
|
745
748
|
if (status === 401 || status === 403) {
|
|
746
|
-
return { kind: "unauthorized", status, message };
|
|
749
|
+
return { kind: "unauthorized", status, message: message2 };
|
|
747
750
|
}
|
|
748
751
|
if (status === 400 || status === 422) {
|
|
749
|
-
return { kind: "validation", status, message, details: errObj.details };
|
|
752
|
+
return { kind: "validation", status, message: message2, details: errObj.details };
|
|
750
753
|
}
|
|
751
754
|
if (status === 502 || status === 504) {
|
|
752
755
|
if (errObj.code === "provider_timeout" || status === 504) {
|
|
753
|
-
return { kind: "timeout", provider: errObj.provider, message };
|
|
756
|
+
return { kind: "timeout", provider: errObj.provider, message: message2 };
|
|
754
757
|
}
|
|
755
758
|
return {
|
|
756
759
|
kind: "provider",
|
|
757
760
|
status,
|
|
758
761
|
provider: errObj.provider,
|
|
759
762
|
code: errObj.code ?? "provider_error",
|
|
760
|
-
message,
|
|
763
|
+
message: message2,
|
|
761
764
|
retryable: errObj.retryable ?? true
|
|
762
765
|
};
|
|
763
766
|
}
|
|
764
767
|
if (status >= 500 || status === 429) {
|
|
765
|
-
return { kind: "server", status, message };
|
|
768
|
+
return { kind: "server", status, message: message2 };
|
|
766
769
|
}
|
|
767
|
-
return { kind: "validation", status, message, details: errObj.details };
|
|
770
|
+
return { kind: "validation", status, message: message2, details: errObj.details };
|
|
768
771
|
}
|
|
769
772
|
function backoffMs(attempt) {
|
|
770
773
|
return 1e3 * 2 ** attempt;
|
|
@@ -835,6 +838,27 @@ var BackendClient = class {
|
|
|
835
838
|
signal
|
|
836
839
|
);
|
|
837
840
|
}
|
|
841
|
+
/** Remote cache lookup. A miss (404) — or an old backend without the route — returns null. */
|
|
842
|
+
async getCacheEntry(cacheKey, signal) {
|
|
843
|
+
try {
|
|
844
|
+
const res = await this.http.getJson(`/api/canvas/cache/${encodeURIComponent(cacheKey)}`, signal);
|
|
845
|
+
return res.entry;
|
|
846
|
+
} catch (e) {
|
|
847
|
+
if (e instanceof BackendHttpError && "status" in e.detail && e.detail.status === 404) return null;
|
|
848
|
+
throw e;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
async putCacheEntry(entry, signal) {
|
|
852
|
+
await this.http.putJson(
|
|
853
|
+
`/api/canvas/cache/${encodeURIComponent(entry.cacheKey)}`,
|
|
854
|
+
entry,
|
|
855
|
+
signal
|
|
856
|
+
);
|
|
857
|
+
}
|
|
858
|
+
/** Durable run-history record — POST /api/canvas/runs (idempotent server-side on runId). */
|
|
859
|
+
async recordRun(payload, signal) {
|
|
860
|
+
await this.http.postJson("/api/canvas/runs", payload, signal);
|
|
861
|
+
}
|
|
838
862
|
getArtifact(kind, name, version, signal) {
|
|
839
863
|
const path16 = version ? `/api/canvas/artifacts/${encodeURIComponent(kind)}/${encodeURIComponent(name)}/${encodeURIComponent(version)}` : `/api/canvas/artifacts/${encodeURIComponent(kind)}/${encodeURIComponent(name)}`;
|
|
840
864
|
return this.http.getJson(path16, signal);
|
|
@@ -858,14 +882,17 @@ function requireCredentialsFromEnv(env = process.env) {
|
|
|
858
882
|
}
|
|
859
883
|
return c;
|
|
860
884
|
}
|
|
885
|
+
function remoteCacheEnabledFromEnv(env = process.env) {
|
|
886
|
+
return env.BAKER_CANVAS_REMOTE_CACHE !== "off";
|
|
887
|
+
}
|
|
861
888
|
|
|
862
889
|
// src/engine/engine/errors.ts
|
|
863
890
|
function isBlocking(issue) {
|
|
864
891
|
return issue.severity !== "warning";
|
|
865
892
|
}
|
|
866
893
|
var CanvasError = class extends Error {
|
|
867
|
-
constructor(
|
|
868
|
-
super(
|
|
894
|
+
constructor(message2) {
|
|
895
|
+
super(message2);
|
|
869
896
|
this.name = "CanvasError";
|
|
870
897
|
}
|
|
871
898
|
};
|
|
@@ -922,7 +949,7 @@ function describeCause(c) {
|
|
|
922
949
|
}
|
|
923
950
|
}
|
|
924
951
|
|
|
925
|
-
//
|
|
952
|
+
// ../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/810146e81bae4e3a061fe487864f2fde80c4b03b886877dc0f1fffbc6480b67e/node_modules/safe-stable-stringify/esm/wrapper.js
|
|
926
953
|
var import__ = __toESM(require_safe_stable_stringify(), 1);
|
|
927
954
|
var configure = import__.default.configure;
|
|
928
955
|
var wrapper_default = import__.default;
|
|
@@ -1583,6 +1610,158 @@ function encodeRandom() {
|
|
|
1583
1610
|
return out;
|
|
1584
1611
|
}
|
|
1585
1612
|
|
|
1613
|
+
// src/engine/storage/remote-cache-store.ts
|
|
1614
|
+
var CANVAS_ASSETS_URL_SEGMENT = "/canvas-assets/";
|
|
1615
|
+
function isPersistedAssetRef(ref) {
|
|
1616
|
+
return typeof ref.url === "string" && ref.url.includes(`${CANVAS_ASSETS_URL_SEGMENT}${ref.sha256}`);
|
|
1617
|
+
}
|
|
1618
|
+
function isAssetRefLike(value) {
|
|
1619
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && typeof value.sha256 === "string" && typeof value.mime === "string";
|
|
1620
|
+
}
|
|
1621
|
+
function collectAssetRefLikes(value, out = []) {
|
|
1622
|
+
if (Array.isArray(value)) {
|
|
1623
|
+
for (const item of value) collectAssetRefLikes(item, out);
|
|
1624
|
+
return out;
|
|
1625
|
+
}
|
|
1626
|
+
if (typeof value !== "object" || value === null) return out;
|
|
1627
|
+
if (isAssetRefLike(value)) {
|
|
1628
|
+
out.push(value);
|
|
1629
|
+
}
|
|
1630
|
+
for (const item of Object.values(value)) collectAssetRefLikes(item, out);
|
|
1631
|
+
return out;
|
|
1632
|
+
}
|
|
1633
|
+
function entryFullyPersisted(entry) {
|
|
1634
|
+
return collectAssetRefLikes(entry.outputs).every((ref) => isPersistedAssetRef(ref));
|
|
1635
|
+
}
|
|
1636
|
+
function stripLocalFields(entry) {
|
|
1637
|
+
const clone = JSON.parse(JSON.stringify(entry));
|
|
1638
|
+
for (const ref of collectAssetRefLikes(clone.outputs)) {
|
|
1639
|
+
delete ref.path;
|
|
1640
|
+
delete ref.bytes;
|
|
1641
|
+
}
|
|
1642
|
+
return clone;
|
|
1643
|
+
}
|
|
1644
|
+
var RemoteCacheStore = class {
|
|
1645
|
+
client;
|
|
1646
|
+
log;
|
|
1647
|
+
constructor(client, log) {
|
|
1648
|
+
this.client = client;
|
|
1649
|
+
this.log = log ?? (() => void 0);
|
|
1650
|
+
}
|
|
1651
|
+
async get(cacheKey) {
|
|
1652
|
+
return await this.client.getCacheEntry(cacheKey);
|
|
1653
|
+
}
|
|
1654
|
+
async put(entry) {
|
|
1655
|
+
if (!entryFullyPersisted(entry)) {
|
|
1656
|
+
this.log(`[cache ] ${entry.cacheKey.slice(0, 12)}\u2026 has local-only assets, kept local`);
|
|
1657
|
+
return;
|
|
1658
|
+
}
|
|
1659
|
+
const stripped = stripLocalFields(entry);
|
|
1660
|
+
if (stripped.refs.length > MAX_REMOTE_REFS) {
|
|
1661
|
+
stripped.refs = stripped.refs.slice(0, MAX_REMOTE_REFS);
|
|
1662
|
+
}
|
|
1663
|
+
await this.client.putCacheEntry(stripped);
|
|
1664
|
+
}
|
|
1665
|
+
};
|
|
1666
|
+
var MAX_REMOTE_REFS = 512;
|
|
1667
|
+
var LayeredCacheStore = class {
|
|
1668
|
+
rootDir;
|
|
1669
|
+
local;
|
|
1670
|
+
remote;
|
|
1671
|
+
assets;
|
|
1672
|
+
log;
|
|
1673
|
+
constructor(opts) {
|
|
1674
|
+
this.local = opts.local;
|
|
1675
|
+
this.remote = opts.remote;
|
|
1676
|
+
this.assets = opts.assets;
|
|
1677
|
+
this.rootDir = opts.local.rootDir;
|
|
1678
|
+
this.log = opts.log ?? (() => void 0);
|
|
1679
|
+
}
|
|
1680
|
+
async get(cacheKey) {
|
|
1681
|
+
const localHit = await this.local.get(cacheKey);
|
|
1682
|
+
if (localHit) return localHit;
|
|
1683
|
+
let remoteEntry;
|
|
1684
|
+
try {
|
|
1685
|
+
remoteEntry = await this.remote.get(cacheKey);
|
|
1686
|
+
} catch (e) {
|
|
1687
|
+
this.log(`[cache ] remote lookup failed (${message(e)}) \u2014 treating as miss`);
|
|
1688
|
+
return null;
|
|
1689
|
+
}
|
|
1690
|
+
if (!remoteEntry) return null;
|
|
1691
|
+
let rehydrated;
|
|
1692
|
+
try {
|
|
1693
|
+
rehydrated = await this.rehydrate(remoteEntry);
|
|
1694
|
+
} catch (e) {
|
|
1695
|
+
this.log(`[cache ] ${cacheKey.slice(0, 12)}\u2026 rehydration failed (${message(e)}) \u2014 treating as miss`);
|
|
1696
|
+
return null;
|
|
1697
|
+
}
|
|
1698
|
+
await this.local.put(rehydrated);
|
|
1699
|
+
return rehydrated;
|
|
1700
|
+
}
|
|
1701
|
+
async put(entry) {
|
|
1702
|
+
await this.local.put(entry);
|
|
1703
|
+
try {
|
|
1704
|
+
await this.remote.put(entry);
|
|
1705
|
+
} catch (e) {
|
|
1706
|
+
this.log(`[cache ] remote write failed (${message(e)}) \u2014 entry kept local`);
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
/**
|
|
1710
|
+
* Download every referenced asset into the local content-addressed store
|
|
1711
|
+
* (sha-verified) and stamp fresh local paths. Any ref that cannot be
|
|
1712
|
+
* rehydrated fails the WHOLE entry — a partially-hydrated cache hit would
|
|
1713
|
+
* crash materialization later with a far less actionable error.
|
|
1714
|
+
*/
|
|
1715
|
+
async rehydrate(entry) {
|
|
1716
|
+
const clone = JSON.parse(JSON.stringify(entry));
|
|
1717
|
+
for (const ref of collectAssetRefLikes(clone.outputs)) {
|
|
1718
|
+
if (!isPersistedAssetRef(ref)) {
|
|
1719
|
+
throw new Error(`ref ${ref.sha256.slice(0, 12)}\u2026 has no persisted url`);
|
|
1720
|
+
}
|
|
1721
|
+
const ingested = await this.assets.ingestRemote({
|
|
1722
|
+
kind: typeof ref.kind === "string" ? ref.kind : "json",
|
|
1723
|
+
url: ref.url,
|
|
1724
|
+
sha256: ref.sha256,
|
|
1725
|
+
mime: ref.mime,
|
|
1726
|
+
metadata: ref.metadata ?? void 0
|
|
1727
|
+
});
|
|
1728
|
+
ref.path = ingested.path;
|
|
1729
|
+
}
|
|
1730
|
+
return clone;
|
|
1731
|
+
}
|
|
1732
|
+
};
|
|
1733
|
+
function message(e) {
|
|
1734
|
+
return e instanceof Error ? e.message : String(e);
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
// src/engine/nodes/remote/upload.ts
|
|
1738
|
+
async function presignAndPut(args) {
|
|
1739
|
+
const { putUrl, publicUrl } = await args.ctx.client.presignAssetUpload(args.sha256, args.mime, args.ctx.signal);
|
|
1740
|
+
const putRes = await fetch(putUrl, {
|
|
1741
|
+
method: "PUT",
|
|
1742
|
+
body: new Uint8Array(args.bytes),
|
|
1743
|
+
headers: { "Content-Type": args.mime },
|
|
1744
|
+
signal: args.ctx.signal
|
|
1745
|
+
});
|
|
1746
|
+
if (!putRes.ok) {
|
|
1747
|
+
throw new Error(`upload: presigned PUT failed ${putRes.status} ${putRes.statusText}`);
|
|
1748
|
+
}
|
|
1749
|
+
return publicUrl;
|
|
1750
|
+
}
|
|
1751
|
+
async function ensureUploaded(ref, ctx) {
|
|
1752
|
+
if (ref.url) return ref;
|
|
1753
|
+
const bytes = await ctx.assets.readBytes(ref.sha256, ref.mime);
|
|
1754
|
+
const url = await presignAndPut({ bytes, sha256: ref.sha256, mime: ref.mime, ctx });
|
|
1755
|
+
return { ...ref, url };
|
|
1756
|
+
}
|
|
1757
|
+
async function persistOutputAssetUrls(outputs, ctx) {
|
|
1758
|
+
for (const ref of collectAssetRefLikes(outputs)) {
|
|
1759
|
+
if (isPersistedAssetRef(ref)) continue;
|
|
1760
|
+
const bytes = await ctx.assets.readBytes(ref.sha256, ref.mime);
|
|
1761
|
+
ref.url = await presignAndPut({ bytes, sha256: ref.sha256, mime: ref.mime, ctx });
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1586
1765
|
// src/engine/schema/canvas.ts
|
|
1587
1766
|
import { z } from "zod";
|
|
1588
1767
|
var REF_PREFIX = "$ref:";
|
|
@@ -2799,6 +2978,7 @@ var Engine = class {
|
|
|
2799
2978
|
cache;
|
|
2800
2979
|
outputsDir;
|
|
2801
2980
|
log;
|
|
2981
|
+
persistAssets;
|
|
2802
2982
|
constructor(opts) {
|
|
2803
2983
|
this.registry = opts.registry;
|
|
2804
2984
|
this.client = opts.client;
|
|
@@ -2806,6 +2986,7 @@ var Engine = class {
|
|
|
2806
2986
|
this.cache = opts.cache;
|
|
2807
2987
|
this.outputsDir = opts.outputsDir;
|
|
2808
2988
|
this.log = opts.log ?? (() => void 0);
|
|
2989
|
+
this.persistAssets = opts.persistAssets ?? false;
|
|
2809
2990
|
}
|
|
2810
2991
|
validate(canvas) {
|
|
2811
2992
|
return validateCanvas(canvas, this.registry);
|
|
@@ -2852,7 +3033,7 @@ var Engine = class {
|
|
|
2852
3033
|
`[done ] ${stats.cached_nodes}/${stats.total_nodes} cached, ${stats.total_credits} credits, ${stats.duration_ms}ms`
|
|
2853
3034
|
);
|
|
2854
3035
|
this.log(`outputs in: ${writer.runDir}`);
|
|
2855
|
-
return { run_id: runId, output, outputs_by_node: outputs, stats, outputs_dir: writer.runDir };
|
|
3036
|
+
return { run_id: runId, output, outputs_by_node: outputs, stats, outputs_dir: writer.runDir, node_runs: nodeRuns };
|
|
2856
3037
|
}
|
|
2857
3038
|
async runLayers(canvas, outputs, runId, writer, opts, counters, nodeRuns) {
|
|
2858
3039
|
const layers = topologicalLayers(this.pruneToOutput(canvas, buildGraph(canvas)));
|
|
@@ -2949,6 +3130,14 @@ var Engine = class {
|
|
|
2949
3130
|
const credits = def.cost ? def.cost({ params: parsedParams }).credits : 0;
|
|
2950
3131
|
const outputsObj = result;
|
|
2951
3132
|
outputs[node.id] = outputsObj;
|
|
3133
|
+
if (this.persistAssets) {
|
|
3134
|
+
try {
|
|
3135
|
+
await persistOutputAssetUrls(outputsObj, ctx);
|
|
3136
|
+
} catch (e) {
|
|
3137
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
3138
|
+
this.log(`[warn ] ${node.id}: asset persistence failed (${msg}) \u2014 outputs stay local-only`);
|
|
3139
|
+
}
|
|
3140
|
+
}
|
|
2952
3141
|
if (policy === "read_write") {
|
|
2953
3142
|
await this.cache.put({
|
|
2954
3143
|
cacheKey: prepared.cacheKey,
|
|
@@ -3267,27 +3456,6 @@ var FontRef = BaseAssetRef.extend({
|
|
|
3267
3456
|
});
|
|
3268
3457
|
var AssetRef = z4.discriminatedUnion("kind", [ImageRef, VideoRef, AudioRef, JsonRef, TextRef, FontRef]);
|
|
3269
3458
|
|
|
3270
|
-
// src/engine/nodes/remote/upload.ts
|
|
3271
|
-
async function presignAndPut(args) {
|
|
3272
|
-
const { putUrl, publicUrl } = await args.ctx.client.presignAssetUpload(args.sha256, args.mime, args.ctx.signal);
|
|
3273
|
-
const putRes = await fetch(putUrl, {
|
|
3274
|
-
method: "PUT",
|
|
3275
|
-
body: new Uint8Array(args.bytes),
|
|
3276
|
-
headers: { "Content-Type": args.mime },
|
|
3277
|
-
signal: args.ctx.signal
|
|
3278
|
-
});
|
|
3279
|
-
if (!putRes.ok) {
|
|
3280
|
-
throw new Error(`upload: presigned PUT failed ${putRes.status} ${putRes.statusText}`);
|
|
3281
|
-
}
|
|
3282
|
-
return publicUrl;
|
|
3283
|
-
}
|
|
3284
|
-
async function ensureUploaded(ref, ctx) {
|
|
3285
|
-
if (ref.url) return ref;
|
|
3286
|
-
const bytes = await ctx.assets.readBytes(ref.sha256, ref.mime);
|
|
3287
|
-
const url = await presignAndPut({ bytes, sha256: ref.sha256, mime: ref.mime, ctx });
|
|
3288
|
-
return { ...ref, url };
|
|
3289
|
-
}
|
|
3290
|
-
|
|
3291
3459
|
// src/engine/nodes/remote/delegate.ts
|
|
3292
3460
|
function delegated(spec) {
|
|
3293
3461
|
return {
|
|
@@ -3682,10 +3850,10 @@ function inferKindFromMime(mime) {
|
|
|
3682
3850
|
if (mime.startsWith("font/")) return "font";
|
|
3683
3851
|
return null;
|
|
3684
3852
|
}
|
|
3685
|
-
function localExecError(ctx,
|
|
3853
|
+
function localExecError(ctx, message2) {
|
|
3686
3854
|
return new NodeExecutionError(ctx.nodeId, ctx.nodeType, {
|
|
3687
3855
|
kind: "local",
|
|
3688
|
-
cause: new Error(`ingest: ${
|
|
3856
|
+
cause: new Error(`ingest: ${message2}`)
|
|
3689
3857
|
});
|
|
3690
3858
|
}
|
|
3691
3859
|
async function execLocalFile(params, ctx) {
|
|
@@ -4352,7 +4520,7 @@ async function refToUrl(ref) {
|
|
|
4352
4520
|
return `data:${ref.mime};base64,${bytes.toString("base64")}`;
|
|
4353
4521
|
}
|
|
4354
4522
|
var ASSET_KINDS = /* @__PURE__ */ new Set(["image", "video", "audio", "json", "text", "font"]);
|
|
4355
|
-
function
|
|
4523
|
+
function isAssetRefLike2(value) {
|
|
4356
4524
|
if (!value || typeof value !== "object") return false;
|
|
4357
4525
|
const v = value;
|
|
4358
4526
|
return typeof v.kind === "string" && ASSET_KINDS.has(v.kind) && typeof v.mime === "string" && typeof v.sha256 === "string" && (typeof v.url === "string" || typeof v.path === "string");
|
|
@@ -4724,8 +4892,8 @@ var NEVER_BLOCK = [
|
|
|
4724
4892
|
/text[_-]?occluded/i
|
|
4725
4893
|
];
|
|
4726
4894
|
var UNAVAILABLE = /unknown command|command not found|not found|Did you mean|Unknown argument|ENOENT/i;
|
|
4727
|
-
function isAdvisory(code,
|
|
4728
|
-
const hay = `${code} ${
|
|
4895
|
+
function isAdvisory(code, message2) {
|
|
4896
|
+
const hay = `${code} ${message2}`;
|
|
4729
4897
|
return NEVER_BLOCK.some((re) => re.test(hay));
|
|
4730
4898
|
}
|
|
4731
4899
|
function parseCheckJson(raw) {
|
|
@@ -4753,10 +4921,10 @@ function classifyLint(json) {
|
|
|
4753
4921
|
for (const f of findings) {
|
|
4754
4922
|
const rec = f;
|
|
4755
4923
|
const code = String(rec?.code ?? "");
|
|
4756
|
-
const
|
|
4924
|
+
const message2 = String(rec?.message ?? "");
|
|
4757
4925
|
const severity = String(rec?.severity ?? "info");
|
|
4758
|
-
const blocking = severity === "error" && !isAdvisory(code,
|
|
4759
|
-
out.push({ source: "lint", code, message, severity: blocking ? "blocking" : "warning" });
|
|
4926
|
+
const blocking = severity === "error" && !isAdvisory(code, message2);
|
|
4927
|
+
out.push({ source: "lint", code, message: message2, severity: blocking ? "blocking" : "warning" });
|
|
4760
4928
|
}
|
|
4761
4929
|
return out;
|
|
4762
4930
|
}
|
|
@@ -4768,9 +4936,9 @@ function classifyInspect(json) {
|
|
|
4768
4936
|
for (const iss of issues) {
|
|
4769
4937
|
const rec = iss;
|
|
4770
4938
|
const code = String(rec?.code ?? rec?.type ?? "overflow");
|
|
4771
|
-
const
|
|
4939
|
+
const message2 = String(rec?.message ?? rec?.detail ?? JSON.stringify(iss));
|
|
4772
4940
|
const severity = rec?.severity ? String(rec.severity) : obj?.ok === false ? "error" : "warning";
|
|
4773
|
-
out.push({ source: "inspect", code, message, severity: severity === "error" ? "blocking" : "warning" });
|
|
4941
|
+
out.push({ source: "inspect", code, message: message2, severity: severity === "error" ? "blocking" : "warning" });
|
|
4774
4942
|
}
|
|
4775
4943
|
return out;
|
|
4776
4944
|
}
|
|
@@ -5215,7 +5383,7 @@ async function buildSubstitutionValues(compositionParams, meta, duration) {
|
|
|
5215
5383
|
}
|
|
5216
5384
|
function coerceImageParam(value) {
|
|
5217
5385
|
if (typeof value === "string") return Promise.resolve(value);
|
|
5218
|
-
if (
|
|
5386
|
+
if (isAssetRefLike2(value)) return refToUrl(value);
|
|
5219
5387
|
throw new Error("hyperframe_render: image param must be a URL string or AssetRef");
|
|
5220
5388
|
}
|
|
5221
5389
|
async function substituteCompositionFiles(tmp, values) {
|
|
@@ -5445,7 +5613,7 @@ async function buildSubstitutionValues2(compositionParams, meta) {
|
|
|
5445
5613
|
}
|
|
5446
5614
|
function coerceImageParam2(value) {
|
|
5447
5615
|
if (typeof value === "string") return Promise.resolve(value);
|
|
5448
|
-
if (
|
|
5616
|
+
if (isAssetRefLike2(value)) return refToUrl(value);
|
|
5449
5617
|
throw new Error("hyperframe_snapshot: image param must be a URL string or AssetRef");
|
|
5450
5618
|
}
|
|
5451
5619
|
async function substituteCompositionFiles2(tmp, values) {
|
|
@@ -6471,17 +6639,29 @@ function createEngineFromEnv(opts = {}) {
|
|
|
6471
6639
|
const cacheDir = opts.cacheDir ?? path15.join(cwd, "canvas", ".cache");
|
|
6472
6640
|
const outputsDir = opts.outputsDir ?? path15.join(cwd, "canvas");
|
|
6473
6641
|
const creds = requireCredentialsFromEnv();
|
|
6642
|
+
const client = new BackendClient({ baseUrl: creds.url, apiKey: creds.apiKey });
|
|
6643
|
+
const assets = new LocalAssetStore(path15.join(cacheDir, "assets"));
|
|
6644
|
+
const localCache = new LocalCacheStore(path15.join(cacheDir, "index"));
|
|
6645
|
+
const remoteCacheEnabled = opts.remoteCache ?? remoteCacheEnabledFromEnv();
|
|
6646
|
+
const cache = remoteCacheEnabled ? new LayeredCacheStore({
|
|
6647
|
+
local: localCache,
|
|
6648
|
+
remote: new RemoteCacheStore(client, opts.log),
|
|
6649
|
+
assets,
|
|
6650
|
+
log: opts.log
|
|
6651
|
+
}) : localCache;
|
|
6474
6652
|
return new Engine({
|
|
6475
6653
|
registry: defaultRegistry(),
|
|
6476
|
-
client
|
|
6477
|
-
assets
|
|
6478
|
-
cache
|
|
6654
|
+
client,
|
|
6655
|
+
assets,
|
|
6656
|
+
cache,
|
|
6479
6657
|
outputsDir,
|
|
6480
|
-
log: opts.log
|
|
6658
|
+
log: opts.log,
|
|
6659
|
+
persistAssets: remoteCacheEnabled
|
|
6481
6660
|
});
|
|
6482
6661
|
}
|
|
6483
6662
|
|
|
6484
6663
|
export {
|
|
6664
|
+
requireCredentialsFromEnv,
|
|
6485
6665
|
LayerExecutionError,
|
|
6486
6666
|
describeFailureReason,
|
|
6487
6667
|
SEEDANCE_DURATIONS,
|
|
@@ -6489,6 +6669,10 @@ export {
|
|
|
6489
6669
|
IMAGE_GENERATE_MODELS,
|
|
6490
6670
|
MODEL_REGISTRY,
|
|
6491
6671
|
resolveConcurrency,
|
|
6672
|
+
ulid,
|
|
6673
|
+
isPersistedAssetRef,
|
|
6674
|
+
collectAssetRefLikes,
|
|
6675
|
+
sha256Hex,
|
|
6492
6676
|
BackendClient2 as BackendClient,
|
|
6493
6677
|
Engine2 as Engine,
|
|
6494
6678
|
LocalAssetStore2 as LocalAssetStore,
|
|
@@ -6499,4 +6683,4 @@ export {
|
|
|
6499
6683
|
defaultRegistry,
|
|
6500
6684
|
createEngineFromEnv
|
|
6501
6685
|
};
|
|
6502
|
-
//# sourceMappingURL=chunk-
|
|
6686
|
+
//# sourceMappingURL=chunk-OCMOQOIJ.js.map
|