@koda-sl/baker-cli 0.359.0-dev.d2f320fd3 → 0.360.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.
@@ -85,6 +85,9 @@ function mapHttpError(status) {
85
85
  if (status === 429) {
86
86
  return "RATE_LIMITED";
87
87
  }
88
+ if (status === 503) {
89
+ return "UNAVAILABLE";
90
+ }
88
91
  return "INTERNAL_ERROR";
89
92
  }
90
93
  async function handleResponse(response) {
@@ -205,4 +208,4 @@ export {
205
208
  apiGet,
206
209
  apiPost
207
210
  };
208
- //# sourceMappingURL=chunk-4QXKWIFC.js.map
211
+ //# sourceMappingURL=chunk-MZQC4LII.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/client.ts"],"sourcesContent":["import { debugLogHttp, readBodyForLog } from \"./debugLog.ts\";\nimport { getEnv } from \"./env.ts\";\n\nconst MAX_RATE_LIMIT_RETRIES = 3;\nconst MAX_TOTAL_WAIT_MS = 2 * 60 * 1000;\n\nasync function fetchWithRateLimitRetry(url: string, init: RequestInit): Promise<Response> {\n let totalWaited = 0;\n\n for (let attempt = 0; attempt <= MAX_RATE_LIMIT_RETRIES; attempt++) {\n const response = await fetch(url, init);\n\n if (response.status !== 429 || attempt >= MAX_RATE_LIMIT_RETRIES) {\n return response;\n }\n\n const retryAfterHeader = response.headers.get(\"Retry-After\");\n const waitMs = retryAfterHeader ? Number(retryAfterHeader) * 1000 : 2000 * 2 ** attempt;\n\n if (totalWaited + waitMs > MAX_TOTAL_WAIT_MS) {\n return response;\n }\n\n totalWaited += waitMs;\n await new Promise((resolve) => setTimeout(resolve, waitMs));\n }\n\n return fetch(url, init);\n}\n\nexport type ErrorCode =\n | \"UNAUTHORIZED\"\n | \"FORBIDDEN\"\n | \"NOT_FOUND\"\n | \"CONFLICT\"\n | \"VALIDATION_ERROR\"\n | \"RATE_LIMITED\"\n | \"UNAVAILABLE\"\n | \"INTERNAL_ERROR\"\n | \"NETWORK_ERROR\"\n | \"TIMEOUT\"\n | \"IMAGE_PROCESSING_ERROR\";\n\nexport class ApiError extends Error {\n code: ErrorCode;\n\n constructor(code: ErrorCode, message: string) {\n super(message);\n this.name = \"ApiError\";\n this.code = code;\n }\n}\n\nconst CONVEX_ID_RE = /^[a-zA-Z0-9_]+$/;\n\nfunction hasControlCharacters(value: string): boolean {\n for (let i = 0; i < value.length; i++) {\n const code = value.charCodeAt(i);\n // Allow tab (9), newline (10), carriage return (13)\n if (code < 32 && code !== 9 && code !== 10 && code !== 13) {\n return true;\n }\n }\n return false;\n}\n\nfunction validateStringValue(value: string): void {\n if (hasControlCharacters(value)) {\n throw new ApiError(\"VALIDATION_ERROR\", \"String value contains invalid control characters\");\n }\n}\n\nexport function validateConvexId(id: string): void {\n if (!CONVEX_ID_RE.test(id)) {\n throw new ApiError(\"VALIDATION_ERROR\", `Invalid ID format: \"${id}\". Expected alphanumeric string.`);\n }\n}\n\nfunction sanitizeParams(params: Record<string, string>): Record<string, string> {\n const sanitized: Record<string, string> = {};\n for (const [key, value] of Object.entries(params)) {\n validateStringValue(value);\n sanitized[key] = value;\n }\n return sanitized;\n}\n\n/**\n * Sent on every call so the backend can attribute immediately-applied writes\n * (library images and videos, followed advertisers) to the Session they were\n * made from. Absent outside a chat-attached environment.\n */\nfunction chatHeader(): Record<string, string> {\n const chatId = getEnv().BAKER_CHAT_ID;\n return chatId ? { \"x-baker-chat-id\": chatId } : {};\n}\n\nfunction mapHttpError(status: number): ErrorCode {\n if (status === 401) {\n return \"UNAUTHORIZED\";\n }\n // 403 is never \"not connected\" — every backend `FORBIDDEN` means connected but\n // not allowed (an asset outside the granted scope, a capability the account\n // lacks). Folding it into UNAUTHORIZED sent agents down the reconnect path,\n // which cannot fix a permission gap and wastes the user's time.\n if (status === 403) {\n return \"FORBIDDEN\";\n }\n if (status === 404) {\n return \"NOT_FOUND\";\n }\n if (status === 422 || status === 400) {\n return \"VALIDATION_ERROR\";\n }\n // 409 is a state the caller has to change, never a fault to sit out. It is\n // what the backend returns for \"this workspace has not switched analytics on\n // yet\" and its siblings — folded into INTERNAL_ERROR, that reaches an agent\n // as a transient failure worth retrying, which it never is.\n if (status === 409) {\n return \"CONFLICT\";\n }\n if (status === 429) {\n return \"RATE_LIMITED\";\n }\n // 503 is the backend's `UNAVAILABLE`: a system it depends on did not answer\n // just now, and asking again is the fix. Folded into INTERNAL_ERROR, it reads\n // exactly like a fault of ours, and the agent cannot tell which one waits out.\n if (status === 503) {\n return \"UNAVAILABLE\";\n }\n return \"INTERNAL_ERROR\";\n}\n\nasync function handleResponse<T>(response: Response): Promise<T> {\n const body = await response.text();\n\n if (!response.ok) {\n let message = `HTTP ${response.status}: ${response.statusText}`;\n try {\n const parsed = JSON.parse(body) as { error?: string | { message?: string }; message?: string };\n if (typeof parsed.error === \"string\") {\n message = parsed.error;\n } else if (parsed.error?.message) {\n message = parsed.error.message;\n } else if (parsed.message) {\n message = parsed.message;\n }\n } catch {\n // Use default message\n }\n throw new ApiError(mapHttpError(response.status), message);\n }\n\n try {\n return JSON.parse(body) as T;\n } catch {\n throw new ApiError(\"INTERNAL_ERROR\", \"Failed to parse API response as JSON\");\n }\n}\n\n/**\n * A GET that also hands back the response headers.\n *\n * Some endpoints have to say something *about* the answer that the answer has\n * no room for — `GET /api/testimonials` returns a bare array and sets\n * `X-Baker-Next-Cursor` when rows past this page remain unexamined, so the rows\n * alone cannot tell one page of a library from the whole of it.\n */\nexport async function apiGetWithHeaders<T>(\n path: string,\n params?: Record<string, string>,\n): Promise<{ data: T; headers: Headers }> {\n const env = getEnv();\n const url = new URL(path, env.BAKER_API_URL);\n if (params) {\n const clean = sanitizeParams(params);\n for (const [key, value] of Object.entries(clean)) {\n url.searchParams.set(key, value);\n }\n }\n\n const urlStr = url.toString();\n const startedAt = Date.now();\n let response: Response;\n try {\n response = await fetchWithRateLimitRetry(urlStr, {\n method: \"GET\",\n headers: {\n Authorization: `Bearer ${env.BAKER_API_KEY}`,\n Accept: \"application/json\",\n ...chatHeader(),\n },\n signal: AbortSignal.timeout(60_000),\n });\n } catch (err) {\n debugLogHttp({ source: \"cli\", method: \"GET\", url: urlStr, durationMs: Date.now() - startedAt, error: err });\n if (err instanceof Error && (err.name === \"TimeoutError\" || err.name === \"AbortError\")) {\n throw new ApiError(\"TIMEOUT\", \"Request timed out after 60 seconds\");\n }\n throw new ApiError(\"NETWORK_ERROR\", `Request failed: ${err instanceof Error ? err.message : \"Unknown error\"}`);\n }\n\n debugLogHttp({\n source: \"cli\",\n method: \"GET\",\n url: urlStr,\n status: response.status,\n ok: response.ok,\n responseBody: await readBodyForLog(response),\n durationMs: Date.now() - startedAt,\n });\n return { data: await handleResponse<T>(response), headers: response.headers };\n}\n\nexport async function apiGet<T>(path: string, params?: Record<string, string>): Promise<T> {\n return (await apiGetWithHeaders<T>(path, params)).data;\n}\n\nexport async function apiPost<T>(path: string, body: unknown, opts?: { timeoutMs?: number }): Promise<T> {\n const env = getEnv();\n const timeoutMs = opts?.timeoutMs ?? 60_000;\n const urlStr = new URL(path, env.BAKER_API_URL).toString();\n const startedAt = Date.now();\n let response: Response;\n try {\n response = await fetchWithRateLimitRetry(urlStr, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${env.BAKER_API_KEY}`,\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n ...chatHeader(),\n },\n body: JSON.stringify(body),\n signal: AbortSignal.timeout(timeoutMs),\n });\n } catch (err) {\n debugLogHttp({\n source: \"cli\",\n method: \"POST\",\n url: urlStr,\n requestBody: body,\n durationMs: Date.now() - startedAt,\n error: err,\n });\n if (err instanceof Error && (err.name === \"TimeoutError\" || err.name === \"AbortError\")) {\n throw new ApiError(\"TIMEOUT\", `Request timed out after ${Math.round(timeoutMs / 1000)} seconds`);\n }\n throw new ApiError(\"NETWORK_ERROR\", `Request failed: ${err instanceof Error ? err.message : \"Unknown error\"}`);\n }\n\n debugLogHttp({\n source: \"cli\",\n method: \"POST\",\n url: urlStr,\n requestBody: body,\n status: response.status,\n ok: response.ok,\n responseBody: await readBodyForLog(response),\n durationMs: Date.now() - startedAt,\n });\n return handleResponse<T>(response);\n}\n"],"mappings":"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","names":[]}
@@ -2,10 +2,10 @@ import {
2
2
  handleConnectionError,
3
3
  needsConnectionFix,
4
4
  writeAdsJson
5
- } from "./chunk-2IC3CGQO.js";
5
+ } from "./chunk-VVQLRFCI.js";
6
6
  import {
7
7
  ApiError
8
- } from "./chunk-4QXKWIFC.js";
8
+ } from "./chunk-MZQC4LII.js";
9
9
  import {
10
10
  getEnv
11
11
  } from "./chunk-2OKSJNLA.js";
