@koda-sl/baker-cli 0.142.0 → 0.145.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.
package/README.md CHANGED
@@ -42,7 +42,7 @@ export BAKER_CHAT_ID="<chat-id>"
42
42
  - `BAKER_GA4_PROPERTY_ID` — default GA4 property ID. Used when `--property-id` is not passed. If neither is set and exactly one property is connected, it is auto-selected.
43
43
  - `BAKER_GSC_SITE_URL` — default GSC site URL. Used when `--site-url` is not passed. If neither is set and exactly one site is verified, it is auto-selected.
44
44
  - `BAKER_X_ADS_ACCOUNT_ID` — default X Ads account ID (base36). Used when `--account-id` is not passed. If neither is set and exactly one X Ads account is connected, it is auto-selected.
45
- - `BAKER_CHAT_ID` — chat context for staged Work Action commands and staged Scheduled Action create/update/delete. `scheduled-actions trigger` does not require it.
45
+ - `BAKER_CHAT_ID` — chat context for staged Work Action commands and staged Scheduled Action create/update/delete. `scheduled-actions trigger` does not require it. When set, it is also sent as an `x-baker-chat-id` header on every request, so library additions that apply immediately (images, videos, followed brands) are listed in that chat's changes as already applied.
46
46
 
47
47
  ## Debug logging
48
48
 
@@ -2132,7 +2132,11 @@ baker videos search "tutorial" --tags explainer,ugc --limit 5
2132
2132
 
2133
2133
  ### `baker videos get <id>`
2134
2134
 
2135
- Get a single video by ID.
2135
+ Get a single video by ID. `--full` includes the auto-generated **transcript** (Baker transcribes every upload with speech-to-text), a `transcript_segment_count`, and the titled/timestamped **scene breakdown** — so you can read what a clip says and shows without watching it. Default JSON also returns the raw `transcript` + timed `transcriptSegments`; `--full` is what surfaces them in `--output md`/`files` too.
2136
+
2137
+ ```bash
2138
+ baker videos get j571abc123 --full --output md
2139
+ ```
2136
2140
 
2137
2141
  ### `baker videos upload <file>`
2138
2142
 
@@ -2326,6 +2330,16 @@ Top winning ads for one advertiser id → `GET /api/ad-library/advertiser-winner
2326
2330
  baker winning-ads winners adv_123 --top 15 --output md
2327
2331
  ```
2328
2332
 
2333
+ ### `baker winning-ads content <ad-id>`
2334
+
2335
+ Read what's **inside** one ad → `GET /api/ad-library/ad-content`. `search`/`winners`/`feed` return a lean shortlist; this opens a single `ad_id` so you can understand a reference before reproducing it. For a **video** it returns the spoken `transcript`, the `on_screen_text` overlays, and the ad `copy` (`primary_text`/`headline`/`cta`); a **static** ad returns only the copy. Add `--full` for speech (voiceover style, speaker count, direct-address), pacing (scene count, shot length, key timeline moments), any recognised soundtrack, and each on-screen overlay's role + position (e.g. `GLOBAL PAYROLL @0.5s [hook, top]`) so you can place text faithfully. `--platform meta|linkedin` selects which platform the id is on (default `meta`).
2336
+
2337
+ ```bash
2338
+ baker winning-ads content adg_123 # transcript + on-screen text + copy
2339
+ baker winning-ads content adg_123 --platform meta --full # + speech, pacing, soundtrack
2340
+ baker winning-ads content adg_123 --full --output md
2341
+ ```
2342
+
2329
2343
  ### `baker winning-ads unfollow <advertiser>`
2330
2344
 
2331
2345
  Stop following a brand by advertiser id → `POST /api/ad-library/unfollow`. Returns `{ removed }`.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  apiGet
3
- } from "./chunk-SCI57ZZ6.js";
3
+ } from "./chunk-LVCBCJWF.js";
4
4
  import {
5
5
  getEnv
6
6
  } from "./chunk-RK67WL4O.js";
@@ -261,4 +261,4 @@ export {
261
261
  writeAdsOutput,
262
262
  resolveCustomerId
263
263
  };
264
- //# sourceMappingURL=chunk-PEBP6G74.js.map
264
+ //# sourceMappingURL=chunk-FBUVCOH6.js.map
@@ -62,6 +62,10 @@ function sanitizeParams(params) {
62
62
  }
63
63
  return sanitized;
64
64
  }
