@koda-sl/baker-cli 0.359.0 → 0.361.0-dev.0306d9ad9

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-AI4HWQVN.js";
72
+ } from "./chunk-K2IKLMCM.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,
@@ -19818,7 +19818,7 @@ function handleLinkedinError(err) {
19818
19818
  explanation: explanation.message,
19819
19819
  correctedCommand: explanation.correctedCommand
19820
19820
  },
19821
- retryable: err.code === "RATE_LIMITED" || err.code === "INTERNAL_ERROR"
19821
+ retryable: err.code === "RATE_LIMITED" || err.code === "UNAVAILABLE" || err.code === "INTERNAL_ERROR"
19822
19822
  }
19823
19823
  };
19824
19824
  writeAdsJson(envelope);
@@ -26357,10 +26357,10 @@ Example: baker ads meta ads retag 120252158038400247 --url-tags "utm_source={{si
26357
26357
  }
26358
26358
  },
26359
26359
  run: async ({ args: args5 }) => {
26360
- const { apiPost: apiPost2 } = await import("./client-ODCNYCP4.js");
26360
+ const { apiPost: apiPost2 } = await import("./client-3DGBQ2H3.js");
26361
26361
  const { requireChatId: requireChatId2 } = await import("./env-BSG7TY5Y.js");
26362
- const { writeJsonEnvelope: writeJsonEnvelope2 } = await import("./output-2XBCYF7L.js");
26363
- 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");
26364
26364
  try {
26365
26365
  const accountId = bareAccountId2(args5);
26366
26366
  const urlTags = typeof args5["url-tags"] === "string" ? args5["url-tags"] : void 0;
@@ -26505,10 +26505,10 @@ function duplicateCommand2(entity, label) {
26505
26505
  replace: { type: "boolean", description: "Pause the original once the copy publishes" }
26506
26506
  },
26507
26507
  run: async ({ args: args5 }) => {
26508
- const { apiPost: apiPost2 } = await import("./client-ODCNYCP4.js");
26508
+ const { apiPost: apiPost2 } = await import("./client-3DGBQ2H3.js");
26509
26509
  const { requireChatId: requireChatId2 } = await import("./env-BSG7TY5Y.js");
26510
- const { writeJsonEnvelope: writeJsonEnvelope2 } = await import("./output-2XBCYF7L.js");
26511
- 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");
26512
26512
  try {
26513
26513
  const accountId = bareAccountId2(args5);
26514
26514
  const chatId = requireChatId2();
@@ -40880,6 +40880,7 @@ async function clearCreativeDirty(slug, base) {
40880
40880
  }
40881
40881
 
40882
40882
  // src/commands/canvas/run-resume.ts
40883
+ import { readFileSync as readFileSync14 } from "fs";
40883
40884
  import { mkdir as mkdir4, readFile as readFile9, rm as rm4, writeFile as writeFile6 } from "fs/promises";
40884
40885
  import path13 from "path";
40885
40886
  function markerKey(canvasPath) {
@@ -40902,9 +40903,12 @@ function classifyRemoteRun(run, now) {
40902
40903
  }
40903
40904
  async function resolveRunId(opts) {
40904
40905
  if (opts.explicitRunId) return { runId: opts.explicitRunId, resumed: false };
40906
+ const existing = await readMarker(opts.outputsDir, opts.canvasPath);
40907
+ if (existing?.pid !== void 0 && existing.pid !== process.pid && (opts.isRendering ?? isRenderProcess)(existing.pid)) {
40908
+ return { runId: existing.runId, resumed: false, rendering: { runId: existing.runId, pid: existing.pid } };
40909
+ }
40905
40910
  if (!opts.fresh) {
40906
- const existing = await readMarkerRunId(opts.outputsDir, opts.canvasPath);
40907
- if (existing) return { runId: existing, resumed: true, source: "local" };
40911
+ if (existing) return { runId: existing.runId, resumed: true, source: "local" };
40908
40912
  if (opts.remote) {
40909
40913
  try {
40910
40914
  const active = await opts.remote.getActiveRun(opts.remote.creativeSlug, opts.remote.canvasSha);
@@ -40924,22 +40928,31 @@ async function resolveRunId(opts) {
40924
40928
  }
40925
40929
  return { runId: `r_${ulid()}`, resumed: false };
40926
40930
  }
40927
- async function readMarkerRunId(outputsDir, canvasPath) {
40931
+ async function readMarker(outputsDir, canvasPath) {
40928
40932
  for (const key of [markerKey(canvasPath), legacyMarkerKey(canvasPath)]) {
40929
40933
  try {
40930
40934
  const raw = await readFile9(markerFile(outputsDir, key), "utf8");
40931
40935
  const parsed = JSON.parse(raw);
40932
- if (typeof parsed.runId === "string" && parsed.runId.length > 0) return parsed.runId;
40936
+ if (typeof parsed.runId === "string" && parsed.runId.length > 0) {
40937
+ return { runId: parsed.runId, ...Number.isInteger(parsed.pid) ? { pid: parsed.pid } : {} };
40938
+ }
40933
40939
  } catch {
40934
40940
  }
40935
40941
  }
40936
40942
  return null;
40937
40943
  }
40944
+ function isRenderProcess(pid) {
40945
+ try {
40946
+ return readFileSync14(`/proc/${pid}/cmdline`, "utf8").includes("canvas");
40947
+ } catch {
40948
+ return false;
40949
+ }
40950
+ }
40938
40951
  async function markRunInFlight(outputsDir, canvasPath, runId) {
40939
40952
  try {
40940
40953
  const file = markerFile(outputsDir, markerKey(canvasPath));
40941
40954
  await mkdir4(path13.dirname(file), { recursive: true });
40942
- await writeFile6(file, JSON.stringify({ runId, canvasPath: path13.resolve(canvasPath), startedAt: Date.now() }));
40955
+ await writeFile6(file, JSON.stringify({ runId, pid: process.pid, canvasPath: path13.resolve(canvasPath), startedAt: Date.now() }));
40943
40956
  } catch {
40944
40957
  }
40945
40958
  }
@@ -41897,13 +41910,31 @@ ${describeRewrites(healed.rewrites)}
41897
41910
  const creativeSlug = creativeSlugFromCanvasPath(filePath) ?? void 0;
41898
41911
  const client = opts.record === false ? null : buildBackendClient();
41899
41912
  const outputsDir = opts.outputsDir ? path16.resolve(opts.outputsDir) : path16.resolve("canvas");
41900
- const { runId, resumed, source, concurrentRunId } = await resolveRunId({
41913
+ const { runId, resumed, source, concurrentRunId, rendering } = await resolveRunId({
41901
41914
  explicitRunId: opts.runId,
41902
41915
  fresh: opts.fresh === true,
41903
41916
  outputsDir,
41904
41917
  canvasPath: filePath,
41905
41918
  remote: client && creativeSlug ? { creativeSlug, canvasSha, getActiveRun: (slug, sha) => client.getActiveRun(slug, sha) } : void 0
41906
41919
  });
41920
+ if (rendering) {
41921
+ process.stderr.write(
41922
+ `${JSON.stringify(
41923
+ {
41924
+ ok: false,
41925
+ error: {
41926
+ code: "CONFLICT",
41927
+ message: `This canvas is already rendering in another process (run ${rendering.runId}, pid ${rendering.pid}).`,
41928
+ fix: "Wait for that render to finish and use its result instead of starting a second one: two renders of one canvas share a run and overwrite each other. If you started it in the background, read its output when it completes."
41929
+ }
41930
+ },
41931
+ null,
41932
+ 2
41933
+ )}
41934
+ `
41935
+ );
41936
+ process.exit(2);
41937
+ }
41907
41938
  if (resumed) {
41908
41939
  process.stdout.write(
41909
41940
  `[resume] continuing interrupted run ${runId}${source === "remote" ? " (adopted from run history)" : ""} \u2014 in-flight jobs re-attach, cached nodes skip
@@ -48257,11 +48288,11 @@ var addSideEffectCommand = defineCommand125({
48257
48288
  });
48258
48289
 
48259
48290
  // src/commands/flows/map.ts
48260
- import { readFileSync as readFileSync15 } from "fs";
48291
+ import { readFileSync as readFileSync16 } from "fs";
48261
48292
  import { defineCommand as defineCommand126 } from "citty";
48262
48293
 
48263
48294
  // src/commands/flows/value-expression.ts
48264
- import { existsSync as existsSync6, readFileSync as readFileSync14 } from "fs";
48295
+ import { existsSync as existsSync6, readFileSync as readFileSync15 } from "fs";
48265
48296
  import { join as join4 } from "path";
48266
48297
  var PREFIXES2 = ["field", "tracking", "global", "code"];
48267
48298
  function splitParts(raw) {
@@ -48348,7 +48379,7 @@ function trackingFieldIds() {
48348
48379
  const path48 = join4(flowsDir(), "..", "tracking.ts");
48349
48380
  if (!existsSync6(path48)) return null;
48350
48381
  try {
48351
- const source = readFileSync14(path48, "utf-8");
48382
+ const source = readFileSync15(path48, "utf-8");
48352
48383
  const block2 = source.match(/TRACKING_FIELD_IDS\s*=\s*\[([\s\S]*?)\]\s*as const/)?.[1];
48353
48384
  if (!block2) return null;
48354
48385
  const ids = [...block2.matchAll(/"(tracking\.[a-z0-9_]+)"/g)].map((match) => match[1]);
@@ -48925,7 +48956,7 @@ function specsFromFile(parsed) {
48925
48956
  function readSpecFile(path48) {
48926
48957
  let raw;
48927
48958
  try {
48928
- raw = path48 === "-" ? readFileSync15(0, "utf-8") : readFileSync15(path48, "utf-8");
48959
+ raw = path48 === "-" ? readFileSync16(0, "utf-8") : readFileSync16(path48, "utf-8");
48929
48960
  } catch (error) {
48930
48961
  refuse(
48931
48962
  `Could not read ${path48 === "-" ? "the mapping from stdin" : `"${path48}"`}: ${error instanceof Error ? error.message : String(error)}`
@@ -49624,7 +49655,7 @@ Full guide: __tooling__/docs/tools/baker/flows.md`
49624
49655
  });
49625
49656
 
49626
49657
  // src/commands/funnel/index.ts
49627
- import { readFileSync as readFileSync16 } from "fs";
49658
+ import { readFileSync as readFileSync17 } from "fs";
49628
49659
  import { defineCommand as defineCommand130 } from "citty";
49629
49660
 
49630
49661
  // src/commands/funnel/formMoments.ts
@@ -50420,7 +50451,7 @@ function workspaceForms() {
50420
50451
  const forms = [];
50421
50452
  for (const slug of listFlowSlugs()) {
50422
50453
  try {
50423
- forms.push({ slug, tree: JSON.parse(readFileSync16(flowDataPath(slug), "utf-8")) });
50454
+ forms.push({ slug, tree: JSON.parse(readFileSync17(flowDataPath(slug), "utf-8")) });
50424
50455
  } catch {
50425
50456
  }
50426
50457
  }
@@ -50555,7 +50586,7 @@ Examples:
50555
50586
  import { defineCommand as defineCommand132 } from "citty";
50556
50587
 
50557
50588
  // src/commands/ga4/shared.ts
50558
- import { readFileSync as readFileSync17 } from "fs";
50589
+ import { readFileSync as readFileSync18 } from "fs";
50559
50590
  function failValidation3(message) {
50560
50591
  writeJsonEnvelope({ ok: false, error: { code: "VALIDATION_ERROR", message } });
50561
50592
  process.exit(1);
@@ -50571,7 +50602,7 @@ function requireTarget5(args5, entity) {
50571
50602
  function readJsonSource(args5, flag) {
50572
50603
  const inline = args5[flag];
50573
50604
  const file = args5.file;
50574
- const raw = typeof file === "string" && file.length > 0 ? readFileSync17(file, "utf8") : typeof inline === "string" && inline.length > 0 ? inline : void 0;
50605
+ const raw = typeof file === "string" && file.length > 0 ? readFileSync18(file, "utf8") : typeof inline === "string" && inline.length > 0 ? inline : void 0;
50575
50606
  if (raw === void 0) {
50576
50607
  failValidation3(`pass --${flag} with inline JSON or --file with a path to a JSON file`);
50577
50608
  }
@@ -50602,7 +50633,7 @@ function loadJsonPayloads(args5) {
50602
50633
  }
50603
50634
  return parsed.map((entry, index) => asObject(entry, "json", index));
50604
50635
  }
50605
- var RETRYABLE_CODES = /* @__PURE__ */ new Set(["RATE_LIMITED", "INTERNAL_ERROR", "NETWORK_ERROR", "TIMEOUT"]);
50636
+ var RETRYABLE_CODES = /* @__PURE__ */ new Set(["RATE_LIMITED", "UNAVAILABLE", "INTERNAL_ERROR", "NETWORK_ERROR", "TIMEOUT"]);
50606
50637
  function handleError5(err) {
50607
50638
  if (err instanceof ApiError) {
50608
50639
  const readOnly = err.code === "FORBIDDEN" && /read the property but not change it/i.test(err.message);
@@ -50922,7 +50953,7 @@ Examples:
50922
50953
  });
50923
50954
 
50924
50955
  // src/commands/ga4/query.ts
50925
- import { appendFileSync as appendFileSync2, existsSync as existsSync7, readFileSync as readFileSync18, writeFileSync as writeFileSync4 } from "fs";
50956
+ import { appendFileSync as appendFileSync2, existsSync as existsSync7, readFileSync as readFileSync19, writeFileSync as writeFileSync4 } from "fs";
50926
50957
  import { resolve as resolve2 } from "path";
50927
50958
  import { defineCommand as defineCommand135 } from "citty";
50928
50959
 
@@ -51013,7 +51044,7 @@ function writeRowsToFile2(filePath, rows, append) {
51013
51044
  writeFileSync4(filePath, content, "utf-8");
51014
51045
  }
51015
51046
  } else if (append && existsSync7(filePath)) {
51016
- const existing = JSON.parse(readFileSync18(filePath, "utf-8"));
51047
+ const existing = JSON.parse(readFileSync19(filePath, "utf-8"));
51017
51048
  writeFileSync4(filePath, JSON.stringify([...existing, ...rows], null, 2), "utf-8");
51018
51049
  } else {
51019
51050
  writeFileSync4(filePath, JSON.stringify(rows, null, 2), "utf-8");
@@ -51865,7 +51896,7 @@ Full guide: __tooling__/docs/tools/baker/ga4.md`
51865
51896
  import { defineCommand as defineCommand141 } from "citty";
51866
51897
 
51867
51898
  // src/commands/gsc/query.ts
51868
- import { appendFileSync as appendFileSync3, existsSync as existsSync8, readFileSync as readFileSync19, writeFileSync as writeFileSync5 } from "fs";
51899
+ import { appendFileSync as appendFileSync3, existsSync as existsSync8, readFileSync as readFileSync20, writeFileSync as writeFileSync5 } from "fs";
51869
51900
  import { resolve as resolve3 } from "path";
51870
51901
  import { defineCommand as defineCommand138 } from "citty";
51871
51902
 
@@ -52013,7 +52044,7 @@ function writeRowsToFile3(filePath, rows, append) {
52013
52044
  writeFileSync5(filePath, content, "utf-8");
52014
52045
  }
52015
52046
  } else if (append && existsSync8(filePath)) {
52016
- const existing = JSON.parse(readFileSync19(filePath, "utf-8"));
52047
+ const existing = JSON.parse(readFileSync20(filePath, "utf-8"));
52017
52048
  writeFileSync5(filePath, JSON.stringify([...existing, ...rows], null, 2), "utf-8");
52018
52049
  } else {
52019
52050
  writeFileSync5(filePath, JSON.stringify(rows, null, 2), "utf-8");
@@ -53854,6 +53885,22 @@ function parseAutoIngest(raw, max) {
53854
53885
  return { ok: true, value };
53855
53886
  }
53856
53887
 
53888
+ // src/commands/images/extractErrors.ts
53889
+ var RETRYABLE = /* @__PURE__ */ new Set(["UNAVAILABLE", "RATE_LIMITED", "TIMEOUT", "NETWORK_ERROR"]);
53890
+ function extractErrorEnvelope(err, url) {
53891
+ const retryable = RETRYABLE.has(err.code);
53892
+ 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.`;
53893
+ return {
53894
+ ok: false,
53895
+ error: {
53896
+ code: err.code,
53897
+ message: err.message,
53898
+ retryable,
53899
+ 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}` }
53900
+ }
53901
+ };
53902
+ }
53903
+
53857
53904
  // src/commands/images/extract.ts
53858
53905
  registerSchema({
53859
53906
  command: "images.extract",
@@ -53920,7 +53967,7 @@ var extractCommand = defineCommand149({
53920
53967
  writeJson({ ok: true, data });
53921
53968
  } catch (err) {
53922
53969
  if (err instanceof ApiError) {
53923
- writeJson({ ok: false, error: { code: err.code, message: err.message } });
53970
+ writeJson(extractErrorEnvelope(err, String(args5.url)));
53924
53971
  process.exit(1);
53925
53972
  }
53926
53973
  writeJson({ ok: false, error: { code: "INTERNAL_ERROR", message: "Unexpected error" } });
@@ -65631,7 +65678,7 @@ import { defineCommand as defineCommand221 } from "citty";
65631
65678
  import { defineCommand as defineCommand218 } from "citty";
65632
65679
 
65633
65680
  // src/commands/tag-manager/shared.ts
65634
- import { readFileSync as readFileSync20 } from "fs";
65681
+ import { readFileSync as readFileSync21 } from "fs";
65635
65682
  function failValidation5(message) {
65636
65683
  writeJsonEnvelope({ ok: false, error: { code: "VALIDATION_ERROR", message } });
65637
65684
  process.exit(1);
@@ -65647,7 +65694,7 @@ function requireTarget6(args5, entity) {
65647
65694
  function loadJsonArg2(args5, flag = "json") {
65648
65695
  const inline = args5[flag];
65649
65696
  const file = args5.file;
65650
- const raw = typeof file === "string" && file.length > 0 ? readFileSync20(file, "utf8") : typeof inline === "string" && inline.length > 0 ? inline : void 0;
65697
+ const raw = typeof file === "string" && file.length > 0 ? readFileSync21(file, "utf8") : typeof inline === "string" && inline.length > 0 ? inline : void 0;
65651
65698
  if (raw === void 0) {
65652
65699
  failValidation5(`pass --${flag} with inline JSON or --file with a path to a JSON file`);
65653
65700
  }
@@ -65661,7 +65708,7 @@ function loadJsonArg2(args5, flag = "json") {
65661
65708
  return failValidation5(`--${flag} is not valid JSON`);
65662
65709
  }
65663
65710
  }
65664
- var RETRYABLE_CODES2 = /* @__PURE__ */ new Set(["RATE_LIMITED", "INTERNAL_ERROR", "NETWORK_ERROR", "TIMEOUT"]);
65711
+ var RETRYABLE_CODES2 = /* @__PURE__ */ new Set(["RATE_LIMITED", "UNAVAILABLE", "INTERNAL_ERROR", "NETWORK_ERROR", "TIMEOUT"]);
65665
65712
  function handleError8(err) {
65666
65713
  if (err instanceof ApiError) {
65667
65714
  writeJsonEnvelope({
@@ -69050,7 +69097,7 @@ function outputFlagForCommand(root, argv) {
69050
69097
  }
69051
69098
 
69052
69099
  // src/version.ts
69053
- import { readFileSync as readFileSync21 } from "fs";
69100
+ import { readFileSync as readFileSync22 } from "fs";
69054
69101
  function packageJsonUrl() {
69055
69102
  return new URL("../package.json", import.meta.url);
69056
69103
  }
@@ -69062,7 +69109,7 @@ function parsePackageVersion(raw) {
69062
69109
  throw new Error("Invalid CLI package.json: missing version");
69063
69110
  }
69064
69111
  function getCliVersion() {
69065
- return parsePackageVersion(readFileSync21(packageJsonUrl(), "utf8"));
69112
+ return parsePackageVersion(readFileSync22(packageJsonUrl(), "utf8"));
69066
69113
  }
69067
69114
 
69068
69115
  // src/cli.ts