@@ -35,7 +35,7 @@ function handleMetaError(err) {
35
35
  code: err.code,
36
36
  message: err.message,
37
37
  fix: err.code === "FORBIDDEN" ? metaForbiddenFix(err.message) : { action: "reject", explanation: err.message },
38
- retryable: err.code === "RATE_LIMITED" || err.code === "INTERNAL_ERROR"
38
+ retryable: err.code === "RATE_LIMITED" || err.code === "UNAVAILABLE" || err.code === "INTERNAL_ERROR"
39
39
  }
40
40
  };
41
41
  writeAdsJson(envelope);
@@ -108,4 +108,4 @@ export {
108
108
  csvOrJson,
109
109
  resolveEffectiveStatus
110
110
  };
111
- //# sourceMappingURL=chunk-NA5CPMUA.js.map
111
+ //# sourceMappingURL=chunk-SR56QQTD.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/ads/meta/shared.ts"],"sourcesContent":["import { ApiError } from \"../../../client.ts\";\nimport { getEnv } from \"../../../env.ts\";\nimport { handleConnectionError, needsConnectionFix } from \"../../../error-handler.ts\";\nimport { writeAdsJson } from \"../output.ts\";\nimport type { AdsErrorEnvelope, QueryFix } from \"../types.ts\";\n\nconst DAY_MS = 86_400_000;\n\n/**\n * Meta answers a permission refusal (Graph codes 200/10/294) with a bare\n * \"Permissions error\" that covers two causes needing opposite remedies: a\n * consent grant the connection never got — nothing validates Meta's granted\n * scopes after connect, unlike HubSpot's `requireCapability` — or a role the\n * user lacks on one asset, since a Pixel/Dataset, Page or catalog lives in a\n * Business and is assigned separately from the ad account. Only the blast radius\n * separates them, so hand the agent that test rather than a verdict: guessing\n * \"reconnect\" wastes the user's time on an unassigned asset, and ruling it out\n * strands a connection that really is short a scope.\n */\nexport function metaForbiddenFix(message: string): QueryFix {\n return {\n action: \"reject\",\n explanation: [\n message,\n \"This is a permission refusal, not a dead connection, and Meta returns the same code for two causes with opposite remedies. Tell them apart by how much else works before you answer.\",\n \"If other reads on this account are ALSO refused (campaigns, ad sets, insights), the connection is short a permission it was never granted. Only the user can widen it, by reconnecting Meta from the dashboard.\",\n \"If those reads work and only this object is refused, the asset (pixel/dataset, Page, or catalog) belongs to a Business the connected user holds no role in. Ad-account access does not carry asset access and reconnecting cannot grant it — the client assigns that asset to the connected user or partner in Meta Business settings, under the asset's own section (Data sources → Datasets for a pixel).\",\n \"Either way do not retry the same call, and do not abort the wider job. Continue with what you can read, and state in your answer that this specific check could not run and why — never present the task as verified when this step was skipped.\",\n ].join(\"\\n\\n\"),\n };\n}\n\nexport function handleMetaError(err: unknown): never {\n if (err instanceof ApiError) {\n if (needsConnectionFix(err.code, err.message)) {\n handleConnectionError(\"meta_ads\", err.message);\n }\n const envelope: AdsErrorEnvelope = {\n ok: false,\n error: {\n code: err.code,\n message: err.message,\n fix: err.code === \"FORBIDDEN\" ? metaForbiddenFix(err.message) : { action: \"reject\", explanation: err.message },\n retryable: err.code === \"RATE_LIMITED\" || err.code === \"UNAVAILABLE\" || err.code === \"INTERNAL_ERROR\",\n },\n };\n writeAdsJson(envelope);\n process.exit(1);\n }\n const message = err instanceof Error ? err.message : \"Unexpected error\";\n writeAdsJson({ ok: false, error: { code: \"NETWORK_ERROR\", message } });\n process.exit(1);\n}\n\nconst ACCOUNT_ID_RE = /^(act_)?[0-9]+$/;\n\n/** Pull the default Meta ad account ID from `--account-id` arg or `BAKER_META_AD_ACCOUNT_ID` env. */\nexport function resolveAccountIdArg(args: Record<string, unknown>): string {\n const fromArgs = args[\"account-id\"] as string | undefined;\n const id = fromArgs || getEnv().BAKER_META_AD_ACCOUNT_ID;\n if (!id) {\n writeAdsJson({\n ok: false,\n error: {\n code: \"MISSING_ACCOUNT_ID\",\n message: \"Pass --account-id or set BAKER_META_AD_ACCOUNT_ID. Run `baker ads meta accounts` to find IDs.\",\n },\n });\n process.exit(1);\n }\n if (!ACCOUNT_ID_RE.test(id)) {\n writeAdsJson({\n ok: false,\n error: {\n code: \"INVALID_ACCOUNT_ID\",\n message: `Invalid account ID \"${id}\". Expected numeric ID, optionally prefixed with \"act_\".`,\n },\n });\n process.exit(1);\n }\n return id.startsWith(\"act_\") ? id : `act_${id}`;\n}\n\nexport function getDateRange(args: Record<string, unknown>): { datePreset?: string; since?: string; until?: string } {\n const datePreset = args[\"date-preset\"] as string | undefined;\n const since = args.since as string | undefined;\n const until = args.until as string | undefined;\n if (since && until) return { since, until };\n return { datePreset: datePreset ?? \"last_7d\" };\n}\n\nexport function todayIso(): string {\n return new Date().toISOString().slice(0, 10);\n}\n\nexport function daysAgoIso(days: number): string {\n return new Date(Date.now() - days * DAY_MS).toISOString().slice(0, 10);\n}\n\nexport function csvOrJson(args: Record<string, unknown>): string {\n return (args.output as string | undefined) ?? \"json\";\n}\n\n/**\n * Default to ACTIVE-only on listings — that's almost always what an AI agent or\n * a human glance wants. Pass `--effective-status ACTIVE,PAUSED` to override, or\n * `--all-statuses` to drop the filter entirely.\n */\nexport function resolveEffectiveStatus(args: Record<string, unknown>): string | undefined {\n if (args[\"all-statuses\"]) {\n return undefined;\n }\n const explicit = args[\"effective-status\"] as string | undefined;\n if (explicit) {\n return explicit;\n }\n return \"ACTIVE\";\n}\n"],"mappings":";;;;;;;;;;;;;AAMA,IAAM,SAAS;AAaR,SAAS,iBAAiB,SAA2B;AAC1D,SAAO;AAAA,IACL,QAAQ;AAAA,IACR,aAAa;AAAA,MACX;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF,EAAE,KAAK,MAAM;AAAA,EACf;AACF;AAEO,SAAS,gBAAgB,KAAqB;AACnD,MAAI,eAAe,UAAU;AAC3B,QAAI,mBAAmB,IAAI,MAAM,IAAI,OAAO,GAAG;AAC7C,4BAAsB,YAAY,IAAI,OAAO;AAAA,IAC/C;AACA,UAAM,WAA6B;AAAA,MACjC,IAAI;AAAA,MACJ,OAAO;AAAA,QACL,MAAM,IAAI;AAAA,QACV,SAAS,IAAI;AAAA,QACb,KAAK,IAAI,SAAS,cAAc,iBAAiB,IAAI,OAAO,IAAI,EAAE,QAAQ,UAAU,aAAa,IAAI,QAAQ;AAAA,QAC7G,WAAW,IAAI,SAAS,kBAAkB,IAAI,SAAS,iBAAiB,IAAI,SAAS;AAAA,MACvF;AAAA,IACF;AACA,iBAAa,QAAQ;AACrB,YAAQ,KAAK,CAAC;AAAA,EAChB;AACA,QAAM,UAAU,eAAe,QAAQ,IAAI,UAAU;AACrD,eAAa,EAAE,IAAI,OAAO,OAAO,EAAE,MAAM,iBAAiB,QAAQ,EAAE,CAAC;AACrE,UAAQ,KAAK,CAAC;AAChB;AAEA,IAAM,gBAAgB;AAGf,SAAS,oBAAoB,MAAuC;AACzE,QAAM,WAAW,KAAK,YAAY;AAClC,QAAM,KAAK,YAAY,OAAO,EAAE;AAChC,MAAI,CAAC,IAAI;AACP,iBAAa;AAAA,MACX,IAAI;AAAA,MACJ,OAAO;AAAA,QACL,MAAM;AAAA,QACN,SAAS;AAAA,MACX;AAAA,IACF,CAAC;AACD,YAAQ,KAAK,CAAC;AAAA,EAChB;AACA,MAAI,CAAC,cAAc,KAAK,EAAE,GAAG;AAC3B,iBAAa;AAAA,MACX,IAAI;AAAA,MACJ,OAAO;AAAA,QACL,MAAM;AAAA,QACN,SAAS,uBAAuB,EAAE;AAAA,MACpC;AAAA,IACF,CAAC;AACD,YAAQ,KAAK,CAAC;AAAA,EAChB;AACA,SAAO,GAAG,WAAW,MAAM,IAAI,KAAK,OAAO,EAAE;AAC/C;AAEO,SAAS,aAAa,MAAwF;AACnH,QAAM,aAAa,KAAK,aAAa;AACrC,QAAM,QAAQ,KAAK;AACnB,QAAM,QAAQ,KAAK;AACnB,MAAI,SAAS,MAAO,QAAO,EAAE,OAAO,MAAM;AAC1C,SAAO,EAAE,YAAY,cAAc,UAAU;AAC/C;AAEO,SAAS,WAAmB;AACjC,UAAO,oBAAI,KAAK,GAAE,YAAY,EAAE,MAAM,GAAG,EAAE;AAC7C;AAEO,SAAS,WAAW,MAAsB;AAC/C,SAAO,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,EAAE;AACvE;AAEO,SAAS,UAAU,MAAuC;AAC/D,SAAQ,KAAK,UAAiC;AAChD;AAOO,SAAS,uBAAuB,MAAmD;AACxF,MAAI,KAAK,cAAc,GAAG;AACxB,WAAO;AAAA,EACT;AACA,QAAM,WAAW,KAAK,kBAAkB;AACxC,MAAI,UAAU;AACZ,WAAO;AAAA,EACT;AACA,SAAO;AACT;","names":[]}
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  ApiError,
3
3
  apiGet