65
+ function chatHeader() {
66
+ const chatId = getEnv().BAKER_CHAT_ID;
67
+ return chatId ? { "x-baker-chat-id": chatId } : {};
68
+ }
65
69
  function mapHttpError(status) {
66
70
  if (status === 401 || status === 403) {
67
71
  return "UNAUTHORIZED";
@@ -117,7 +121,8 @@ async function apiGet(path, params) {
117
121
  method: "GET",
118
122
  headers: {
119
123
  Authorization: `Bearer ${env.BAKER_API_KEY}`,
120
- Accept: "application/json"
124
+ Accept: "application/json",
125
+ ...chatHeader()
121
126
  },
122
127
  signal: AbortSignal.timeout(6e4)
123
128
  });
@@ -151,7 +156,8 @@ async function apiPost(path, body, opts) {
151
156
  headers: {
152
157
  Authorization: `Bearer ${env.BAKER_API_KEY}`,
153
158
  "Content-Type": "application/json",
154
- Accept: "application/json"
159
+ Accept: "application/json",
160
+ ...chatHeader()
155
161
  },
156
162
  body: JSON.stringify(body),
157
163
  signal: AbortSignal.timeout(timeoutMs)
@@ -189,4 +195,4 @@ export {
189
195
  apiGet,
190
196
  apiPost
191
197
  };
192
- //# sourceMappingURL=chunk-SCI57ZZ6.js.map
198
+ //# sourceMappingURL=chunk-LVCBCJWF.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\ntype ErrorCode =\n | \"UNAUTHORIZED\"\n | \"NOT_FOUND\"\n | \"VALIDATION_ERROR\"\n | \"RATE_LIMITED\"\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 || status === 403) {\n return \"UNAUTHORIZED\";\n }\n if (status === 404) {\n return \"NOT_FOUND\";\n }\n if (status === 422 || status === 400) {\n return \"VALIDATION_ERROR\";\n }\n if (status === 429) {\n return \"RATE_LIMITED\";\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\nexport async function apiGet<T>(path: string, params?: Record<string, string>): Promise<T> {\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 handleResponse<T>(response);\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":[]}
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  handleConnectionError,
3
3
  writeAdsJson
4
- } from "./chunk-PEBP6G74.js";
4
+ } from "./chunk-FBUVCOH6.js";
5
5
  import {
6
6
  ApiError
7
- } from "./chunk-SCI57ZZ6.js";
7
+ } from "./chunk-LVCBCJWF.js";
8
8
  import {
9
9
  getEnv
10
10
  } from "./chunk-RK67WL4O.js";
@@ -94,4 +94,4 @@ export {
94
94
  csvOrJson,
95
95
  resolveEffectiveStatus
96
96
  };
97
- //# sourceMappingURL=chunk-BFZELBTE.js.map
97
+ //# sourceMappingURL=chunk-UFBKUMN2.js.map
package/dist/cli.js CHANGED
@@ -43,7 +43,7 @@ import {
43
43
  resolveAccountIdArg,
44
44
  resolveEffectiveStatus,
45
45
  todayIso
46
- } from "./chunk-BFZELBTE.js";
46
+ } from "./chunk-UFBKUMN2.js";
47
47
  import {
48
48
  buildQueryCacheKey,
49
49
  cacheGet,
@@ -56,13 +56,13 @@ import {
56
56
  writeAdsJson,
57
57
  writeAdsOutput,
58
58
  writeJsonEnvelope
59
- } from "./chunk-PEBP6G74.js";
59
+ } from "./chunk-FBUVCOH6.js";
60
60
  import {
61
61
  ApiError,
62
62
  apiGet,
63
63
  apiPost,
64
64
  validateConvexId
65
- } from "./chunk-SCI57ZZ6.js";
65
+ } from "./chunk-LVCBCJWF.js";
66
66
  import {
67
67
  installStreamTaps,
68
68
  logInvocation
@@ -73,7 +73,7 @@ import {
73
73
  } from "./chunk-RK67WL4O.js";
74
74
 
75
75
  // src/cli.ts
76
- import { defineCommand as defineCommand177, runMain } from "citty";
76
+ import { defineCommand as defineCommand178, runMain } from "citty";
77
77
 
78
78
  // src/commands/actions/index.ts
79
79
  import { defineCommand as defineCommand18 } from "citty";
@@ -145,6 +145,22 @@ function compactVideo(video) {
145
145
  status: String(video.status ?? "")
146
146
  };
147
147
  }
148
+ function sceneLines(value) {
149
+ if (!Array.isArray(value)) {
150
+ return [];
151
+ }
152
+ return value.flatMap((s) => {
153
+ const scene = asRecord(s);
154
+ const title = typeof scene.title === "string" ? scene.title : "";
155
+ if (!title) {
156
+ return [];
157
+ }
158
+ const start = round2(scene.startSecond);
159
+ const end = round2(scene.endSecond);
160
+ const range = start !== null && end !== null ? ` (${start}-${end}s)` : "";
161
+ return [`${title}${range}`];
162
+ });
163
+ }
148
164
  function fullVideo(video) {
149
165
  const compact = compactVideo(video);
150
166
  return {
@@ -152,6 +168,12 @@ function fullVideo(video) {
152
168
  duration: video.duration ?? null,
153
169
  muxPlaybackId: String(video.muxPlaybackId ?? ""),
154
170
  source: String(video.source ?? ""),
171
+ // The spoken transcript + AI scene breakdown of the client's own clip — the
172
+ // "understand what's in this video" payload. Kept out of the compact/search
173
+ // projection (too large for a list) and surfaced here on --full.
174
+ transcript: video.transcript ?? null,
175
+ transcript_segment_count: Array.isArray(video.transcriptSegments) ? video.transcriptSegments.length : 0,
176
+ scenes: sceneLines(video.scenes),
155
177
  createdAt: video.createdAt ?? null
156
178
  };
157
179
  }
@@ -243,6 +265,76 @@ function winningAdNormalizer(record, full) {
243
265
  }
244
266
  return compactWinningAd(record);
245
267
  }