4
- } from "./chunk-4QXKWIFC.js";
4
+ } from "./chunk-MZQC4LII.js";
5
5
  import {
6
6
  getEnv
7
7
  } from "./chunk-2OKSJNLA.js";
@@ -294,4 +294,4 @@ export {
294
294
  writeAdsOutput,
295
295
  resolveCustomerId
296
296
  };
297
- //# sourceMappingURL=chunk-2IC3CGQO.js.map
297
+ //# sourceMappingURL=chunk-VVQLRFCI.js.map
package/dist/cli.js CHANGED
@@ -69,7 +69,7 @@ import {
69
69
  validateCanvasDeep,
70
70
  withoutCastName,
71
71
  ytDlpBlockSignal
72
- } from "./chunk-K2IKLMCM.js";
72
+ } from "./chunk-AI4HWQVN.js";
73
73
  import {
74
74
  csvOrJson,
75
75
  daysAgoIso,
@@ -78,7 +78,7 @@ import {
78
78
  resolveAccountIdArg,
79
79
  resolveEffectiveStatus,
80
80
  todayIso
81
- } from "./chunk-NA5CPMUA.js";
81
+ } from "./chunk-SR56QQTD.js";
82
82
  import {
83
83
  buildQueryCacheKey,
84
84
  cacheGet,
@@ -94,14 +94,14 @@ import {
94
94
  writeAdsJson,
95
95
  writeAdsOutput,
96
96
  writeJsonEnvelope
97
- } from "./chunk-2IC3CGQO.js";
97
+ } from "./chunk-VVQLRFCI.js";
98
98
  import {
99
99
  ApiError,
100
100
  apiGet,
101
101
  apiGetWithHeaders,
102
102
  apiPost,
103
103
  validateConvexId
104
- } from "./chunk-4QXKWIFC.js";
104
+ } from "./chunk-MZQC4LII.js";
105
105
  import {
106
106
  installExitRecord,
107
107
  installStreamTaps,
@@ -8052,7 +8052,6 @@ var testimonialDocSchema = z28.object({
8052
8052
  sourceType: testimonialSourceTypeSchema,
8053
8053
  reviewText: z28.string(),
8054
8054
  reviewTitle: z28.string().optional(),
8055
- searchText: z28.string().optional(),
8056
8055
  reviewerName: z28.string().optional(),
8057
8056
  reviewerImageUrl: z28.string().optional(),
8058
8057
  reviewerImageId: z28.string().optional(),
@@ -8064,13 +8063,11 @@ var testimonialDocSchema = z28.object({
8064
8063
  imageIds: z28.array(z28.string()).optional(),
8065
8064
  videoIds: z28.array(z28.string()).optional(),
8066
8065
  sourceUrl: z28.string().optional(),
8067
- rawData: z28.unknown().optional(),
8068
8066
  tags: z28.array(z28.string()),
8069
8067
  highlight: z28.string().optional(),
8070
8068
  language: z28.string().optional(),
8071
8069
  summary: z28.string().optional(),
8072
8070
  sentiment: testimonialSentimentSchema.optional(),
8073
- textEmbedding: z28.array(z28.number()).optional(),
8074
8071
  externalId: z28.string().optional(),
8075
8072
  contentHash: z28.string().optional(),
8076
8073
  status: testimonialStatusSchema,
@@ -19821,7 +19818,7 @@ function handleLinkedinError(err) {
19821
19818
  explanation: explanation.message,
19822
19819
  correctedCommand: explanation.correctedCommand
19823
19820
  },
19824
- retryable: err.code === "RATE_LIMITED" || err.code === "INTERNAL_ERROR"
19821
+ retryable: err.code === "RATE_LIMITED" || err.code === "UNAVAILABLE" || err.code === "INTERNAL_ERROR"
19825
19822
  }
19826
19823
  };
19827
19824
  writeAdsJson(envelope);
@@ -26360,10 +26357,10 @@ Example: baker ads meta ads retag 120252158038400247 --url-tags "utm_source={{si
26360
26357
  }
26361
26358
  },
26362
26359
  run: async ({ args: args5 }) => {
26363
- const { apiPost: apiPost2 } = await import("./client-ODCNYCP4.js");
26360
+ const { apiPost: apiPost2 } = await import("./client-3DGBQ2H3.js");
26364
26361
  const { requireChatId: requireChatId2 } = await import("./env-BSG7TY5Y.js");
26365
- const { writeJsonEnvelope: writeJsonEnvelope2 } = await import("./output-2XBCYF7L.js");
26366
- const { handleMetaError: handleMetaError2 } = await import("./shared-RYAF7Y7H.js");
26362
+ const { writeJsonEnvelope: writeJsonEnvelope2 } = await import("./output-OSSBUGT7.js");
26363
+ const { handleMetaError: handleMetaError2 } = await import("./shared-MSI4UMZ3.js");
26367
26364
  try {
26368
26365
  const accountId = bareAccountId2(args5);
26369
26366
  const urlTags = typeof args5["url-tags"] === "string" ? args5["url-tags"] : void 0;
@@ -26508,10 +26505,10 @@ function duplicateCommand2(entity, label) {
26508
26505
  replace: { type: "boolean", description: "Pause the original once the copy publishes" }
26509
26506
  },
26510
26507
  run: async ({ args: args5 }) => {
26511
- const { apiPost: apiPost2 } = await import("./client-ODCNYCP4.js");
26508
+ const { apiPost: apiPost2 } = await import("./client-3DGBQ2H3.js");
26512
26509
  const { requireChatId: requireChatId2 } = await import("./env-BSG7TY5Y.js");
26513
- const { writeJsonEnvelope: writeJsonEnvelope2 } = await import("./output-2XBCYF7L.js");
26514
- const { handleMetaError: handleMetaError2 } = await import("./shared-RYAF7Y7H.js");
26510
+ const { writeJsonEnvelope: writeJsonEnvelope2 } = await import("./output-OSSBUGT7.js");
26511
+ const { handleMetaError: handleMetaError2 } = await import("./shared-MSI4UMZ3.js");
26515
26512
  try {
26516
26513
  const accountId = bareAccountId2(args5);
26517
26514
  const chatId = requireChatId2();
@@ -36644,134 +36641,9 @@ var critiqueCommand = defineCommand109({
36644
36641
  // src/commands/canvas/inspect.ts
36645
36642
  import { execFile } from "child_process";
36646
36643
  import { readdir as readdir2, readFile as readFile4, stat } from "fs/promises";
36647
- import path6 from "path";
36644
+ import path4 from "path";
36648
36645
  import { promisify } from "util";
36649
36646
  import { defineCommand as defineCommand110 } from "citty";
36650
-
36651
- // src/commands/canvas/normalize-paths.ts
36652
- import { existsSync as existsSync4, realpathSync } from "fs";
36653
- import { writeFile as writeFile2 } from "fs/promises";
36654
- import path5 from "path";
36655
-
36656
- // src/commands/canvas/resolve-paths.ts
36657
- import path4 from "path";
36658
- function resolveRelativeCanvasPaths(canvas, baseDir) {
36659
- if (!canvas || typeof canvas !== "object") return canvas;
36660
- const c = canvas;
36661
- if (!Array.isArray(c.nodes)) return canvas;
36662
- return { ...canvas, nodes: c.nodes.map((n) => resolveNode(n, baseDir)) };
36663
- }
36664
- function localPathParam(node) {
36665
- if (node.type === "ingest") return node.params?.source === "path" ? "path" : null;
36666
- if (node.type === "hyperframe_render" || node.type === "hyperframe_snapshot") return "composition";
36667
- return null;
36668
- }
36669
- function resolveNode(node, baseDir) {
36670
- if (!node || typeof node !== "object") return node;
36671
- const n = node;
36672
- const params = n.params;
36673
- if (!params || typeof params !== "object") return node;
36674
- const key = localPathParam(n);
36675
- if (key && isResolvableRelative(params[key])) {
36676
- return { ...node, params: { ...params, [key]: path4.resolve(baseDir, params[key]) } };
36677
- }
36678
- return node;
36679
- }
36680
- function isResolvableRelative(value) {
36681
- return typeof value === "string" && value.length > 0 && !value.includes("[TODO") && !looksLikeHttpUrl(value) && !path4.isAbsolute(value);
36682
- }
36683
-
36684
- // src/commands/canvas/normalize-paths.ts
36685
- function findWorkspaceRoot(startDir, exists2 = existsSync4, maxDepth = 12) {
36686
- let dir = path5.resolve(startDir);
36687
- for (let i = 0; i < maxDepth; i++) {
36688
- if (exists2(path5.join(dir, "package.json"))) return dir;
36689
- const parent = path5.dirname(dir);
36690
- if (parent === dir) break;
36691
- dir = parent;
36692
- }
36693
- return null;
36694
- }
36695
- function canvasOutputsRoot(canvasFile, cwd = process.cwd(), exists2 = existsSync4) {
36696
- const root = findWorkspaceRoot(path5.dirname(path5.resolve(cwd, canvasFile)), exists2) ?? cwd;
36697
- return path5.join(root, "canvas");
36698
- }
36699
- function canonicalize(target2) {
36700
- const abs = path5.resolve(target2);
36701
- let dir = abs;
36702
- for (; ; ) {
36703
- try {
36704
- const real = realpathSync(dir);
36705
- return dir === abs ? real : path5.join(real, path5.relative(dir, abs));
36706
- } catch {
36707
- const parent = path5.dirname(dir);
36708
- if (parent === dir) return abs;
36709
- dir = parent;
36710
- }
36711
- }
36712
- }
36713
- function isInside(root, target2) {
36714
- const rel = path5.relative(root, target2);
36715
- return rel !== "" && !rel.startsWith("..") && !path5.isAbsolute(rel);
36716
- }
36717
- function toCanvasRelative(canvasDir, target2) {
36718
- return path5.relative(canvasDir, target2).split(path5.sep).join("/");
36719
- }
36720
- var RUNTIME_WORKSPACE_ROOT = "/home/user/repo";
36721
- function rewriteTarget(value, workspaceRoot) {
36722
- if (typeof value !== "string" || value.length === 0) return null;
36723
- if (value.includes("[TODO") || looksLikeHttpUrl(value) || !path5.isAbsolute(value)) return null;
36724
- const target2 = canonicalize(value);
36725
- if (isInside(workspaceRoot, target2)) return target2;
36726
- const fromRuntime = path5.relative(RUNTIME_WORKSPACE_ROOT, path5.normalize(value));
36727
- if (fromRuntime !== "" && !fromRuntime.startsWith("..") && !path5.isAbsolute(fromRuntime)) {
36728
- return path5.join(workspaceRoot, fromRuntime);
36729
- }
36730
- return null;
36731
- }
36732
- function normalizeCanvasPaths(canvas, canvasDir, workspaceRoot) {
36733
- if (!canvas || typeof canvas !== "object") return { canvas, rewrites: [] };
36734
- const c = canvas;
36735
- if (!Array.isArray(c.nodes)) return { canvas, rewrites: [] };
36736
- const root = canonicalize(workspaceRoot);
36737
- const dir = canonicalize(canvasDir);
36738
- const rewrites = [];
36739
- const nodes = c.nodes.map((node) => {
36740
- if (!node || typeof node !== "object") return node;
36741
- const n = node;
36742
- const params = n.params;
36743
- if (!params || typeof params !== "object") return node;
36744
- const nodeId = typeof n.id === "string" ? n.id : "?";
36745
- if (n.type === "ingest" && params.source === "path" && looksLikeHttpUrl(params.path)) {
36746
- rewrites.push({ nodeId, param: "source", from: "path", to: "url" });
36747
- return { ...node, params: coercePathUrlToUrl(params) };
36748
- }
36749
- const key = localPathParam(n);
36750
- if (!key) return node;
36751
- const target2 = rewriteTarget(params[key], root);
36752
- if (target2 === null) return node;
36753
- const to = toCanvasRelative(dir, target2);
36754
- rewrites.push({ nodeId, param: key, from: params[key], to });
36755
- return { ...node, params: { ...params, [key]: to } };
36756
- });
36757
- return rewrites.length === 0 ? { canvas, rewrites } : { canvas: { ...canvas, nodes }, rewrites };
36758
- }
36759
- async function healCanvasPaths(filePath, canvas) {
36760
- const canvasDir = path5.dirname(filePath);
36761
- const workspaceRoot = findWorkspaceRoot(canvasDir);
36762
- if (!workspaceRoot) return { canvas, rewrites: [], text: null };
36763
- const normalized = normalizeCanvasPaths(canvas, canvasDir, workspaceRoot);
36764
- if (normalized.rewrites.length === 0) return { ...normalized, text: null };
36765
- const text2 = `${JSON.stringify(normalized.canvas, null, 2)}
36766
- `;
36767
- await writeFile2(filePath, text2, "utf8");
36768
- return { ...normalized, text: text2 };
36769
- }
36770
- function describeRewrites(rewrites) {
36771
- return rewrites.map((r) => ` ${r.nodeId}.${r.param}: ${r.from} \u2192 ${r.to}`).join("\n");
36772
- }
36773
-
36774
- // src/commands/canvas/inspect.ts
36775
36647
  var execFileAsync = promisify(execFile);
36776
36648
  var inspectCommand = defineCommand110({
36777
36649
  meta: {
@@ -36780,15 +36652,14 @@ var inspectCommand = defineCommand110({
36780
36652
  },
36781
36653
  args: {
36782
36654
  run: { type: "positional", required: true, description: "Run id (e.g. r_01K...) or absolute run directory" },
36783
- "outputs-dir": { type: "string", description: "Per-run outputs root (default <workspace root>/canvas, where `canvas run` writes)" },
36655
+ "outputs-dir": { type: "string", description: "Per-run outputs root (default ./canvas)" },
36784
36656
  thumbnails: {
36785
36657
  type: "boolean",
36786
36658
  description: "Extract start/middle/end frames from every video output (requires ffmpeg)"
36787
36659
  }
36788
36660
  },
36789
36661
  async run({ args: args5 }) {
36790
- const cwd = process.cwd();
36791
- const outputsDir = args5["outputs-dir"] ? path6.resolve(String(args5["outputs-dir"])) : path6.join(findWorkspaceRoot(cwd) ?? cwd, "canvas");
36662
+ const outputsDir = path4.resolve(String(args5["outputs-dir"] ?? "canvas"));
36792
36663
  const runArg = String(args5.run);
36793
36664
  const runDir = await resolveRunDir(runArg, outputsDir);
36794
36665
  const manifest = await loadManifest(runDir);
@@ -36800,7 +36671,7 @@ var inspectCommand = defineCommand110({
36800
36671
  }
36801
36672
  const summary = {
36802
36673
  ok: true,
36803
- run_id: manifest.run_id ?? path6.basename(runDir),
36674
+ run_id: manifest.run_id ?? path4.basename(runDir),
36804
36675
  run_dir: runDir,
36805
36676
  stats: manifest.stats ?? null,
36806
36677
  output: manifest.output ?? null,
@@ -36813,18 +36684,18 @@ var inspectCommand = defineCommand110({
36813
36684
  }
36814
36685
  });
36815
36686
  async function resolveRunDir(run, outputsDir) {
36816
- if (path6.isAbsolute(run)) {
36687
+ if (path4.isAbsolute(run)) {
36817
36688
  const s2 = await stat(run).catch(() => null);
36818
36689
  if (s2?.isDirectory()) return run;
36819
36690
  throw new Error(`inspect: ${run} is not a directory`);
36820
36691
  }
36821
- const candidate = path6.join(outputsDir, run);
36692
+ const candidate = path4.join(outputsDir, run);
36822
36693
  const s = await stat(candidate).catch(() => null);
36823
36694
  if (s?.isDirectory()) return candidate;
36824
36695
  throw new Error(`inspect: no run directory at ${candidate}`);
36825
36696
  }
36826
36697
  async function loadManifest(runDir) {
36827
- const manifestPath = path6.join(runDir, "manifest.json");
36698
+ const manifestPath = path4.join(runDir, "manifest.json");
36828
36699
  try {
36829
36700
  const raw = await readFile4(manifestPath, "utf-8");
36830
36701
  return JSON.parse(raw);
@@ -36836,7 +36707,7 @@ async function listRunFiles(runDir) {
36836
36707
  const out = [];
36837
36708
  const names = await readdir2(runDir);
36838
36709
  for (const name of names) {
36839
- const abs = path6.join(runDir, name);
36710
+ const abs = path4.join(runDir, name);
36840
36711
  const s = await stat(abs).catch(() => null);
36841
36712
  if (!s?.isFile()) continue;
36842
36713
  out.push({ name, path: abs, size: s.size });
@@ -36889,6 +36760,95 @@ import { readFile as readFile12 } from "fs/promises";
36889
36760
  import path16 from "path";
36890
36761
  import { defineCommand as defineCommand111 } from "citty";
36891
36762
 
36763
+ // src/commands/canvas/normalize-paths.ts
36764
+ import { existsSync as existsSync4, realpathSync } from "fs";
36765
+ import { writeFile as writeFile2 } from "fs/promises";
36766
+ import path5 from "path";
36767
+ function findWorkspaceRoot(startDir, exists2 = existsSync4, maxDepth = 12) {
36768
+ let dir = path5.resolve(startDir);
36769
+ for (let i = 0; i < maxDepth; i++) {
36770
+ if (exists2(path5.join(dir, "package.json"))) return dir;
36771
+ const parent = path5.dirname(dir);
36772
+ if (parent === dir) break;
36773
+ dir = parent;
36774
+ }
36775
+ return null;
36776
+ }
36777
+ function canonicalize(target2) {
36778
+ const abs = path5.resolve(target2);
36779
+ let dir = abs;
36780
+ for (; ; ) {
36781
+ try {
36782
+ const real = realpathSync(dir);
36783
+ return dir === abs ? real : path5.join(real, path5.relative(dir, abs));
36784
+ } catch {
36785
+ const parent = path5.dirname(dir);
36786
+ if (parent === dir) return abs;
36787
+ dir = parent;
36788
+ }
36789
+ }
36790
+ }
36791
+ function isInside(root, target2) {
36792
+ const rel = path5.relative(root, target2);
36793
+ return rel !== "" && !rel.startsWith("..") && !path5.isAbsolute(rel);
36794
+ }
36795
+ function toCanvasRelative(canvasDir, target2) {
36796
+ return path5.relative(canvasDir, target2).split(path5.sep).join("/");
36797
+ }
36798
+ var RUNTIME_WORKSPACE_ROOT = "/home/user/repo";
36799
+ function rewriteTarget(value, workspaceRoot) {
36800
+ if (typeof value !== "string" || value.length === 0) return null;
36801
+ if (value.includes("[TODO") || looksLikeHttpUrl(value) || !path5.isAbsolute(value)) return null;
36802
+ const target2 = canonicalize(value);
36803
+ if (isInside(workspaceRoot, target2)) return target2;
36804
+ const fromRuntime = path5.relative(RUNTIME_WORKSPACE_ROOT, path5.normalize(value));
36805
+ if (fromRuntime !== "" && !fromRuntime.startsWith("..") && !path5.isAbsolute(fromRuntime)) {
36806
+ return path5.join(workspaceRoot, fromRuntime);
36807
+ }
36808
+ return null;
36809
+ }
36810
+ function normalizeCanvasPaths(canvas, canvasDir, workspaceRoot) {
36811
+ if (!canvas || typeof canvas !== "object") return { canvas, rewrites: [] };
36812
+ const c = canvas;
36813
+ if (!Array.isArray(c.nodes)) return { canvas, rewrites: [] };
36814
+ const root = canonicalize(workspaceRoot);
36815
+ const dir = canonicalize(canvasDir);
36816
+ const rewrites = [];
36817
+ const nodes = c.nodes.map((node) => {
36818
+ if (!node || typeof node !== "object") return node;
36819
+ const n = node;
36820
+ const params = n.params;
36821
+ if (!params || typeof params !== "object") return node;
36822
+ const nodeId = typeof n.id === "string" ? n.id : "?";
36823
+ if (n.type === "ingest" && params.source === "path" && looksLikeHttpUrl(params.path)) {
36824
+ rewrites.push({ nodeId, param: "source", from: "path", to: "url" });
36825
+ return { ...node, params: coercePathUrlToUrl(params) };
36826
+ }
36827
+ const key = n.type === "ingest" && params.source === "path" ? "path" : n.type === "hyperframe_render" ? "composition" : null;
36828
+ if (!key) return node;
36829
+ const target2 = rewriteTarget(params[key], root);
36830
+ if (target2 === null) return node;
36831
+ const to = toCanvasRelative(dir, target2);
36832
+ rewrites.push({ nodeId, param: key, from: params[key], to });
36833
+ return { ...node, params: { ...params, [key]: to } };
36834
+ });
36835
+ return rewrites.length === 0 ? { canvas, rewrites } : { canvas: { ...canvas, nodes }, rewrites };
36836
+ }
36837
+ async function healCanvasPaths(filePath, canvas) {
36838
+ const canvasDir = path5.dirname(filePath);
36839
+ const workspaceRoot = findWorkspaceRoot(canvasDir);
36840
+ if (!workspaceRoot) return { canvas, rewrites: [], text: null };
36841
+ const normalized = normalizeCanvasPaths(canvas, canvasDir, workspaceRoot);
36842
+ if (normalized.rewrites.length === 0) return { ...normalized, text: null };
36843
+ const text2 = `${JSON.stringify(normalized.canvas, null, 2)}
36844
+ `;
36845
+ await writeFile2(filePath, text2, "utf8");
36846
+ return { ...normalized, text: text2 };
36847
+ }
36848
+ function describeRewrites(rewrites) {
36849
+ return rewrites.map((r) => ` ${r.nodeId}.${r.param}: ${r.from} \u2192 ${r.to}`).join("\n");
36850
+ }
36851
+
36892
36852
  // src/commands/canvas/placeholders.ts
36893
36853
  function unsuppliedPlaceholderAssets(canvas) {
36894
36854
  const nodes = canvas?.nodes;
@@ -36905,6 +36865,31 @@ function unsuppliedPlaceholderAssets(canvas) {
36905
36865
  return out;
36906
36866
  }
36907
36867
 
36868
+ // src/commands/canvas/resolve-paths.ts
36869
+ import path6 from "path";
36870
+ function resolveRelativeCanvasPaths(canvas, baseDir) {
36871
+ if (!canvas || typeof canvas !== "object") return canvas;
36872
+ const c = canvas;
36873
+ if (!Array.isArray(c.nodes)) return canvas;
36874
+ return { ...canvas, nodes: c.nodes.map((n) => resolveNode(n, baseDir)) };
36875
+ }
36876
+ function resolveNode(node, baseDir) {
36877
+ if (!node || typeof node !== "object") return node;
36878
+ const n = node;
36879
+ const params = n.params;
36880
+ if (!params || typeof params !== "object") return node;
36881
+ if (n.type === "ingest" && params.source === "path" && isResolvableRelative(params.path)) {
36882
+ return { ...node, params: { ...params, path: path6.resolve(baseDir, params.path) } };
36883
+ }
36884
+ if (n.type === "hyperframe_render" && isResolvableRelative(params.composition)) {
36885
+ return { ...node, params: { ...params, composition: path6.resolve(baseDir, params.composition) } };
36886
+ }
36887
+ return node;
36888
+ }
36889
+ function isResolvableRelative(value) {
36890
+ return typeof value === "string" && value.length > 0 && !value.includes("[TODO") && !looksLikeHttpUrl(value) && !path6.isAbsolute(value);
36891
+ }
36892
+
36908
36893
  // src/commands/canvas/source-version.ts
36909
36894
  import { readFile as readFile6 } from "fs/promises";
36910
36895
  import path8 from "path";
@@ -41014,9 +40999,12 @@ function localPathRefsFromCanvas(parsed) {
41014
40999
  const n = node;
41015
41000
  const params = n.params;
41016
41001
  if (!params || typeof params !== "object") continue;
41017
- const key = localPathParam(n);
41018
- const value = key ? params[key] : void 0;
41019
- if (isSnapshotablePath(value)) refs.add(value);
41002
+ if (n.type === "ingest" && params.source === "path" && isSnapshotablePath(params.path)) {
41003
+ refs.add(params.path);
41004
+ }
41005
+ if (n.type === "hyperframe_render" && isSnapshotablePath(params.composition)) {
41006
+ refs.add(params.composition);
41007
+ }
41020
41008
  }
41021
41009
  return [...refs];
41022
41010
  }
@@ -41701,8 +41689,8 @@ var runCommand = defineCommand111({
41701
41689
  type: "string",
41702
41690
  description: "What the person asked for, IN THEIR OWN WORDS \u2014 paste their message, do not summarise it. The review compares the finished video against this and reports what they asked for that is missing or contradicted. Without it the review can only say whether the video is well made, not whether it is the one they wanted: every other check grades the render against the spec, and a spec that misread the request agrees with itself."
41703
41691
  },
41704
- "cache-dir": { type: "string", description: "Cache root (default <workspace root>/canvas/.cache, whatever directory you run from)" },
41705
- "outputs-dir": { type: "string", description: "Per-run outputs root (default <workspace root>/canvas, whatever directory you run from)" },
41692
+ "cache-dir": { type: "string", description: "Cache root (default ./canvas/.cache)" },
41693
+ "outputs-dir": { type: "string", description: "Per-run outputs root (default ./canvas)" },
41706
41694
  "run-id": { type: "string", description: "Override run id (also resumes that run, re-attaching its in-flight jobs)" },
41707
41695
  fresh: {
41708
41696
  type: "boolean",
@@ -41780,7 +41768,6 @@ function stopBeforeKinds(until) {
41780
41768
  }
41781
41769
  async function executeCanvasRun(opts) {
41782
41770
  const filePath = path16.resolve(opts.file);
41783
- const canvasRoot = canvasOutputsRoot(filePath);
41784
41771
  const raw = await readFile12(filePath, "utf8");
41785
41772
  let parsed;
41786
41773
  try {
@@ -41792,7 +41779,7 @@ async function executeCanvasRun(opts) {
41792
41779
  process.exit(2);
41793
41780
  }
41794
41781
  const attemptedSlug = creativeSlugFromCanvasPath(filePath);
41795
- if (attemptedSlug) await clearCreativeDirty(attemptedSlug, path16.join(canvasRoot, ".dirty"));
41782
+ if (attemptedSlug) await clearCreativeDirty(attemptedSlug);
41796
41783
  const healed = await healCanvasPaths(filePath, parsed);
41797
41784
  parsed = healed.canvas;
41798
41785
  const canvasText = healed.text ?? raw;
@@ -41888,8 +41875,8 @@ ${describeRewrites(healed.rewrites)}
41888
41875
  }
41889
41876
  }
41890
41877
  const engine = createEngineFromEnv({
41891
- cacheDir: opts.cacheDir ?? path16.join(canvasRoot, ".cache"),
41892
- outputsDir: opts.outputsDir ?? canvasRoot,
41878
+ cacheDir: opts.cacheDir,
41879
+ outputsDir: opts.outputsDir,
41893
41880
  log: (line) => process.stdout.write(`${line}
41894
41881
  `),
41895
41882
  remoteCache: opts.remoteCache
@@ -41909,7 +41896,7 @@ ${describeRewrites(healed.rewrites)}
41909
41896
  const canvasSha = sha256Hex(Buffer.from(canvasText));
41910
41897
  const creativeSlug = creativeSlugFromCanvasPath(filePath) ?? void 0;
41911
41898
  const client = opts.record === false ? null : buildBackendClient();
41912
- const outputsDir = opts.outputsDir ? path16.resolve(opts.outputsDir) : canvasRoot;
41899
+ const outputsDir = opts.outputsDir ? path16.resolve(opts.outputsDir) : path16.resolve("canvas");
41913
41900
  const { runId, resumed, source, concurrentRunId } = await resolveRunId({
41914
41901
  explicitRunId: opts.runId,
41915
41902
  fresh: opts.fresh === true,
@@ -41943,7 +41930,7 @@ ${describeRewrites(healed.rewrites)}
41943
41930
  const canvasSnapshotUrl = client && creativeSlug ? await uploadRunSnapshot(client, { canvasPath: filePath, raw: canvasText, creativeSlug, parsed }) ?? void 0 : void 0;
41944
41931
  const recordMeta = {
41945
41932
  creativeSlug,
41946
- canvasPath: path16.relative(path16.dirname(canvasRoot), filePath) || void 0,
41933
+ canvasPath: path16.relative(process.cwd(), filePath) || void 0,
41947
41934
  canvasSha,
41948
41935
  // The fingerprint the dashboard compares against the current source to flag
41949
41936
  // "edited since last render". Computed from the on-disk canvas.json +
@@ -50615,7 +50602,7 @@ function loadJsonPayloads(args5) {
50615
50602
  }
50616
50603
  return parsed.map((entry, index) => asObject(entry, "json", index));
50617
50604
  }
50618
- var RETRYABLE_CODES = /* @__PURE__ */ new Set(["RATE_LIMITED", "INTERNAL_ERROR", "NETWORK_ERROR", "TIMEOUT"]);
50605
+ var RETRYABLE_CODES = /* @__PURE__ */ new Set(["RATE_LIMITED", "UNAVAILABLE", "INTERNAL_ERROR", "NETWORK_ERROR", "TIMEOUT"]);
50619
50606
  function handleError5(err) {
50620
50607
  if (err instanceof ApiError) {
50621
50608
  const readOnly = err.code === "FORBIDDEN" && /read the property but not change it/i.test(err.message);
@@ -53867,6 +53854,22 @@ function parseAutoIngest(raw, max) {
53867
53854
  return { ok: true, value };
53868
53855
  }
53869
53856
 
53857
+ // src/commands/images/extractErrors.ts
53858
+ var RETRYABLE = /* @__PURE__ */ new Set(["UNAVAILABLE", "RATE_LIMITED", "TIMEOUT", "NETWORK_ERROR"]);
53859
+ function extractErrorEnvelope(err, url) {
53860
+ const retryable = RETRYABLE.has(err.code);
53861
+ const fallback = `use \`baker images screenshot ${url}\` to capture the page as one image, or \`baker images logo <domain>\` for the brand's mark, and carry on with the rest of the job.`;
53862
+ return {
53863
+ ok: false,
53864
+ error: {
53865
+ code: err.code,
53866
+ message: err.message,
53867
+ retryable,
53868
+ fix: retryable ? { action: "retry_once", explanation: `Run the same command once more. If it fails again, ${fallback}` } : { action: "use_fallback", explanation: `Retrying the same address will fail the same way \u2014 ${fallback}` }
53869
+ }
53870
+ };
53871
+ }
53872
+
53870
53873
  // src/commands/images/extract.ts
53871
53874
  registerSchema({
53872
53875
  command: "images.extract",
@@ -53933,7 +53936,7 @@ var extractCommand = defineCommand149({
53933
53936
  writeJson({ ok: true, data });
53934
53937
  } catch (err) {
53935
53938
  if (err instanceof ApiError) {
53936
- writeJson({ ok: false, error: { code: err.code, message: err.message } });
53939
+ writeJson(extractErrorEnvelope(err, String(args5.url)));
53937
53940
  process.exit(1);
53938
53941
  }
53939
53942
  writeJson({ ok: false, error: { code: "INTERNAL_ERROR", message: "Unexpected error" } });
@@ -65674,7 +65677,7 @@ function loadJsonArg2(args5, flag = "json") {
65674
65677
  return failValidation5(`--${flag} is not valid JSON`);
65675
65678
  }
65676
65679
  }
65677
- var RETRYABLE_CODES2 = /* @__PURE__ */ new Set(["RATE_LIMITED", "INTERNAL_ERROR", "NETWORK_ERROR", "TIMEOUT"]);
65680
+ var RETRYABLE_CODES2 = /* @__PURE__ */ new Set(["RATE_LIMITED", "UNAVAILABLE", "INTERNAL_ERROR", "NETWORK_ERROR", "TIMEOUT"]);
65678
65681
  function handleError8(err) {
65679
65682
  if (err instanceof ApiError) {
65680
65683
  writeJsonEnvelope({