268
+ function overlayLines(value, detailed = false) {
269
+ if (!Array.isArray(value)) {
270
+ return [];
271
+ }
272
+ return value.flatMap((o) => {
273
+ const item = asRecord(o);
274
+ const text = typeof item.text === "string" ? item.text : "";
275
+ if (!text) {
276
+ return [];
277
+ }
278
+ const at = round2(item.appears_at_s);
279
+ let line = at !== null ? `${text} @${at}s` : text;
280
+ if (detailed) {
281
+ const tags = [item.role, item.position].filter((t) => typeof t === "string" && t.length > 0);
282
+ if (tags.length) {
283
+ line += ` [${tags.join(", ")}]`;
284
+ }
285
+ }
286
+ return [line];
287
+ });
288
+ }
289
+ function compactAdContent(record) {
290
+ return {
291
+ ad_id: record.ad_id ?? null,
292
+ advertiser: record.advertiser ?? null,
293
+ format: record.format ?? null,
294
+ media_kind: record.media_kind ?? null,
295
+ media_url: record.media_url ?? null,
296
+ summary: record.summary ?? null,
297
+ transcript: record.transcript ?? null,
298
+ on_screen_text: overlayLines(record.on_screen_text),
299
+ has_captions: typeof record.has_captions === "boolean" ? record.has_captions : null,
300
+ primary_text: record.primary_text ?? null,
301
+ headline: record.headline ?? null,
302
+ cta: record.cta ?? null
303
+ };
304
+ }
305
+ function fullAdContent(record) {
306
+ const speech = asRecord(record.speech);
307
+ const pacing = asRecord(record.pacing);
308
+ const music = asRecord(record.music);
309
+ return {
310
+ ...compactAdContent(record),
311
+ platform: record.platform ?? null,
312
+ // Re-derive with role/position tags — the lean compact version dropped them.
313
+ on_screen_text: overlayLines(record.on_screen_text, true),
314
+ winner_score: round2(record.winner_score),
315
+ winner_category: record.winner_category ?? null,
316
+ angle: record.angle ?? null,
317
+ hook_archetype: record.hook_archetype ?? null,
318
+ caption_style: record.caption_style ?? null,
319
+ has_speech: typeof record.has_speech === "boolean" ? record.has_speech : null,
320
+ speech_pct_of_runtime: round2(speech.speech_pct_of_runtime),
321
+ speaker_count: speech.speaker_count ?? null,
322
+ voiceover_vs_sync: speech.voiceover_vs_sync ?? null,
323
+ is_direct_address: typeof speech.is_direct_address === "boolean" ? speech.is_direct_address : null,
324
+ speech_emotion_dominant: speech.speech_emotion_dominant ?? null,
325
+ scene_count: pacing.scene_count ?? null,
326
+ avg_shot_length_s: round2(pacing.avg_shot_length_s),
327
+ pacing_shape: pacing.pacing_shape ?? null,
328
+ hook_ends_at_s: round2(pacing.hook_ends_at_s),
329
+ cta_first_shown_at_s: round2(pacing.cta_first_shown_at_s),
330
+ product_first_appears_at_s: round2(pacing.product_first_appears_at_s),
331
+ music_track_title: music.track_title ?? null,
332
+ music_track_artist: music.track_artist ?? null
333
+ };
334
+ }
335
+ function adContentNormalizer(record, full) {
336
+ return full ? fullAdContent(record) : compactAdContent(record);
337
+ }
246
338
  function applyFieldMask(data, fields) {
247
339
  const result = {};
248
340
  for (const field of fields) {
@@ -1135,6 +1227,65 @@ var seedCatalogResponseSchema = z2.object({
1135
1227
  segment: seedCatalogSegmentSchema.nullable(),
1136
1228
  patterns: z2.array(patternSeedSchema)
1137
1229
  });
1230
+ var adContentRequestSchema = z2.object({
1231
+ ad_id: z2.string().min(1),
1232
+ platform: adLibraryPlatformSchema.optional()
1233
+ });
1234
+ var adContentOverlaySchema = z2.object({
1235
+ text: z2.string(),
1236
+ appears_at_s: z2.number().nullable(),
1237
+ position: z2.string().nullable(),
1238
+ role: z2.string().nullable()
1239
+ });
1240
+ var adContentSpeechSchema = z2.object({
1241
+ speech_pct_of_runtime: z2.number().nullable(),
1242
+ speaker_count: z2.number().nullable(),
1243
+ voiceover_vs_sync: z2.string().nullable(),
1244
+ is_direct_address: z2.boolean().nullable(),
1245
+ speech_emotion_dominant: z2.string().nullable()
1246
+ });
1247
+ var adContentPacingSchema = z2.object({
1248
+ scene_count: z2.number().nullable(),
1249
+ avg_shot_length_s: z2.number().nullable(),
1250
+ pacing_shape: z2.string().nullable(),
1251
+ hook_ends_at_s: z2.number().nullable(),
1252
+ cta_first_shown_at_s: z2.number().nullable(),
1253
+ product_first_appears_at_s: z2.number().nullable()
1254
+ });
1255
+ var adContentMusicSchema = z2.object({
1256
+ track_title: z2.string().nullable(),
1257
+ track_artist: z2.string().nullable()
1258
+ });
1259
+ var adContentSchema = z2.object({
1260
+ ad_id: z2.string().nullable(),
1261
+ advertiser: z2.string().nullable(),
1262
+ advertiser_id: z2.string().nullable(),
1263
+ platform: z2.string().nullable(),
1264
+ format: z2.string().nullable(),
1265
+ media_kind: z2.string().nullable(),
1266
+ media_url: z2.string().nullable(),
1267
+ winner_score: z2.number().nullable(),
1268
+ winner_category: z2.string().nullable(),
1269
+ summary: z2.string().nullable(),
1270
+ angle: z2.string().nullable(),
1271
+ hook_archetype: z2.string().nullable(),
1272
+ // Spoken words (video only; null when the ad has no speech track).
1273
+ transcript: z2.string().nullable(),
1274
+ has_speech: z2.boolean().nullable(),
1275
+ speech: adContentSpeechSchema,
1276
+ // On-screen text / captions.
1277
+ has_captions: z2.boolean().nullable(),
1278
+ caption_style: z2.string().nullable(),
1279
+ on_screen_text: z2.array(adContentOverlaySchema),
1280
+ // Copy the viewer reads outside the media.
1281
+ primary_text: z2.string().nullable(),
1282
+ headline: z2.string().nullable(),
1283
+ cta: z2.string().nullable(),
1284
+ // Structure + soundtrack.
1285
+ pacing: adContentPacingSchema,
1286
+ music: adContentMusicSchema
1287
+ });
1288
+ var adContentResponseSchema = z2.object({ ad: adContentSchema });
1138
1289
 
1139
1290
  // ../api/src/ads-linkedin/limits.ts
1140
1291
  var LINKEDIN_LIMITS = {
@@ -1980,6 +2131,10 @@ var chatChangeTypeSchema = z5.enum([
1980
2131
  "linkedin-ads",
1981
2132
  "google-ads",
1982
2133
  "meta-ads",
2134
+ // Immediate (already-applied) library effects — see lib/chatChanges.ts.
2135
+ "image",
2136
+ "video",
2137
+ "followed-advertiser",
1983
2138
  "briefs"
1984
2139
  ]);
1985
2140
  var chatChangeActionSchema = z5.enum(["created", "updated", "deleted"]);
@@ -4923,7 +5078,7 @@ var demandGenAdSchema = z12.object({
4923
5078
  squareImageAssets: z12.array(refSchema).optional(),
4924
5079
  logoImageAssets: z12.array(refSchema).optional()
4925
5080
  });
4926
- var adContentSchema = z12.discriminatedUnion("format", [
5081
+ var adContentSchema2 = z12.discriminatedUnion("format", [
4927
5082
  responsiveSearchAdSchema,
4928
5083
  responsiveDisplayAdSchema,
4929
5084
  callAdSchema,
@@ -4934,7 +5089,7 @@ var adContentSchema = z12.discriminatedUnion("format", [
4934
5089
  var adCreateSchema = z12.object({
4935
5090
  adGroup: refSchema,
4936
5091
  status: stageableStatusSchema2.default("PAUSED"),
4937
- content: adContentSchema
5092
+ content: adContentSchema2
4938
5093
  });
4939
5094
  var adUpdateSchema = z12.object({
4940
5095
  status: z12.enum(["ENABLED", "PAUSED", "REMOVED"]).optional(),
@@ -12684,10 +12839,10 @@ function duplicateCommand2(entity, label) {
12684
12839
  replace: { type: "boolean", description: "Pause the original once the copy publishes" }
12685
12840
  },
12686
12841
  run: async ({ args }) => {
12687
- const { apiPost: apiPost2 } = await import("./client-N7XWYCHT.js");
12842
+ const { apiPost: apiPost2 } = await import("./client-R5ZSVIK3.js");
12688
12843
  const { requireChatId: requireChatId2 } = await import("./env-3JMYIH25.js");
12689
- const { writeJsonEnvelope: writeJsonEnvelope2 } = await import("./output-7M5VQPTZ.js");
12690
- const { handleMetaError: handleMetaError2 } = await import("./shared-6SZHAUM2.js");
12844
+ const { writeJsonEnvelope: writeJsonEnvelope2 } = await import("./output-R534QTMO.js");
12845
+ const { handleMetaError: handleMetaError2 } = await import("./shared-MUWU7BK5.js");
12691
12846
  try {
12692
12847
  const accountId = bareAccountId2(args);
12693
12848
  const chatId = requireChatId2();
@@ -29434,7 +29589,7 @@ Full guide: __tooling__/docs/tools/baker/videos.md`
29434
29589
  });
29435
29590
 
29436
29591
  // src/commands/winning-ads/index.ts
29437
- import { defineCommand as defineCommand176 } from "citty";
29592
+ import { defineCommand as defineCommand177 } from "citty";
29438
29593
 
29439
29594
  // src/commands/winning-ads/advertisers.ts
29440
29595
  import { defineCommand as defineCommand165 } from "citty";
@@ -29629,8 +29784,70 @@ var briefCommand = defineCommand166({
29629
29784
  }
29630
29785
  });
29631
29786
 
29632
- // src/commands/winning-ads/feed.ts
29787
+ // src/commands/winning-ads/content.ts
29633
29788
  import { defineCommand as defineCommand167 } from "citty";
29789
+ registerSchema({
29790
+ command: "winning-ads.content",
29791
+ description: "Read what's INSIDE one winning ad: the spoken transcript, the on-screen text, and the ad copy. Use this after `search`/`winners`/`feed` return a shortlist \u2014 pass an ad_id to understand a reference before reproducing it. Add --full for speech, pacing, and soundtrack detail. Video ads carry the transcript/on-screen text; static ads carry only the copy.",
29792
+ args: {
29793
+ "ad-id": { type: "string", description: "The ad id to open (from search/winners/feed results)", required: true },
29794
+ platform: {
29795
+ type: "string",
29796
+ description: "Which platform the ad id is on: meta|linkedin (default meta)",
29797
+ required: false
29798
+ }
29799
+ }
29800
+ });
29801
+ var contentCommand = defineCommand167({
29802
+ meta: {
29803
+ name: "content",
29804
+ description: "Read the transcript + on-screen text + copy of one winning ad. Example: baker winning-ads content adg_123 --platform meta --full --output md"
29805
+ },
29806
+ args: {
29807
+ "ad-id": { type: "positional", description: "Ad id (from search/winners/feed)", required: true },
29808
+ platform: {
29809
+ type: "string",
29810
+ description: "Platform the ad id is on: meta|linkedin (default meta)",
29811
+ required: false
29812
+ },
29813
+ output: { type: "string", description: "Output format: json|files|md", required: false, default: "json" },
29814
+ fields: { type: "string", description: "Comma-separated field names to include", required: false },
29815
+ full: {
29816
+ type: "boolean",
29817
+ description: "Include speech, pacing, and soundtrack detail",
29818
+ required: false,
29819
+ default: false
29820
+ }
29821
+ },
29822
+ run: async ({ args }) => {
29823
+ try {
29824
+ const params = { id: String(args["ad-id"]) };
29825
+ if (args.platform) {
29826
+ params.platform = String(args.platform);
29827
+ }
29828
+ const data = await apiGet("/api/ad-library/ad-content", params);
29829
+ const output = args.output || "json";
29830
+ const full = args.full;
29831
+ const ad = data?.ad ?? {};
29832
+ if (output === "json") {
29833
+ writeJson({ ok: true, data: { ad: adContentNormalizer(ad, full) } });
29834
+ return;
29835
+ }
29836
+ writeOutput(
29837
+ { ok: true, data: ad },
29838
+ output,
29839
+ args.fields ? args.fields.split(",") : void 0,
29840
+ full,
29841
+ adContentNormalizer
29842
+ );
29843
+ } catch (err) {
29844
+ reportError(err);
29845
+ }
29846
+ }
29847
+ });
29848
+
29849
+ // src/commands/winning-ads/feed.ts
29850
+ import { defineCommand as defineCommand168 } from "citty";
29634
29851
  function buildFeedParams(input) {
29635
29852
  const params = {};
29636
29853
  const advertiser = splitList(input.advertiser);
@@ -29682,7 +29899,7 @@ registerSchema({
29682
29899
  format: { type: "string", description: "Comma-separated formats to include (e.g. static,video)", required: false }
29683
29900
  }
29684
29901
  });
29685
- var feedCommand = defineCommand167({
29902
+ var feedCommand = defineCommand168({
29686
29903
  meta: {
29687
29904
  name: "feed",
29688
29905
  description: "Winners across every brand you follow (browse, then trim per advertiser). Example: baker winning-ads feed --per-advertiser 5 --output md"
@@ -29767,7 +29984,7 @@ var feedCommand = defineCommand167({
29767
29984
  });
29768
29985
 
29769
29986
  // src/commands/winning-ads/follow.ts
29770
- import { defineCommand as defineCommand168 } from "citty";
29987
+ import { defineCommand as defineCommand169 } from "citty";
29771
29988
  var PLATFORMS = ["meta", "linkedin"];
29772
29989
  registerSchema({
29773
29990
  command: "winning-ads.follow",
@@ -29782,7 +29999,7 @@ registerSchema({
29782
29999
  label: { type: "string", description: "Optional display label (defaults to the resolved name)", required: false }
29783
30000
  }
29784
30001
  });
29785
- var followCommand = defineCommand168({
30002
+ var followCommand = defineCommand169({
29786
30003
  meta: {
29787
30004
  name: "follow",
29788
30005
  description: 'Follow a brand to track ALL its ads \u2014 every platform and country. --platform is how we read your input, not a limit. A domain tracks both Meta + LinkedIn. Example: baker winning-ads follow "deel.com" --platform meta'
@@ -29829,7 +30046,7 @@ var followCommand = defineCommand168({
29829
30046
  });
29830
30047
 
29831
30048
  // src/commands/winning-ads/follow-competitors.ts
29832
- import { defineCommand as defineCommand169 } from "citty";
30049
+ import { defineCommand as defineCommand170 } from "citty";
29833
30050
  var PLATFORMS2 = ["meta", "linkedin"];
29834
30051
  var BATCH_TIMEOUT_MS = 3e5;
29835
30052
  function buildFollowBatchBody(input) {
@@ -29862,7 +30079,7 @@ registerSchema({
29862
30079
  }
29863
30080
  }
29864
30081
  });
29865
- var followCompetitorsCommand = defineCommand169({
30082
+ var followCompetitorsCommand = defineCommand170({
29866
30083
  meta: {
29867
30084
  name: "follow-competitors",
29868
30085
  description: 'Follow many brands at once by domain \u2014 add every competitor in one call. Example: baker winning-ads follow-competitors "deel.com,notion.so,hubspot.com"'
@@ -29937,7 +30154,7 @@ var followCompetitorsCommand = defineCommand169({
29937
30154
  });
29938
30155
 
29939
30156
  // src/commands/winning-ads/following.ts
29940
- import { defineCommand as defineCommand170 } from "citty";
30157
+ import { defineCommand as defineCommand171 } from "citty";
29941
30158
  registerSchema({
29942
30159
  command: "winning-ads.following",
29943
30160
  description: "List the brands you follow in your ad-dna library, with each one's status (ready vs still adding) and cached ad counts.",
@@ -29970,7 +30187,7 @@ function followingNormalizer(record, full) {
29970
30187
  platforms: Array.isArray(record.platforms) ? record.platforms : []
29971
30188
  };
29972
30189
  }
29973
- var followingCommand = defineCommand170({
30190
+ var followingCommand = defineCommand171({
29974
30191
  meta: {
29975
30192
  name: "following",
29976
30193
  description: "List brands you follow, with status (ready / adding\u2026) and cached counts. Example: baker winning-ads following --output md"
@@ -30005,7 +30222,7 @@ var followingCommand = defineCommand170({
30005
30222
  });
30006
30223
 
30007
30224
  // src/commands/winning-ads/patterns.ts
30008
- import { defineCommand as defineCommand171 } from "citty";
30225
+ import { defineCommand as defineCommand172 } from "citty";
30009
30226
  registerSchema({
30010
30227
  command: "winning-ads.patterns",
30011
30228
  description: "Mine what separates two cohorts of ads: pass a comma-list of winning ad ids (--winners) and a comma-list of weaker ad ids (--duds). Returns the discriminating DNA fields.",
@@ -30044,7 +30261,7 @@ function discriminatorRow(record) {
30044
30261
  top_values_duds: Array.isArray(record.top_values_b) ? record.top_values_b.join(", ") : ""
30045
30262
  };
30046
30263
  }
30047
- var patternsCommand = defineCommand171({
30264
+ var patternsCommand = defineCommand172({
30048
30265
  meta: {
30049
30266
  name: "patterns",
30050
30267
  description: "Discover what separates winning ads from weak ones. Example: baker winning-ads patterns --winners a_1,a_2,a_3 --duds a_9,a_8 --output md"
@@ -30100,7 +30317,7 @@ var patternsCommand = defineCommand171({
30100
30317
  });
30101
30318
 
30102
30319
  // src/commands/winning-ads/search.ts
30103
- import { defineCommand as defineCommand172 } from "citty";
30320
+ import { defineCommand as defineCommand173 } from "citty";
30104
30321
  registerSchema({
30105
30322
  command: "winning-ads.search",
30106
30323
  description: "Search the ad-dna corpus of scored winning ads. Returns a lean shortlist (advertiser, summary, scores, media_url) to pick a reference to reproduce.",
@@ -30208,7 +30425,7 @@ function buildSearchBody(args) {
30208
30425
  }
30209
30426
  return body;
30210
30427
  }
30211
- var searchCommand4 = defineCommand172({
30428
+ var searchCommand4 = defineCommand173({
30212
30429
  meta: {
30213
30430
  name: "search",
30214
30431
  description: "Search winning reference ads. Example: baker winning-ads search 'B2B SaaS before/after AI automation' --platform meta --format static --winner-category winner --exclude-advertiser adv_123 --output md"
@@ -30323,7 +30540,7 @@ var searchCommand4 = defineCommand172({
30323
30540
  });
30324
30541
 
30325
30542
  // src/commands/winning-ads/seeds.ts
30326
- import { defineCommand as defineCommand173 } from "citty";
30543
+ import { defineCommand as defineCommand174 } from "citty";
30327
30544
  function leanRow(r) {
30328
30545
  return {
30329
30546
  key: r.key,
@@ -30351,7 +30568,7 @@ function makeSeedCommand(opts) {
30351
30568
  limit: { type: "number", description: "Max keys 1-100 (default 20)", required: false, default: 20 }
30352
30569
  }
30353
30570
  });
30354
- return defineCommand173({
30571
+ return defineCommand174({
30355
30572
  meta: { name: opts.name, description: opts.description },
30356
30573
  args: {
30357
30574
  platform: { type: "string", description: "Single platform to segment on", required: false },
@@ -30400,7 +30617,7 @@ var formatsCommand = makeSeedCommand({
30400
30617
  });
30401
30618
 
30402
30619
  // src/commands/winning-ads/unfollow.ts
30403
- import { defineCommand as defineCommand174 } from "citty";
30620
+ import { defineCommand as defineCommand175 } from "citty";
30404
30621
  registerSchema({
30405
30622
  command: "winning-ads.unfollow",
30406
30623
  description: "Stop following a brand \u2014 removes it from your ad-dna library by advertiser id.",
@@ -30408,7 +30625,7 @@ registerSchema({
30408
30625
  advertiser: { type: "string", description: "Advertiser id to unfollow", required: true }
30409
30626
  }
30410
30627
  });
30411
- var unfollowCommand = defineCommand174({
30628
+ var unfollowCommand = defineCommand175({
30412
30629
  meta: {
30413
30630
  name: "unfollow",
30414
30631
  description: "Stop following a brand by advertiser id. Example: baker winning-ads unfollow adv_123"
@@ -30429,7 +30646,7 @@ var unfollowCommand = defineCommand174({
30429
30646
  });
30430
30647
 
30431
30648
  // src/commands/winning-ads/winners.ts
30432
- import { defineCommand as defineCommand175 } from "citty";
30649
+ import { defineCommand as defineCommand176 } from "citty";
30433
30650
  registerSchema({
30434
30651
  command: "winning-ads.winners",
30435
30652
  description: "Top winning ads for one advertiser id (from `advertisers` or `following`). Returns lean winner cards; add --full for DNA + longevity.",
@@ -30439,7 +30656,7 @@ registerSchema({
30439
30656
  platform: { type: "string", description: "Filter to a single platform: meta|linkedin", required: false }
30440
30657
  }
30441
30658
  });
30442
- var winnersCommand = defineCommand175({
30659
+ var winnersCommand = defineCommand176({
30443
30660
  meta: {
30444
30661
  name: "winners",
30445
30662
  description: "Top winning ads for a specific advertiser id. Example: baker winning-ads winners adv_123 --top 15 --output md"
@@ -30489,7 +30706,7 @@ var winnersCommand = defineCommand175({
30489
30706
  });
30490
30707
 
30491
30708
  // src/commands/winning-ads/index.ts
30492
- var winningAdsCommand = defineCommand176({
30709
+ var winningAdsCommand = defineCommand177({
30493
30710
  meta: {
30494
30711
  name: "winning-ads",
30495
30712
  description: `Search the ad-dna corpus of scored "winning" ads for reference creatives to reproduce, and manage the brands your library tracks. Proxied through the Baker backend (BAKER_API_KEY) \u2014 no separate token needed.
@@ -30504,6 +30721,7 @@ Subcommands:
30504
30721
  baker winning-ads following \u2014 list brands you follow (status + counts)
30505
30722
  baker winning-ads feed \u2014 winners across EVERY brand you follow; trim with --advertiser
30506
30723
  baker winning-ads winners <advertiser> \u2014 top winners for one advertiser id
30724
+ baker winning-ads content <ad-id> \u2014 read ONE ad's transcript + on-screen text + copy (understand a reference before reproducing it)
30507
30725
  baker winning-ads unfollow <advertiser> \u2014 stop following a brand
30508
30726
  baker winning-ads brief \u2014 creative brief grounded in similar winners
30509
30727
  baker winning-ads patterns --winners \u2026 --duds \u2026 \u2014 what separates a winning cohort from a weak one
@@ -30519,6 +30737,7 @@ Examples:
30519
30737
  baker winning-ads feed --per-advertiser 5 --output md
30520
30738
  baker winning-ads feed --advertiser adv_123,adv_456 --platform meta --output md
30521
30739
  baker winning-ads winners adv_123 --top 15 --output md
30740
+ baker winning-ads content adg_123 --full --output md
30522
30741
  baker winning-ads hooks --platform meta --awareness problem_aware --industry saas --output md
30523
30742
  baker winning-ads search "fintech onboarding" --hook-archetype callout --winner-category winner --output md
30524
30743
  baker winning-ads patterns --winners a_1,a_2 --duds a_9,a_8 --output md
@@ -30532,6 +30751,7 @@ Full guide: __tooling__/docs/tools/baker/winning-ads.md`
30532
30751
  following: followingCommand,
30533
30752
  feed: feedCommand,
30534
30753
  winners: winnersCommand,
30754
+ content: contentCommand,
30535
30755
  unfollow: unfollowCommand,
30536
30756
  brief: briefCommand,
30537
30757
  patterns: patternsCommand,
@@ -30558,7 +30778,7 @@ function getCliVersion() {
30558
30778
  }
30559
30779
 
30560
30780
  // src/cli.ts
30561
- var main = defineCommand177({
30781
+ var main = defineCommand178({
30562
30782
  meta: {
30563
30783
  name: "baker",
30564
30784
  version: getCliVersion(),