@koda-sl/baker-cli 0.387.0 → 0.389.0-dev.8065ad8f0

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.
Files changed (967) hide show
  1. package/README.md +1090 -211
  2. package/canvas/tiktok-captions-composition/index.html +5 -6
  3. package/canvas/tiktok-captions-composition/meta.json +5 -0
  4. package/dist/account-CLOKMTTC.js +49 -0
  5. package/dist/account-CLOKMTTC.js.map +1 -0
  6. package/dist/account-V5Y2QHAK.js +79 -0
  7. package/dist/account-V5Y2QHAK.js.map +1 -0
  8. package/dist/account-XFK6VX2R.js +80 -0
  9. package/dist/account-XFK6VX2R.js.map +1 -0
  10. package/dist/account-reads-S5VXX2QU.js +218 -0
  11. package/dist/account-reads-S5VXX2QU.js.map +1 -0
  12. package/dist/account-writes-Y7EQXDXF.js +66 -0
  13. package/dist/account-writes-Y7EQXDXF.js.map +1 -0
  14. package/dist/accounts-7YD457YN.js +114 -0
  15. package/dist/accounts-7YD457YN.js.map +1 -0
  16. package/dist/accounts-BJ3X5JQT.js +115 -0
  17. package/dist/accounts-BJ3X5JQT.js.map +1 -0
  18. package/dist/accounts-LOJ4WLIX.js +77 -0
  19. package/dist/accounts-LOJ4WLIX.js.map +1 -0
  20. package/dist/accounts-RYBDLQ6H.js +75 -0
  21. package/dist/accounts-RYBDLQ6H.js.map +1 -0
  22. package/dist/accounts-UO4XBWM2.js +611 -0
  23. package/dist/accounts-UO4XBWM2.js.map +1 -0
  24. package/dist/actions-3AW3R63U.js +55 -0
  25. package/dist/actions-3AW3R63U.js.map +1 -0
  26. package/dist/active-entities-R6GZIO6Z.js +103 -0
  27. package/dist/active-entities-R6GZIO6Z.js.map +1 -0
  28. package/dist/activities-ESBF3GFS.js +74 -0
  29. package/dist/activities-ESBF3GFS.js.map +1 -0
  30. package/dist/ad-group-writes-I225OYDZ.js +170 -0
  31. package/dist/ad-group-writes-I225OYDZ.js.map +1 -0
  32. package/dist/ad-groups-O35MWNF6.js +68 -0
  33. package/dist/ad-groups-O35MWNF6.js.map +1 -0
  34. package/dist/ad-writes-SS5YZO5W.js +174 -0
  35. package/dist/ad-writes-SS5YZO5W.js.map +1 -0
  36. package/dist/add-43DUOVNR.js +104 -0
  37. package/dist/add-43DUOVNR.js.map +1 -0
  38. package/dist/add-IE5WDELB.js +302 -0
  39. package/dist/add-IE5WDELB.js.map +1 -0
  40. package/dist/add-QZHAWUBA.js +97 -0
  41. package/dist/add-QZHAWUBA.js.map +1 -0
  42. package/dist/add-TGQIVVXH.js +105 -0
  43. package/dist/add-TGQIVVXH.js.map +1 -0
  44. package/dist/add-accounts-6QYPOPR2.js +91 -0
  45. package/dist/add-accounts-6QYPOPR2.js.map +1 -0
  46. package/dist/ads-AUSRB4GU.js +128 -0
  47. package/dist/ads-AUSRB4GU.js.map +1 -0
  48. package/dist/ads-COYOTEKE.js +77 -0
  49. package/dist/ads-COYOTEKE.js.map +1 -0
  50. package/dist/ads-MALHYD2D.js +40 -0
  51. package/dist/ads-MALHYD2D.js.map +1 -0
  52. package/dist/ads-XZCMSPM5.js +113 -0
  53. package/dist/ads-XZCMSPM5.js.map +1 -0
  54. package/dist/adsets-AKR75THA.js +176 -0
  55. package/dist/adsets-AKR75THA.js.map +1 -0
  56. package/dist/advertisers-52FMMBTQ.js +120 -0
  57. package/dist/advertisers-52FMMBTQ.js.map +1 -0
  58. package/dist/advertisers-BCGKUMEH.js +125 -0
  59. package/dist/advertisers-BCGKUMEH.js.map +1 -0
  60. package/dist/analytics-JPDBKETM.js +1980 -0
  61. package/dist/analytics-JPDBKETM.js.map +1 -0
  62. package/dist/analytics-YVQ6H4BG.js +433 -0
  63. package/dist/analytics-YVQ6H4BG.js.map +1 -0
  64. package/dist/animate-JXIT77Y5.js +480 -0
  65. package/dist/animate-JXIT77Y5.js.map +1 -0
  66. package/dist/assign-66NTWTR2.js +77 -0
  67. package/dist/assign-66NTWTR2.js.map +1 -0
  68. package/dist/audience-size-LVQJWQAS.js +94 -0
  69. package/dist/audience-size-LVQJWQAS.js.map +1 -0
  70. package/dist/audiences-DC3IOG34.js +85 -0
  71. package/dist/audiences-DC3IOG34.js.map +1 -0
  72. package/dist/audiences-NEW3LLQR.js +268 -0
  73. package/dist/audiences-NEW3LLQR.js.map +1 -0
  74. package/dist/audiences-NYGMBGQ3.js +79 -0
  75. package/dist/audiences-NYGMBGQ3.js.map +1 -0
  76. package/dist/audit-UIPMTI5C.js +88 -0
  77. package/dist/audit-UIPMTI5C.js.map +1 -0
  78. package/dist/audit-XGN2OFLN.js +151 -0
  79. package/dist/audit-XGN2OFLN.js.map +1 -0
  80. package/dist/autocomplete-LVPDV3JE.js +111 -0
  81. package/dist/autocomplete-LVPDV3JE.js.map +1 -0
  82. package/dist/avatars-S6NSLMJK.js +38 -0
  83. package/dist/avatars-S6NSLMJK.js.map +1 -0
  84. package/dist/bid-pricing-2LYULXU2.js +99 -0
  85. package/dist/bid-pricing-2LYULXU2.js.map +1 -0
  86. package/dist/brand-WWHUZ72R.js +22 -0
  87. package/dist/brand-WWHUZ72R.js.map +1 -0
  88. package/dist/brief-XY3TFWBC.js +492 -0
  89. package/dist/brief-XY3TFWBC.js.map +1 -0
  90. package/dist/businesses-J7OP7MSK.js +53 -0
  91. package/dist/businesses-J7OP7MSK.js.map +1 -0
  92. package/dist/campaign-groups-N4TZ2F3L.js +95 -0
  93. package/dist/campaign-groups-N4TZ2F3L.js.map +1 -0
  94. package/dist/campaign-writes-5XA727KI.js +170 -0
  95. package/dist/campaign-writes-5XA727KI.js.map +1 -0
  96. package/dist/campaign-writes-7YFRQIQT.js +165 -0
  97. package/dist/campaign-writes-7YFRQIQT.js.map +1 -0
  98. package/dist/campaigns-JPYNVFQI.js +76 -0
  99. package/dist/campaigns-JPYNVFQI.js.map +1 -0
  100. package/dist/campaigns-L43ZUKAI.js +107 -0
  101. package/dist/campaigns-L43ZUKAI.js.map +1 -0
  102. package/dist/campaigns-OD7Z332M.js +109 -0
  103. package/dist/campaigns-OD7Z332M.js.map +1 -0
  104. package/dist/campaigns-R4DXIR2Q.js +110 -0
  105. package/dist/campaigns-R4DXIR2Q.js.map +1 -0
  106. package/dist/canvas-J7SLIVL7.js +46 -0
  107. package/dist/canvas-J7SLIVL7.js.map +1 -0
  108. package/dist/capabilities-FVTZAFPV.js +144 -0
  109. package/dist/capabilities-FVTZAFPV.js.map +1 -0
  110. package/dist/cards-7H32TNMO.js +85 -0
  111. package/dist/cards-7H32TNMO.js.map +1 -0
  112. package/dist/catalog-OTICZPJA.js +64 -0
  113. package/dist/catalog-OTICZPJA.js.map +1 -0
  114. package/dist/catalogs-ABCQLCDU.js +128 -0
  115. package/dist/catalogs-ABCQLCDU.js.map +1 -0
  116. package/dist/changes-7ZIGNUW6.js +135 -0
  117. package/dist/changes-7ZIGNUW6.js.map +1 -0
  118. package/dist/chatgpt-B3NHRJUE.js +47 -0
  119. package/dist/chatgpt-B3NHRJUE.js.map +1 -0
  120. package/dist/chats-E7DBRHBF.js +262 -0
  121. package/dist/chats-E7DBRHBF.js.map +1 -0
  122. package/dist/chunk-2CV7LX23.js +474 -0
  123. package/dist/chunk-2CV7LX23.js.map +1 -0
  124. package/dist/chunk-2HCI5BXB.js +479 -0
  125. package/dist/chunk-2HCI5BXB.js.map +1 -0
  126. package/dist/chunk-2SQJL7ZN.js +56 -0
  127. package/dist/chunk-2SQJL7ZN.js.map +1 -0
  128. package/dist/chunk-2Y5V23PU.js +29 -0
  129. package/dist/chunk-2Y5V23PU.js.map +1 -0
  130. package/dist/chunk-32CPGJPL.js +149 -0
  131. package/dist/chunk-32CPGJPL.js.map +1 -0
  132. package/dist/chunk-3ACMDRSE.js +39 -0
  133. package/dist/chunk-3ACMDRSE.js.map +1 -0
  134. package/dist/chunk-3CIXGQJS.js +150 -0
  135. package/dist/chunk-3CIXGQJS.js.map +1 -0
  136. package/dist/chunk-3U7TDBIB.js +275 -0
  137. package/dist/chunk-3U7TDBIB.js.map +1 -0
  138. package/dist/chunk-3UDSYYND.js +42 -0
  139. package/dist/chunk-3UDSYYND.js.map +1 -0
  140. package/dist/chunk-3ZWWXAQZ.js +150 -0
  141. package/dist/chunk-3ZWWXAQZ.js.map +1 -0
  142. package/dist/chunk-4MH6LONO.js +274 -0
  143. package/dist/chunk-4MH6LONO.js.map +1 -0
  144. package/dist/chunk-4NZ47VXO.js +137 -0
  145. package/dist/chunk-4NZ47VXO.js.map +1 -0
  146. package/dist/chunk-564AEU2Q.js +541 -0
  147. package/dist/chunk-564AEU2Q.js.map +1 -0
  148. package/dist/chunk-5HYSKH77.js +315 -0
  149. package/dist/chunk-5HYSKH77.js.map +1 -0
  150. package/dist/chunk-5MBPIONY.js +29 -0
  151. package/dist/chunk-5MBPIONY.js.map +1 -0
  152. package/dist/chunk-5OVOLYY3.js +74 -0
  153. package/dist/chunk-5OVOLYY3.js.map +1 -0
  154. package/dist/chunk-66USKCVY.js +4410 -0
  155. package/dist/chunk-66USKCVY.js.map +1 -0
  156. package/dist/chunk-6I7SYEYN.js +166 -0
  157. package/dist/chunk-6I7SYEYN.js.map +1 -0
  158. package/dist/chunk-6O7ISBC7.js +65 -0
  159. package/dist/chunk-6O7ISBC7.js.map +1 -0
  160. package/dist/chunk-752S7JF6.js +35 -0
  161. package/dist/chunk-752S7JF6.js.map +1 -0
  162. package/dist/chunk-77DN7R35.js +228 -0
  163. package/dist/chunk-77DN7R35.js.map +1 -0
  164. package/dist/chunk-7F7MFMLI.js +175 -0
  165. package/dist/chunk-7F7MFMLI.js.map +1 -0
  166. package/dist/chunk-7THBBTJJ.js +183 -0
  167. package/dist/chunk-7THBBTJJ.js.map +1 -0
  168. package/dist/chunk-7XU3HKMX.js +1599 -0
  169. package/dist/chunk-7XU3HKMX.js.map +1 -0
  170. package/dist/chunk-A2CBZLQ6.js +111 -0
  171. package/dist/chunk-A2CBZLQ6.js.map +1 -0
  172. package/dist/chunk-AFJGYJ7Z.js +45 -0
  173. package/dist/chunk-AFJGYJ7Z.js.map +1 -0
  174. package/dist/chunk-AR7PU7BB.js +76 -0
  175. package/dist/chunk-AR7PU7BB.js.map +1 -0
  176. package/dist/chunk-ASL7JFBD.js +96 -0
  177. package/dist/chunk-ASL7JFBD.js.map +1 -0
  178. package/dist/chunk-AUSKKFVH.js +84 -0
  179. package/dist/chunk-AUSKKFVH.js.map +1 -0
  180. package/dist/chunk-B52DCYL2.js +159 -0
  181. package/dist/chunk-B52DCYL2.js.map +1 -0
  182. package/dist/{chunk-XHC4LMK4.js → chunk-BVM5IRZW.js} +16 -8
  183. package/dist/chunk-BVM5IRZW.js.map +1 -0
  184. package/dist/chunk-CAMX6OHF.js +3182 -0
  185. package/dist/chunk-CAMX6OHF.js.map +1 -0
  186. package/dist/chunk-CYBLKMXQ.js +310 -0
  187. package/dist/chunk-CYBLKMXQ.js.map +1 -0
  188. package/dist/chunk-CZENWCBS.js +333 -0
  189. package/dist/chunk-CZENWCBS.js.map +1 -0
  190. package/dist/chunk-D5FY4FAK.js +24 -0
  191. package/dist/chunk-D5FY4FAK.js.map +1 -0
  192. package/dist/chunk-DGCUPJZD.js +39 -0
  193. package/dist/chunk-DGCUPJZD.js.map +1 -0
  194. package/dist/chunk-DNCLWSHO.js +41 -0
  195. package/dist/chunk-DNCLWSHO.js.map +1 -0
  196. package/dist/chunk-DX4GIDO7.js +296 -0
  197. package/dist/chunk-DX4GIDO7.js.map +1 -0
  198. package/dist/chunk-EDLZGTGN.js +216 -0
  199. package/dist/chunk-EDLZGTGN.js.map +1 -0
  200. package/dist/chunk-EDNKALM6.js +304 -0
  201. package/dist/chunk-EDNKALM6.js.map +1 -0
  202. package/dist/chunk-EDNX3AOS.js +76 -0
  203. package/dist/chunk-EDNX3AOS.js.map +1 -0
  204. package/dist/chunk-EF5DP6EB.js +52 -0
  205. package/dist/chunk-EF5DP6EB.js.map +1 -0
  206. package/dist/chunk-ERSX3MKH.js +34 -0
  207. package/dist/chunk-ERSX3MKH.js.map +1 -0
  208. package/dist/chunk-F3IJSR7O.js +346 -0
  209. package/dist/chunk-F3IJSR7O.js.map +1 -0
  210. package/dist/chunk-FKOECEVC.js +126 -0
  211. package/dist/chunk-FKOECEVC.js.map +1 -0
  212. package/dist/chunk-FWT55B3O.js +509 -0
  213. package/dist/chunk-FWT55B3O.js.map +1 -0
  214. package/dist/chunk-FYWWTMXP.js +2902 -0
  215. package/dist/chunk-FYWWTMXP.js.map +1 -0
  216. package/dist/chunk-FZGCAW46.js +64 -0
  217. package/dist/chunk-FZGCAW46.js.map +1 -0
  218. package/dist/chunk-G7AFVTUS.js +106 -0
  219. package/dist/chunk-G7AFVTUS.js.map +1 -0
  220. package/dist/chunk-GIW7ABMM.js +74 -0
  221. package/dist/chunk-GIW7ABMM.js.map +1 -0
  222. package/dist/chunk-GIWHHCD6.js +145 -0
  223. package/dist/chunk-GIWHHCD6.js.map +1 -0
  224. package/dist/chunk-GRQ2AXDX.js +9 -0
  225. package/dist/chunk-GRQ2AXDX.js.map +1 -0
  226. package/dist/chunk-GXDLWBOY.js +73 -0
  227. package/dist/chunk-GXDLWBOY.js.map +1 -0
  228. package/dist/chunk-GYABNLWW.js +629 -0
  229. package/dist/chunk-GYABNLWW.js.map +1 -0
  230. package/dist/chunk-H7MLHG2I.js +94 -0
  231. package/dist/chunk-H7MLHG2I.js.map +1 -0
  232. package/dist/chunk-H7XVKLIY.js +206 -0
  233. package/dist/chunk-H7XVKLIY.js.map +1 -0
  234. package/dist/chunk-HAE4WWFQ.js +455 -0
  235. package/dist/chunk-HAE4WWFQ.js.map +1 -0
  236. package/dist/chunk-HDHWIEJJ.js +136 -0
  237. package/dist/chunk-HDHWIEJJ.js.map +1 -0
  238. package/dist/chunk-HJTIBTF7.js +98 -0
  239. package/dist/chunk-HJTIBTF7.js.map +1 -0
  240. package/dist/{chunk-VZEDAFAR.js → chunk-HLXWKMTK.js} +47 -8
  241. package/dist/chunk-HLXWKMTK.js.map +1 -0
  242. package/dist/{chunk-MF7YA62W.js → chunk-HREWXNTO.js} +10 -5
  243. package/dist/chunk-HREWXNTO.js.map +1 -0
  244. package/dist/chunk-HYXKRNRV.js +176 -0
  245. package/dist/chunk-HYXKRNRV.js.map +1 -0
  246. package/dist/chunk-I3DIZWN5.js +97 -0
  247. package/dist/chunk-I3DIZWN5.js.map +1 -0
  248. package/dist/chunk-IJMWEO4P.js +57 -0
  249. package/dist/chunk-IJMWEO4P.js.map +1 -0
  250. package/dist/chunk-IOCSDFF2.js +23992 -0
  251. package/dist/chunk-IOCSDFF2.js.map +1 -0
  252. package/dist/chunk-IVIJJXXT.js +62 -0
  253. package/dist/chunk-IVIJJXXT.js.map +1 -0
  254. package/dist/chunk-J57GWTPK.js +106 -0
  255. package/dist/chunk-J57GWTPK.js.map +1 -0
  256. package/dist/chunk-JPK34QPS.js +134 -0
  257. package/dist/chunk-JPK34QPS.js.map +1 -0
  258. package/dist/chunk-JRCFCIE3.js +188 -0
  259. package/dist/chunk-JRCFCIE3.js.map +1 -0
  260. package/dist/chunk-JUAIGJ2A.js +76 -0
  261. package/dist/chunk-JUAIGJ2A.js.map +1 -0
  262. package/dist/chunk-K5OQES4V.js +58 -0
  263. package/dist/chunk-K5OQES4V.js.map +1 -0
  264. package/dist/chunk-KBS5GH4T.js +1626 -0
  265. package/dist/chunk-KBS5GH4T.js.map +1 -0
  266. package/dist/chunk-KIGGMKAE.js +66 -0
  267. package/dist/chunk-KIGGMKAE.js.map +1 -0
  268. package/dist/chunk-KKJ4KJCW.js +29 -0
  269. package/dist/chunk-KKJ4KJCW.js.map +1 -0
  270. package/dist/chunk-KLK6KQLQ.js +55 -0
  271. package/dist/chunk-KLK6KQLQ.js.map +1 -0
  272. package/dist/chunk-KXGG6EQM.js +382 -0
  273. package/dist/chunk-KXGG6EQM.js.map +1 -0
  274. package/dist/chunk-KXRKPZZY.js +169 -0
  275. package/dist/chunk-KXRKPZZY.js.map +1 -0
  276. package/dist/chunk-KZUHRZ2C.js +5187 -0
  277. package/dist/chunk-KZUHRZ2C.js.map +1 -0
  278. package/dist/chunk-L45A3IYV.js +1079 -0
  279. package/dist/chunk-L45A3IYV.js.map +1 -0
  280. package/dist/chunk-LA6PPSNA.js +65 -0
  281. package/dist/chunk-LA6PPSNA.js.map +1 -0
  282. package/dist/chunk-LEIQAHWF.js +25 -0
  283. package/dist/chunk-LEIQAHWF.js.map +1 -0
  284. package/dist/chunk-LERWCRY5.js +297 -0
  285. package/dist/chunk-LERWCRY5.js.map +1 -0
  286. package/dist/chunk-LLPFY3NI.js +207 -0
  287. package/dist/chunk-LLPFY3NI.js.map +1 -0
  288. package/dist/chunk-M6UNISGO.js +66 -0
  289. package/dist/chunk-M6UNISGO.js.map +1 -0
  290. package/dist/chunk-MDATIJPM.js +92 -0
  291. package/dist/chunk-MDATIJPM.js.map +1 -0
  292. package/dist/chunk-MGKHFGLV.js +75 -0
  293. package/dist/chunk-MGKHFGLV.js.map +1 -0
  294. package/dist/chunk-MZWC6HHA.js +455 -0
  295. package/dist/chunk-MZWC6HHA.js.map +1 -0
  296. package/dist/chunk-N3XJ434P.js +197 -0
  297. package/dist/chunk-N3XJ434P.js.map +1 -0
  298. package/dist/chunk-NFSLRRIE.js +77 -0
  299. package/dist/chunk-NFSLRRIE.js.map +1 -0
  300. package/dist/chunk-NHU33T2S.js +60 -0
  301. package/dist/chunk-NHU33T2S.js.map +1 -0
  302. package/dist/chunk-NJEZM4OB.js +51 -0
  303. package/dist/chunk-NJEZM4OB.js.map +1 -0
  304. package/dist/chunk-O6QKCJE4.js +455 -0
  305. package/dist/chunk-O6QKCJE4.js.map +1 -0
  306. package/dist/chunk-OFFAMC5H.js +225 -0
  307. package/dist/chunk-OFFAMC5H.js.map +1 -0
  308. package/dist/chunk-OHGTCFFE.js +57 -0
  309. package/dist/chunk-OHGTCFFE.js.map +1 -0
  310. package/dist/chunk-P4NSJS54.js +20 -0
  311. package/dist/chunk-P4NSJS54.js.map +1 -0
  312. package/dist/chunk-P5T7OZKV.js +26 -0
  313. package/dist/chunk-P5T7OZKV.js.map +1 -0
  314. package/dist/chunk-PFBQERWX.js +10 -0
  315. package/dist/chunk-PFBQERWX.js.map +1 -0
  316. package/dist/chunk-PUPEGICD.js +103 -0
  317. package/dist/chunk-PUPEGICD.js.map +1 -0
  318. package/dist/chunk-Q6ULQMWG.js +41 -0
  319. package/dist/chunk-Q6ULQMWG.js.map +1 -0
  320. package/dist/chunk-QO23YHLK.js +84 -0
  321. package/dist/chunk-QO23YHLK.js.map +1 -0
  322. package/dist/chunk-QSGIWV4P.js +337 -0
  323. package/dist/chunk-QSGIWV4P.js.map +1 -0
  324. package/dist/chunk-R4Y5P3PB.js +19 -0
  325. package/dist/chunk-R4Y5P3PB.js.map +1 -0
  326. package/dist/chunk-R5OQM7FF.js +179 -0
  327. package/dist/chunk-R5OQM7FF.js.map +1 -0
  328. package/dist/chunk-RODLBWIR.js +592 -0
  329. package/dist/chunk-RODLBWIR.js.map +1 -0
  330. package/dist/chunk-RY7TYEIX.js +504 -0
  331. package/dist/chunk-RY7TYEIX.js.map +1 -0
  332. package/dist/chunk-S4LZ7KTG.js +160 -0
  333. package/dist/chunk-S4LZ7KTG.js.map +1 -0
  334. package/dist/chunk-SHTESUB6.js +1442 -0
  335. package/dist/chunk-SHTESUB6.js.map +1 -0
  336. package/dist/chunk-SJH4EOZH.js +23 -0
  337. package/dist/chunk-SJH4EOZH.js.map +1 -0
  338. package/dist/chunk-SMIENBNX.js +6212 -0
  339. package/dist/chunk-SMIENBNX.js.map +1 -0
  340. package/dist/chunk-T7GT22KA.js +494 -0
  341. package/dist/chunk-T7GT22KA.js.map +1 -0
  342. package/dist/chunk-T7M4WFNV.js +172 -0
  343. package/dist/chunk-T7M4WFNV.js.map +1 -0
  344. package/dist/chunk-TIFVHGQL.js +6683 -0
  345. package/dist/chunk-TIFVHGQL.js.map +1 -0
  346. package/dist/chunk-TMIDUSOR.js +665 -0
  347. package/dist/chunk-TMIDUSOR.js.map +1 -0
  348. package/dist/{chunk-WLDM2TRF.js → chunk-TPXI2GCW.js} +4 -4
  349. package/dist/chunk-U6VRBDNQ.js +1830 -0
  350. package/dist/chunk-U6VRBDNQ.js.map +1 -0
  351. package/dist/chunk-U7BBXM67.js +1743 -0
  352. package/dist/chunk-U7BBXM67.js.map +1 -0
  353. package/dist/chunk-UI6BKYQD.js +225 -0
  354. package/dist/chunk-UI6BKYQD.js.map +1 -0
  355. package/dist/chunk-UOI5S62H.js +846 -0
  356. package/dist/chunk-UOI5S62H.js.map +1 -0
  357. package/dist/chunk-USL5ARRE.js +26 -0
  358. package/dist/chunk-USL5ARRE.js.map +1 -0
  359. package/dist/chunk-UYC45JNS.js +139 -0
  360. package/dist/chunk-UYC45JNS.js.map +1 -0
  361. package/dist/chunk-VNKVBY54.js +363 -0
  362. package/dist/chunk-VNKVBY54.js.map +1 -0
  363. package/dist/chunk-VS4C7VAR.js +533 -0
  364. package/dist/chunk-VS4C7VAR.js.map +1 -0
  365. package/dist/chunk-VZMDYVNS.js +3151 -0
  366. package/dist/chunk-VZMDYVNS.js.map +1 -0
  367. package/dist/chunk-W4W4ZRBP.js +191 -0
  368. package/dist/chunk-W4W4ZRBP.js.map +1 -0
  369. package/dist/chunk-WN4KX7GE.js +25 -0
  370. package/dist/chunk-WN4KX7GE.js.map +1 -0
  371. package/dist/chunk-WNFTXSGR.js +111 -0
  372. package/dist/chunk-WNFTXSGR.js.map +1 -0
  373. package/dist/chunk-WOTDHRNF.js +2109 -0
  374. package/dist/chunk-WOTDHRNF.js.map +1 -0
  375. package/dist/chunk-XGWKTYXQ.js +67 -0
  376. package/dist/chunk-XGWKTYXQ.js.map +1 -0
  377. package/dist/chunk-XIGIJJSF.js +66 -0
  378. package/dist/chunk-XIGIJJSF.js.map +1 -0
  379. package/dist/chunk-XNUXB63I.js +817 -0
  380. package/dist/chunk-XNUXB63I.js.map +1 -0
  381. package/dist/chunk-XQ3CCNJD.js +610 -0
  382. package/dist/chunk-XQ3CCNJD.js.map +1 -0
  383. package/dist/chunk-XX6ZMCY5.js +4227 -0
  384. package/dist/chunk-XX6ZMCY5.js.map +1 -0
  385. package/dist/chunk-Y362QH54.js +98 -0
  386. package/dist/chunk-Y362QH54.js.map +1 -0
  387. package/dist/chunk-Y4T6ATDQ.js +74 -0
  388. package/dist/chunk-Y4T6ATDQ.js.map +1 -0
  389. package/dist/chunk-YQSBHWOI.js +41 -0
  390. package/dist/chunk-YU26SDNT.js +46 -0
  391. package/dist/chunk-YU26SDNT.js.map +1 -0
  392. package/dist/chunk-YULENNA7.js +501 -0
  393. package/dist/chunk-YULENNA7.js.map +1 -0
  394. package/dist/chunk-Z3ENMCFE.js +94 -0
  395. package/dist/chunk-Z3ENMCFE.js.map +1 -0
  396. package/dist/chunk-ZH7ZM3ZM.js +125 -0
  397. package/dist/chunk-ZH7ZM3ZM.js.map +1 -0
  398. package/dist/chunk-ZIXHS6AM.js +42 -0
  399. package/dist/chunk-ZIXHS6AM.js.map +1 -0
  400. package/dist/chunk-ZLLJKSNS.js +96 -0
  401. package/dist/chunk-ZLLJKSNS.js.map +1 -0
  402. package/dist/claim-LF6VQ7OR.js +197 -0
  403. package/dist/claim-LF6VQ7OR.js.map +1 -0
  404. package/dist/cli.js +104 -70041
  405. package/dist/cli.js.map +1 -1
  406. package/dist/client-26SAJ46D.js +19 -0
  407. package/dist/code-MOWYJMDH.js +76 -0
  408. package/dist/code-MOWYJMDH.js.map +1 -0
  409. package/dist/companies-6HSK5BLK.js +195 -0
  410. package/dist/companies-6HSK5BLK.js.map +1 -0
  411. package/dist/complete-2PUAFQEC.js +81 -0
  412. package/dist/complete-2PUAFQEC.js.map +1 -0
  413. package/dist/config-X6GKOVQ6.js +88 -0
  414. package/dist/config-X6GKOVQ6.js.map +1 -0
  415. package/dist/connect-RXPMAJQI.js +42 -0
  416. package/dist/content-UYZC4AKN.js +85 -0
  417. package/dist/content-UYZC4AKN.js.map +1 -0
  418. package/dist/conversation-Y3NB2VDI.js +93 -0
  419. package/dist/conversation-Y3NB2VDI.js.map +1 -0
  420. package/dist/conversion-report-R4GJWWTK.js +125 -0
  421. package/dist/conversion-report-R4GJWWTK.js.map +1 -0
  422. package/dist/conversion-upload-command-YP4DGTPP.js +438 -0
  423. package/dist/conversion-upload-command-YP4DGTPP.js.map +1 -0
  424. package/dist/conversion-writes-WHFFUWWL.js +120 -0
  425. package/dist/conversion-writes-WHFFUWWL.js.map +1 -0
  426. package/dist/conversions-K7C32DPW.js +135 -0
  427. package/dist/conversions-K7C32DPW.js.map +1 -0
  428. package/dist/conversions-OXJAX5GB.js +76 -0
  429. package/dist/conversions-OXJAX5GB.js.map +1 -0
  430. package/dist/countries-PBXNB3BR.js +82 -0
  431. package/dist/countries-PBXNB3BR.js.map +1 -0
  432. package/dist/create-AJ4KPSJ4.js +59 -0
  433. package/dist/create-AJ4KPSJ4.js.map +1 -0
  434. package/dist/create-ALKOAD5J.js +138 -0
  435. package/dist/create-ALKOAD5J.js.map +1 -0
  436. package/dist/create-HTKZMXKB.js +207 -0
  437. package/dist/create-HTKZMXKB.js.map +1 -0
  438. package/dist/create-TZMAOFBR.js +236 -0
  439. package/dist/create-TZMAOFBR.js.map +1 -0
  440. package/dist/creatives-MBTZTIAA.js +51 -0
  441. package/dist/creatives-MBTZTIAA.js.map +1 -0
  442. package/dist/creatives-OZHVU4WB.js +182 -0
  443. package/dist/creatives-OZHVU4WB.js.map +1 -0
  444. package/dist/creatives-VMGY5SU4.js +87 -0
  445. package/dist/creatives-VMGY5SU4.js.map +1 -0
  446. package/dist/credentials-FY4UH6TF.js +287 -0
  447. package/dist/credentials-FY4UH6TF.js.map +1 -0
  448. package/dist/critique-2UTP4DR3.js +216 -0
  449. package/dist/critique-2UTP4DR3.js.map +1 -0
  450. package/dist/critique-B3H7QFM2.js +1449 -0
  451. package/dist/critique-B3H7QFM2.js.map +1 -0
  452. package/dist/crm-2IBLUHDB.js +318 -0
  453. package/dist/crm-2IBLUHDB.js.map +1 -0
  454. package/dist/crm-YMYO4U53.js +224 -0
  455. package/dist/crm-YMYO4U53.js.map +1 -0
  456. package/dist/crop-U3AD4DDR.js +113 -0
  457. package/dist/crop-U3AD4DDR.js.map +1 -0
  458. package/dist/currency-S6KWZSZ6.js +88 -0
  459. package/dist/currency-S6KWZSZ6.js.map +1 -0
  460. package/dist/custom-conversions-FABS6YVT.js +125 -0
  461. package/dist/custom-conversions-FABS6YVT.js.map +1 -0
  462. package/dist/customers-P6LLLJHK.js +56 -0
  463. package/dist/decide-YTHCBKM6.js +199 -0
  464. package/dist/decide-YTHCBKM6.js.map +1 -0
  465. package/dist/delete-B2TDEW7H.js +63 -0
  466. package/dist/delete-B2TDEW7H.js.map +1 -0
  467. package/dist/delete-DZUDLYPC.js +69 -0
  468. package/dist/delete-DZUDLYPC.js.map +1 -0
  469. package/dist/delete-NZGZP4YP.js +71 -0
  470. package/dist/delete-NZGZP4YP.js.map +1 -0
  471. package/dist/delete-SDOZNDWQ.js +54 -0
  472. package/dist/delete-SDOZNDWQ.js.map +1 -0
  473. package/dist/delete-VQMU56OH.js +71 -0
  474. package/dist/delete-VQMU56OH.js.map +1 -0
  475. package/dist/delete-WKQPMVAM.js +66 -0
  476. package/dist/delete-WKQPMVAM.js.map +1 -0
  477. package/dist/demographics-Q3DCXULY.js +138 -0
  478. package/dist/demographics-Q3DCXULY.js.map +1 -0
  479. package/dist/describe-JNTCQZNL.js +97 -0
  480. package/dist/describe-JNTCQZNL.js.map +1 -0
  481. package/dist/describe-MT4334NG.js +97 -0
  482. package/dist/describe-MT4334NG.js.map +1 -0
  483. package/dist/dimensions-FEZUJRAG.js +101 -0
  484. package/dist/dimensions-FEZUJRAG.js.map +1 -0
  485. package/dist/discard-NEZM5CHF.js +66 -0
  486. package/dist/discard-NEZM5CHF.js.map +1 -0
  487. package/dist/discover-4GD7QSF5.js +93 -0
  488. package/dist/discover-4GD7QSF5.js.map +1 -0
  489. package/dist/discover-6K3IOYXR.js +172 -0
  490. package/dist/discover-6K3IOYXR.js.map +1 -0
  491. package/dist/domain-7Y256IDR.js +98 -0
  492. package/dist/domain-7Y256IDR.js.map +1 -0
  493. package/dist/download-L27HWEOE.js +299 -0
  494. package/dist/download-L27HWEOE.js.map +1 -0
  495. package/dist/draft-25G2P2TE.js +281 -0
  496. package/dist/draft-25G2P2TE.js.map +1 -0
  497. package/dist/draft-F6TUGXAY.js +307 -0
  498. package/dist/draft-F6TUGXAY.js.map +1 -0
  499. package/dist/draft-KLULNBWP.js +233 -0
  500. package/dist/draft-KLULNBWP.js.map +1 -0
  501. package/dist/draft-LY5ZPF73.js +217 -0
  502. package/dist/draft-LY5ZPF73.js.map +1 -0
  503. package/dist/draft-M4ZHDQ46.js +306 -0
  504. package/dist/draft-M4ZHDQ46.js.map +1 -0
  505. package/dist/draft-MI7RWY2K.js +100 -0
  506. package/dist/draft-MI7RWY2K.js.map +1 -0
  507. package/dist/draft-P4IVYTY3.js +103 -0
  508. package/dist/draft-P4IVYTY3.js.map +1 -0
  509. package/dist/draft-QQMCVDDS.js +328 -0
  510. package/dist/draft-QQMCVDDS.js.map +1 -0
  511. package/dist/engine/index.d.ts +213 -127
  512. package/dist/engine/index.js +18 -4
  513. package/dist/{env-4P5HOGNA.js → env-FBTHZKGN.js} +12 -2
  514. package/dist/env-FBTHZKGN.js.map +1 -0
  515. package/dist/experiment-HDYTJPUY.js +1397 -0
  516. package/dist/experiment-HDYTJPUY.js.map +1 -0
  517. package/dist/export-TS5GYEMA.js +168 -0
  518. package/dist/export-TS5GYEMA.js.map +1 -0
  519. package/dist/extract-QVQFUOIK.js +113 -0
  520. package/dist/extract-QVQFUOIK.js.map +1 -0
  521. package/dist/facets-7MBSM7BP.js +114 -0
  522. package/dist/facets-7MBSM7BP.js.map +1 -0
  523. package/dist/favorites-7QP7BY5K.js +178 -0
  524. package/dist/favorites-7QP7BY5K.js.map +1 -0
  525. package/dist/feed-EETRO2FG.js +177 -0
  526. package/dist/feed-EETRO2FG.js.map +1 -0
  527. package/dist/feedback-PLMFRAQW.js +66 -0
  528. package/dist/feedback-PLMFRAQW.js.map +1 -0
  529. package/dist/feedback-record-QEHHGVYC.js +177 -0
  530. package/dist/feedback-record-QEHHGVYC.js.map +1 -0
  531. package/dist/fetch-QE3EB756.js +176 -0
  532. package/dist/fetch-QE3EB756.js.map +1 -0
  533. package/dist/fetch-Z7O3ZL4D.js +139 -0
  534. package/dist/fetch-Z7O3ZL4D.js.map +1 -0
  535. package/dist/find-KAQ6JDMO.js +134 -0
  536. package/dist/find-KAQ6JDMO.js.map +1 -0
  537. package/dist/fit-MY5GVZZZ.js +45 -0
  538. package/dist/fit-MY5GVZZZ.js.map +1 -0
  539. package/dist/flows-C67KEMDU.js +247 -0
  540. package/dist/flows-C67KEMDU.js.map +1 -0
  541. package/dist/follow-IX4XVK5Y.js +94 -0
  542. package/dist/follow-IX4XVK5Y.js.map +1 -0
  543. package/dist/follow-competitors-UZBUOYTH.js +112 -0
  544. package/dist/follow-competitors-UZBUOYTH.js.map +1 -0
  545. package/dist/following-VGOIUCIZ.js +113 -0
  546. package/dist/following-VGOIUCIZ.js.map +1 -0
  547. package/dist/fonts-UR7XZ5XZ.js +34 -0
  548. package/dist/fonts-UR7XZ5XZ.js.map +1 -0
  549. package/dist/forecast-3F4SZNAK.js +104 -0
  550. package/dist/forecast-3F4SZNAK.js.map +1 -0
  551. package/dist/forecast-K5E7CTKX.js +168 -0
  552. package/dist/forecast-K5E7CTKX.js.map +1 -0
  553. package/dist/funding-H5YO37OW.js +85 -0
  554. package/dist/funding-H5YO37OW.js.map +1 -0
  555. package/dist/funnel-2PPEPAMY.js +394 -0
  556. package/dist/funnel-2PPEPAMY.js.map +1 -0
  557. package/dist/ga4-YHJ2XMLC.js +55 -0
  558. package/dist/ga4-YHJ2XMLC.js.map +1 -0
  559. package/dist/generate-M2ZX5DMI.js +262 -0
  560. package/dist/generate-M2ZX5DMI.js.map +1 -0
  561. package/dist/get-4EYKCZ6F.js +59 -0
  562. package/dist/get-4EYKCZ6F.js.map +1 -0
  563. package/dist/get-637GALM2.js +63 -0
  564. package/dist/get-637GALM2.js.map +1 -0
  565. package/dist/get-AZARFR6O.js +61 -0
  566. package/dist/get-AZARFR6O.js.map +1 -0
  567. package/dist/get-BPMEHRUG.js +56 -0
  568. package/dist/get-BPMEHRUG.js.map +1 -0
  569. package/dist/get-MQ6MMIZ7.js +78 -0
  570. package/dist/get-MQ6MMIZ7.js.map +1 -0
  571. package/dist/get-N4BPOQCB.js +63 -0
  572. package/dist/get-N4BPOQCB.js.map +1 -0
  573. package/dist/get-RPZHLQ56.js +92 -0
  574. package/dist/get-RPZHLQ56.js.map +1 -0
  575. package/dist/gif-6PRTB2VR.js +94 -0
  576. package/dist/gif-6PRTB2VR.js.map +1 -0
  577. package/dist/google-52JRH7X3.js +136 -0
  578. package/dist/google-52JRH7X3.js.map +1 -0
  579. package/dist/google-X2AQR3RB.js +3190 -0
  580. package/dist/google-X2AQR3RB.js.map +1 -0
  581. package/dist/google-ads-PWX7OF5Z.js +195 -0
  582. package/dist/google-ads-PWX7OF5Z.js.map +1 -0
  583. package/dist/group-AAZ5QKC3.js +43 -0
  584. package/dist/group-AAZ5QKC3.js.map +1 -0
  585. package/dist/group-GSYCZORC.js +43 -0
  586. package/dist/group-GSYCZORC.js.map +1 -0
  587. package/dist/gsc-TS5T7ML2.js +30 -0
  588. package/dist/gsc-TS5T7ML2.js.map +1 -0
  589. package/dist/history-EW6LFFCX.js +67 -0
  590. package/dist/history-EW6LFFCX.js.map +1 -0
  591. package/dist/history-XI5ABCFO.js +133 -0
  592. package/dist/history-XI5ABCFO.js.map +1 -0
  593. package/dist/hubspot-AVUOILRE.js +779 -0
  594. package/dist/hubspot-AVUOILRE.js.map +1 -0
  595. package/dist/icon-K75KI6BV.js +137 -0
  596. package/dist/icon-K75KI6BV.js.map +1 -0
  597. package/dist/ideas-L3SZSIZP.js +35 -0
  598. package/dist/ideas-L3SZSIZP.js.map +1 -0
  599. package/dist/images-CD53I2W5.js +90 -0
  600. package/dist/images-CD53I2W5.js.map +1 -0
  601. package/dist/improve-L3BW4EQG.js +86 -0
  602. package/dist/improve-L3BW4EQG.js.map +1 -0
  603. package/dist/ingest-KRJOYTCQ.js +107 -0
  604. package/dist/ingest-KRJOYTCQ.js.map +1 -0
  605. package/dist/ingest-WE3SPPND.js +23 -0
  606. package/dist/ingest-WE3SPPND.js.map +1 -0
  607. package/dist/insights-PELVD4RY.js +122 -0
  608. package/dist/insights-PELVD4RY.js.map +1 -0
  609. package/dist/insights-XCA4BUJW.js +423 -0
  610. package/dist/insights-XCA4BUJW.js.map +1 -0
  611. package/dist/inspect-QKETG5UJ.js +189 -0
  612. package/dist/inspect-QKETG5UJ.js.map +1 -0
  613. package/dist/inspiration-Y2J2PUPC.js +53 -0
  614. package/dist/inspiration-Y2J2PUPC.js.map +1 -0
  615. package/dist/intent-YJOJGNVJ.js +95 -0
  616. package/dist/intent-YJOJGNVJ.js.map +1 -0
  617. package/dist/job-E2U4FS5P.js +202 -0
  618. package/dist/job-E2U4FS5P.js.map +1 -0
  619. package/dist/job-create-DFYCHVQ7.js +109 -0
  620. package/dist/job-create-DFYCHVQ7.js.map +1 -0
  621. package/dist/job-status-FFPMTSHB.js +74 -0
  622. package/dist/job-status-FFPMTSHB.js.map +1 -0
  623. package/dist/keep-GYZS2UTL.js +78 -0
  624. package/dist/keep-GYZS2UTL.js.map +1 -0
  625. package/dist/keyword-gap-QU7O7UKC.js +130 -0
  626. package/dist/keyword-gap-QU7O7UKC.js.map +1 -0
  627. package/dist/keyword-metrics-HLCM3QU7.js +197 -0
  628. package/dist/keyword-metrics-HLCM3QU7.js.map +1 -0
  629. package/dist/keywords-for-site-BDKPQKWB.js +128 -0
  630. package/dist/keywords-for-site-BDKPQKWB.js.map +1 -0
  631. package/dist/landing-4K6XYNFM.js +28 -0
  632. package/dist/landing-4K6XYNFM.js.map +1 -0
  633. package/dist/landings-F7LOAEJE.js +151 -0
  634. package/dist/landings-F7LOAEJE.js.map +1 -0
  635. package/dist/languages-P3ZM6R2A.js +40 -0
  636. package/dist/languages-P3ZM6R2A.js.map +1 -0
  637. package/dist/languages-XEJKTIFP.js +55 -0
  638. package/dist/languages-XEJKTIFP.js.map +1 -0
  639. package/dist/launches-TCFAYUA5.js +19 -0
  640. package/dist/launches-TCFAYUA5.js.map +1 -0
  641. package/dist/layerize-ZSKFIPZA.js +190 -0
  642. package/dist/layerize-ZSKFIPZA.js.map +1 -0
  643. package/dist/leads-GDF4S2AM.js +90 -0
  644. package/dist/leads-GDF4S2AM.js.map +1 -0
  645. package/dist/leads-TPB5OFSQ.js +293 -0
  646. package/dist/leads-TPB5OFSQ.js.map +1 -0
  647. package/dist/learn-NMFEA5H5.js +203 -0
  648. package/dist/learn-NMFEA5H5.js.map +1 -0
  649. package/dist/library-IP2UKNRU.js +109 -0
  650. package/dist/library-IP2UKNRU.js.map +1 -0
  651. package/dist/lighthouse-57XBJRCO.js +75 -0
  652. package/dist/lighthouse-57XBJRCO.js.map +1 -0
  653. package/dist/line-item-writes-O6SSS5NQ.js +208 -0
  654. package/dist/line-item-writes-O6SSS5NQ.js.map +1 -0
  655. package/dist/line-items-ZZAKH3YZ.js +103 -0
  656. package/dist/line-items-ZZAKH3YZ.js.map +1 -0
  657. package/dist/link-CRY57R3E.js +64 -0
  658. package/dist/link-CRY57R3E.js.map +1 -0
  659. package/dist/linkedin-WC7LGYPP.js +1008 -0
  660. package/dist/linkedin-WC7LGYPP.js.map +1 -0
  661. package/dist/list-6IKPWQGG.js +57 -0
  662. package/dist/list-6IKPWQGG.js.map +1 -0
  663. package/dist/list-CAUH3BMG.js +65 -0
  664. package/dist/list-CAUH3BMG.js.map +1 -0
  665. package/dist/list-CWAKX5TH.js +63 -0
  666. package/dist/list-CWAKX5TH.js.map +1 -0
  667. package/dist/list-FJ5FSVL6.js +151 -0
  668. package/dist/list-FJ5FSVL6.js.map +1 -0
  669. package/dist/list-GTOSN7DP.js +50 -0
  670. package/dist/list-GTOSN7DP.js.map +1 -0
  671. package/dist/list-HLJP4NVE.js +51 -0
  672. package/dist/list-HLJP4NVE.js.map +1 -0
  673. package/dist/list-LCUPF7EF.js +76 -0
  674. package/dist/list-LCUPF7EF.js.map +1 -0
  675. package/dist/list-NH4ZRZTL.js +303 -0
  676. package/dist/list-NH4ZRZTL.js.map +1 -0
  677. package/dist/locations-2BYWKIYA.js +67 -0
  678. package/dist/locations-2BYWKIYA.js.map +1 -0
  679. package/dist/locations-LVVDOSXO.js +40 -0
  680. package/dist/locations-LVVDOSXO.js.map +1 -0
  681. package/dist/log-6JOWOY5G.js +92 -0
  682. package/dist/log-6JOWOY5G.js.map +1 -0
  683. package/dist/logo-AJK2TDQU.js +18 -0
  684. package/dist/logo-AJK2TDQU.js.map +1 -0
  685. package/dist/lookup-H5GEM77H.js +22 -0
  686. package/dist/lookup-H5GEM77H.js.map +1 -0
  687. package/dist/make-N3WZ6OSS.js +640 -0
  688. package/dist/make-N3WZ6OSS.js.map +1 -0
  689. package/dist/map-4KOYFHAN.js +404 -0
  690. package/dist/map-4KOYFHAN.js.map +1 -0
  691. package/dist/mcp-LSS2DHYL.js +463 -0
  692. package/dist/mcp-LSS2DHYL.js.map +1 -0
  693. package/dist/media-JBCN6V4X.js +15 -0
  694. package/dist/media-JBCN6V4X.js.map +1 -0
  695. package/dist/media-RAGEJTZX.js +89 -0
  696. package/dist/media-RAGEJTZX.js.map +1 -0
  697. package/dist/media-sources-GSPAV7HT.js +32 -0
  698. package/dist/media-sources-GSPAV7HT.js.map +1 -0
  699. package/dist/members-GUUVEK2Y.js +44 -0
  700. package/dist/members-GUUVEK2Y.js.map +1 -0
  701. package/dist/meta-Z37ZR3PJ.js +69 -0
  702. package/dist/meta-Z37ZR3PJ.js.map +1 -0
  703. package/dist/metrics-KW6SDULI.js +152 -0
  704. package/dist/metrics-KW6SDULI.js.map +1 -0
  705. package/dist/models-5J4WFUD3.js +155 -0
  706. package/dist/models-5J4WFUD3.js.map +1 -0
  707. package/dist/normalize-DSFVTRXN.js +806 -0
  708. package/dist/normalize-DSFVTRXN.js.map +1 -0
  709. package/dist/normalize-EUDI5HEM.js +141 -0
  710. package/dist/normalize-EUDI5HEM.js.map +1 -0
  711. package/dist/options-P4PPJQH4.js +121 -0
  712. package/dist/options-P4PPJQH4.js.map +1 -0
  713. package/dist/output-DTCLVPUF.js +24 -0
  714. package/dist/output-DTCLVPUF.js.map +1 -0
  715. package/dist/page-N2ZO3O7F.js +105 -0
  716. package/dist/page-N2ZO3O7F.js.map +1 -0
  717. package/dist/pages-FQ4CBAJ2.js +66 -0
  718. package/dist/pages-FQ4CBAJ2.js.map +1 -0
  719. package/dist/performance-A66LYIZX.js +24 -0
  720. package/dist/performance-A66LYIZX.js.map +1 -0
  721. package/dist/pinterest-NV5XTNJE.js +96 -0
  722. package/dist/pinterest-NV5XTNJE.js.map +1 -0
  723. package/dist/pixels-CTJ43FZ4.js +108 -0
  724. package/dist/pixels-CTJ43FZ4.js.map +1 -0
  725. package/dist/pixels-HKINGZCH.js +60 -0
  726. package/dist/pixels-HKINGZCH.js.map +1 -0
  727. package/dist/plan-ZYTWLGWK.js +207 -0
  728. package/dist/plan-ZYTWLGWK.js.map +1 -0
  729. package/dist/preview-5ROBOQG7.js +169 -0
  730. package/dist/preview-5ROBOQG7.js.map +1 -0
  731. package/dist/profile-Q3PHR6FU.js +159 -0
  732. package/dist/profile-Q3PHR6FU.js.map +1 -0
  733. package/dist/promoted-tweet-writes-AEOUK2LK.js +180 -0
  734. package/dist/promoted-tweet-writes-AEOUK2LK.js.map +1 -0
  735. package/dist/promoted-tweets-L2CEC2BJ.js +119 -0
  736. package/dist/promoted-tweets-L2CEC2BJ.js.map +1 -0
  737. package/dist/properties-WLL6M2P6.js +101 -0
  738. package/dist/properties-WLL6M2P6.js.map +1 -0
  739. package/dist/publish-KHNR5ARF.js +322 -0
  740. package/dist/publish-KHNR5ARF.js.map +1 -0
  741. package/dist/query-A34J7BGX.js +61 -0
  742. package/dist/query-A34J7BGX.js.map +1 -0
  743. package/dist/query-IYZN5KHO.js +236 -0
  744. package/dist/query-IYZN5KHO.js.map +1 -0
  745. package/dist/query-UN2P6DBA.js +247 -0
  746. package/dist/query-UN2P6DBA.js.map +1 -0
  747. package/dist/questionnaire-Z2ZIKNBV.js +93 -0
  748. package/dist/questionnaire-Z2ZIKNBV.js.map +1 -0
  749. package/dist/read-GM7PQYJA.js +141 -0
  750. package/dist/read-GM7PQYJA.js.map +1 -0
  751. package/dist/reddit-5NPFULGD.js +26 -0
  752. package/dist/reddit-5NPFULGD.js.map +1 -0
  753. package/dist/refresh-FGXW4Y4X.js +94 -0
  754. package/dist/refresh-FGXW4Y4X.js.map +1 -0
  755. package/dist/release-OKZMC73N.js +60 -0
  756. package/dist/release-OKZMC73N.js.map +1 -0
  757. package/dist/relevant-pages-J76ZKNKF.js +99 -0
  758. package/dist/relevant-pages-J76ZKNKF.js.map +1 -0
  759. package/dist/remove-CA2J6ACH.js +50 -0
  760. package/dist/remove-CA2J6ACH.js.map +1 -0
  761. package/dist/remove-GOYW2G4R.js +48 -0
  762. package/dist/remove-GOYW2G4R.js.map +1 -0
  763. package/dist/reopen-XPKSOE6O.js +63 -0
  764. package/dist/reopen-XPKSOE6O.js.map +1 -0
  765. package/dist/rerun-QA6ISKVO.js +86 -0
  766. package/dist/rerun-QA6ISKVO.js.map +1 -0
  767. package/dist/research-FZ63UZTE.js +62 -0
  768. package/dist/research-FZ63UZTE.js.map +1 -0
  769. package/dist/research-YGQQ6CNG.js +262 -0
  770. package/dist/research-YGQQ6CNG.js.map +1 -0
  771. package/dist/reset-BGPZ4HMA.js +54 -0
  772. package/dist/reset-BGPZ4HMA.js.map +1 -0
  773. package/dist/reset-HTD6LCIT.js +54 -0
  774. package/dist/reset-HTD6LCIT.js.map +1 -0
  775. package/dist/resolve-NRDZ6N2B.js +121 -0
  776. package/dist/resolve-NRDZ6N2B.js.map +1 -0
  777. package/dist/retry-PF4BGKJV.js +60 -0
  778. package/dist/retry-PF4BGKJV.js.map +1 -0
  779. package/dist/retry-XL26JDAV.js +62 -0
  780. package/dist/retry-XL26JDAV.js.map +1 -0
  781. package/dist/retry-failed-BEFWRV4Z.js +45 -0
  782. package/dist/retry-failed-BEFWRV4Z.js.map +1 -0
  783. package/dist/run-3ZDGCCTH.js +61 -0
  784. package/dist/run-3ZDGCCTH.js.map +1 -0
  785. package/dist/run-EMJLI7LR.js +65 -0
  786. package/dist/run-EMJLI7LR.js.map +1 -0
  787. package/dist/scaffold-ad-QW75LLYR.js +1550 -0
  788. package/dist/scaffold-ad-QW75LLYR.js.map +1 -0
  789. package/dist/scaffold-static-ad-S3UOKLJ3.js +973 -0
  790. package/dist/scaffold-static-ad-S3UOKLJ3.js.map +1 -0
  791. package/dist/scaffold-video-3BJQOUAA.js +874 -0
  792. package/dist/scaffold-video-3BJQOUAA.js.map +1 -0
  793. package/dist/schedule-KQ7UIR3N.js +66 -0
  794. package/dist/schedule-KQ7UIR3N.js.map +1 -0
  795. package/dist/schedule-PKPNSISL.js +61 -0
  796. package/dist/schedule-PKPNSISL.js.map +1 -0
  797. package/dist/scheduled-agents-LP5LITA3.js +49 -0
  798. package/dist/scheduled-agents-LP5LITA3.js.map +1 -0
  799. package/dist/schema-B6J6VPTO.js +132 -0
  800. package/dist/schema-B6J6VPTO.js.map +1 -0
  801. package/dist/schema-QXTDLZVC.js +63 -0
  802. package/dist/schema-QXTDLZVC.js.map +1 -0
  803. package/dist/scrape-7YKJWFZE.js +2241 -0
  804. package/dist/scrape-7YKJWFZE.js.map +1 -0
  805. package/dist/screenshot-ZHBSG3SI.js +102 -0
  806. package/dist/screenshot-ZHBSG3SI.js.map +1 -0
  807. package/dist/search-332KFYZ6.js +224 -0
  808. package/dist/search-332KFYZ6.js.map +1 -0
  809. package/dist/search-4PBMXXUY.js +91 -0
  810. package/dist/search-4PBMXXUY.js.map +1 -0
  811. package/dist/search-6WNUKCMP.js +174 -0
  812. package/dist/search-6WNUKCMP.js.map +1 -0
  813. package/dist/search-AVNUU6TJ.js +26 -0
  814. package/dist/search-AVNUU6TJ.js.map +1 -0
  815. package/dist/search-F5INRFFK.js +92 -0
  816. package/dist/search-F5INRFFK.js.map +1 -0
  817. package/dist/search-copy-DZVBYG2B.js +168 -0
  818. package/dist/search-copy-DZVBYG2B.js.map +1 -0
  819. package/dist/seeds-5JTQN5WI.js +101 -0
  820. package/dist/seeds-5JTQN5WI.js.map +1 -0
  821. package/dist/select-2V6CHATD.js +96 -0
  822. package/dist/select-2V6CHATD.js.map +1 -0
  823. package/dist/sends-3L6QBS46.js +309 -0
  824. package/dist/sends-3L6QBS46.js.map +1 -0
  825. package/dist/sequences-YKFV7BDH.js +146 -0
  826. package/dist/sequences-YKFV7BDH.js.map +1 -0
  827. package/dist/set-prompt-XGZXW63I.js +188 -0
  828. package/dist/set-prompt-XGZXW63I.js.map +1 -0
  829. package/dist/setup-links-6H5ONDCQ.js +141 -0
  830. package/dist/setup-links-6H5ONDCQ.js.map +1 -0
  831. package/dist/{shared-6762QE6R.js → shared-QAMP7FZV.js} +8 -6
  832. package/dist/shared-QAMP7FZV.js.map +1 -0
  833. package/dist/shopify-D3GNV5O6.js +3715 -0
  834. package/dist/shopify-D3GNV5O6.js.map +1 -0
  835. package/dist/site-TBPERLY3.js +177 -0
  836. package/dist/site-TBPERLY3.js.map +1 -0
  837. package/dist/sitemaps-XPGLW24S.js +87 -0
  838. package/dist/sitemaps-XPGLW24S.js.map +1 -0
  839. package/dist/sites-WMBY2HJV.js +101 -0
  840. package/dist/sites-WMBY2HJV.js.map +1 -0
  841. package/dist/skills-3HEILQCU.js +47 -0
  842. package/dist/skills-3HEILQCU.js.map +1 -0
  843. package/dist/sources-JFQKTK2L.js +421 -0
  844. package/dist/sources-JFQKTK2L.js.map +1 -0
  845. package/dist/sources-Q3I6DDV7.js +30 -0
  846. package/dist/sources-Q3I6DDV7.js.map +1 -0
  847. package/dist/stale-4SERZW2D.js +66 -0
  848. package/dist/stale-4SERZW2D.js.map +1 -0
  849. package/dist/stats-GNILHSKK.js +34 -0
  850. package/dist/stats-GNILHSKK.js.map +1 -0
  851. package/dist/status-LIZWJYS4.js +62 -0
  852. package/dist/status-LIZWJYS4.js.map +1 -0
  853. package/dist/sticker-KCV3VZHY.js +94 -0
  854. package/dist/sticker-KCV3VZHY.js.map +1 -0
  855. package/dist/stock-VTORKR4C.js +179 -0
  856. package/dist/stock-VTORKR4C.js.map +1 -0
  857. package/dist/studio-QCRA6BY5.js +49 -0
  858. package/dist/studio-QCRA6BY5.js.map +1 -0
  859. package/dist/sync-455DB46Y.js +54 -0
  860. package/dist/sync-455DB46Y.js.map +1 -0
  861. package/dist/sync-A4KKQ3B6.js +265 -0
  862. package/dist/sync-A4KKQ3B6.js.map +1 -0
  863. package/dist/sync-ULKWDFXM.js +54 -0
  864. package/dist/sync-ULKWDFXM.js.map +1 -0
  865. package/dist/sync-definition-YTBLMLX2.js +55 -0
  866. package/dist/sync-definition-YTBLMLX2.js.map +1 -0
  867. package/dist/tag-manager-E7QTXBWQ.js +43 -0
  868. package/dist/tag-manager-E7QTXBWQ.js.map +1 -0
  869. package/dist/tags-4Q4I5GQJ.js +20 -0
  870. package/dist/tags-4Q4I5GQJ.js.map +1 -0
  871. package/dist/tags-AMEVAQZ4.js +26 -0
  872. package/dist/tags-AMEVAQZ4.js.map +1 -0
  873. package/dist/tags-DPLOLH3E.js +20 -0
  874. package/dist/tags-DPLOLH3E.js.map +1 -0
  875. package/dist/tags-LDHXONR3.js +224 -0
  876. package/dist/tags-LDHXONR3.js.map +1 -0
  877. package/dist/tags-UW3WJTAQ.js +20 -0
  878. package/dist/tags-UW3WJTAQ.js.map +1 -0
  879. package/dist/targeting-constants-D64YO3E7.js +118 -0
  880. package/dist/targeting-constants-D64YO3E7.js.map +1 -0
  881. package/dist/targeting-criteria-BP6VJA7X.js +113 -0
  882. package/dist/targeting-criteria-BP6VJA7X.js.map +1 -0
  883. package/dist/targets-CQWO2UXD.js +102 -0
  884. package/dist/targets-CQWO2UXD.js.map +1 -0
  885. package/dist/templates-WZSTNRNZ.js +178 -0
  886. package/dist/templates-WZSTNRNZ.js.map +1 -0
  887. package/dist/testimonials-ETHSNWUH.js +32 -0
  888. package/dist/testimonials-ETHSNWUH.js.map +1 -0
  889. package/dist/themes-YU663LQH.js +94 -0
  890. package/dist/themes-YU663LQH.js.map +1 -0
  891. package/dist/top-companies-KTXHS472.js +108 -0
  892. package/dist/top-companies-KTXHS472.js.map +1 -0
  893. package/dist/translate-6QAJ5ZUU.js +600 -0
  894. package/dist/translate-6QAJ5ZUU.js.map +1 -0
  895. package/dist/triggers-4EDFLNGH.js +141 -0
  896. package/dist/triggers-4EDFLNGH.js.map +1 -0
  897. package/dist/tweets-ZUKQAAUQ.js +123 -0
  898. package/dist/tweets-ZUKQAAUQ.js.map +1 -0
  899. package/dist/unfollow-A4JVCJBF.js +46 -0
  900. package/dist/unfollow-A4JVCJBF.js.map +1 -0
  901. package/dist/unlink-KY2OEKMD.js +67 -0
  902. package/dist/unlink-KY2OEKMD.js.map +1 -0
  903. package/dist/unstage-FHUG67RI.js +84 -0
  904. package/dist/unstage-FHUG67RI.js.map +1 -0
  905. package/dist/unstage-ZRLLCSQF.js +95 -0
  906. package/dist/unstage-ZRLLCSQF.js.map +1 -0
  907. package/dist/update-27LGM65V.js +63 -0
  908. package/dist/update-27LGM65V.js.map +1 -0
  909. package/dist/update-AIY46I2Q.js +155 -0
  910. package/dist/update-AIY46I2Q.js.map +1 -0
  911. package/dist/update-QYD2XZFM.js +142 -0
  912. package/dist/update-QYD2XZFM.js.map +1 -0
  913. package/dist/update-W2OSRNFO.js +98 -0
  914. package/dist/update-W2OSRNFO.js.map +1 -0
  915. package/dist/upload-JGBTSKGH.js +126 -0
  916. package/dist/upload-JGBTSKGH.js.map +1 -0
  917. package/dist/upload-XEHWYMJG.js +150 -0
  918. package/dist/upload-XEHWYMJG.js.map +1 -0
  919. package/dist/upscale-I5P2OHTN.js +109 -0
  920. package/dist/upscale-I5P2OHTN.js.map +1 -0
  921. package/dist/url-options-QHGA42RP.js +130 -0
  922. package/dist/url-options-QHGA42RP.js.map +1 -0
  923. package/dist/url-params-NWSUHWRE.js +98 -0
  924. package/dist/url-params-NWSUHWRE.js.map +1 -0
  925. package/dist/use-U4HG2NF3.js +95 -0
  926. package/dist/use-U4HG2NF3.js.map +1 -0
  927. package/dist/validate-DOQ6AI62.js +91 -0
  928. package/dist/validate-DOQ6AI62.js.map +1 -0
  929. package/dist/variant-FOVE6URJ.js +338 -0
  930. package/dist/variant-FOVE6URJ.js.map +1 -0
  931. package/dist/videos-DP3VCSYG.js +39 -0
  932. package/dist/videos-DP3VCSYG.js.map +1 -0
  933. package/dist/view-HRI3FHCH.js +121 -0
  934. package/dist/view-HRI3FHCH.js.map +1 -0
  935. package/dist/viewer-check-JYUE24M6.js +68 -0
  936. package/dist/viewer-check-JYUE24M6.js.map +1 -0
  937. package/dist/web-AFUIWPFU.js +140 -0
  938. package/dist/web-AFUIWPFU.js.map +1 -0
  939. package/dist/winners-SMMDN6LW.js +113 -0
  940. package/dist/winners-SMMDN6LW.js.map +1 -0
  941. package/dist/winning-ads-PCIJUYVA.js +84 -0
  942. package/dist/winning-ads-PCIJUYVA.js.map +1 -0
  943. package/dist/write-commands-7Z34WG2J.js +206 -0
  944. package/dist/write-commands-7Z34WG2J.js.map +1 -0
  945. package/dist/write-commands-BLT2SEC4.js +713 -0
  946. package/dist/write-commands-BLT2SEC4.js.map +1 -0
  947. package/dist/write-commands-N4FSGRVQ.js +743 -0
  948. package/dist/write-commands-N4FSGRVQ.js.map +1 -0
  949. package/dist/write-commands-Q4MQ3BGM.js +1015 -0
  950. package/dist/write-commands-Q4MQ3BGM.js.map +1 -0
  951. package/dist/x-AUOZJNWE.js +47 -0
  952. package/dist/x-AUOZJNWE.js.map +1 -0
  953. package/package.json +9 -2
  954. package/dist/chunk-FGHMWBUD.js +0 -9989
  955. package/dist/chunk-FGHMWBUD.js.map +0 -1
  956. package/dist/chunk-HJEMOSPH.js +0 -1292
  957. package/dist/chunk-HJEMOSPH.js.map +0 -1
  958. package/dist/chunk-MF7YA62W.js.map +0 -1
  959. package/dist/chunk-VZEDAFAR.js.map +0 -1
  960. package/dist/chunk-XHC4LMK4.js.map +0 -1
  961. package/dist/client-GR6YL3YX.js +0 -17
  962. package/dist/output-NMQXZF3Z.js +0 -18
  963. /package/dist/{chunk-WLDM2TRF.js.map → chunk-TPXI2GCW.js.map} +0 -0
  964. /package/dist/{client-GR6YL3YX.js.map → chunk-YQSBHWOI.js.map} +0 -0
  965. /package/dist/{env-4P5HOGNA.js.map → client-26SAJ46D.js.map} +0 -0
  966. /package/dist/{output-NMQXZF3Z.js.map → connect-RXPMAJQI.js.map} +0 -0
  967. /package/dist/{shared-6762QE6R.js.map → customers-P6LLLJHK.js.map} +0 -0
package/README.md CHANGED
@@ -36,7 +36,7 @@ export BAKER_X_ADS_ACCOUNT_ID="18ce53xyz"
36
36
  export BAKER_CHAT_ID="<chat-id>"
37
37
  ```
38
38
 
39
- - `BAKER_API_KEY` must start with `bk_` (or `bkr_` for the read-only credential a Session review runs with)
39
+ - `BAKER_API_KEY` must start with `bk_` (or `bkr_` for the read-only credential a Session review runs with, or `bkv_` for the one a published report runs with)
40
40
  - A missing or malformed `BAKER_API_KEY` / `BAKER_API_URL` ends any command that talks to Baker with `NOT_CONFIGURED` (non-retryable, with a `fix`) naming the variable and the rule it broke — never its value
41
41
  - A malformed `BAKER_GOOGLE_ADS_CUSTOMER_ID`, `BAKER_LINKEDIN_AD_ACCOUNT_ID` or `BAKER_X_ADS_ACCOUNT_ID` ends it with `VALIDATION_ERROR` instead, whose `fix` says to correct or unset the variable
42
42
  - `BAKER_API_URL` is your Convex site URL — the deployment the CLI talks to, not an address to show anybody
@@ -46,6 +46,8 @@ export BAKER_CHAT_ID="<chat-id>"
46
46
  - `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.
47
47
  - `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.
48
48
  - `BAKER_CHAT_ID` — chat context for staged Work Action commands and staged Scheduled agent create/update/delete/unstage. `scheduled-agents run` 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.
49
+ - `BAKER_THREAD_ID` — set per turn by the sandbox. Sent as an `x-baker-thread-id` header alongside the chat header, so the tools a command reports or changes are those of whoever sent this turn, even while someone else writes in another thread of the same chat.
50
+ - `BAKER_KEY_SCOPE` — `report` while a report's data endpoint runs in the chat preview (set by the reports runtime). Sent as an `x-baker-key-scope` header, Baker then holds the key to what a published report may do — reads only — so a report that writes fails in the preview rather than once published. It can only narrow the key.
49
51
 
50
52
  ## Debug logging
51
53
 
@@ -106,7 +108,7 @@ offered, so a short option is never pointed at an unrelated one (`--from` at `--
106
108
  Previously such an option was accepted and ignored, so a call that
107
109
  looked like it had filtered or narrowed a result was in fact answering the unfiltered
108
110
  question — and nothing in the output said so. Both spellings of an option work
109
- (`--run-id` and `--runId`), as they always did. The error's `fix` names the exact call
111
+ (`--run-id` and `--run-id`), as they always did. The error's `fix` names the exact call
110
112
  that lists every option: `baker schema <command>` (`check_schema`) when the command
111
113
  registers a schema, `baker <command> --help` (`check_help`) when it does not.
112
114
 
@@ -157,7 +159,7 @@ Use `--output` to change format:
157
159
 
158
160
  ### Ad Platforms (`baker ads`)
159
161
 
160
- Multi-platform ad commands. Google, Meta, and LinkedIn support both reads and **staged writes** (create/update campaigns, ad sets/ad groups, ads, creatives, audiences — applied on chat publish); X (Twitter) is read + analytics. TikTok is coming.
162
+ Multi-platform ad commands. Google, Meta, and LinkedIn support both reads and **staged writes** (create/update campaigns, ad sets/ad groups, ads, creatives, audiences — applied on chat publish); X (Twitter) is read + analytics; ChatGPT Ads is read + analytics + staged writes. TikTok is coming.
161
163
 
162
164
  ---
163
165
 
@@ -167,8 +169,11 @@ List the Google Ads accounts selected for this company. Run this first to find a
167
169
 
168
170
  ```bash
169
171
  baker ads google accounts
172
+ baker ads google accounts --available
170
173
  ```
171
174
 
175
+ `--available` lists every account the connected login reaches instead, each `{ id, name, selected }`. Read-only: it never changes the selection. When the account the user means comes back `selected: false`, the hint names it and points at `request_connection` with `pick: true`, which opens the account picker in the chat. The same flag works on `ads meta accounts`, `ads linkedin accounts`, `ads x accounts`, `ga4 properties`, `gsc sites` and `tag-manager containers`.
176
+
172
177
  **Response:**
173
178
 
174
179
  ```json
@@ -217,10 +222,10 @@ baker ads google currency --customer-id 5904042878
217
222
 
218
223
  ### `baker ads google url-options`
219
224
 
220
- The UTM / tracking parameters in force on the account: the account-level Final URL Suffix and tracking template, plus every campaign that overrides them. Start here for any "add `utm_agency=…`" or agency-tag question.
225
+ The UTM / tracking parameters in force on the account: the account-level Final URL Suffix and tracking template, plus every campaign, ad group and ad that overrides them. Start here for any "add `utm_agency=…`" or agency-tag question.
221
226
 
222
227
  ```bash
223
- baker ads google url-options --customer-id 1234567890 # overriding campaigns only
228
+ baker ads google url-options --customer-id 1234567890 # overriding campaigns, ad groups and ads
224
229
  baker ads google url-options --customer-id 1234567890 --full # every campaign read
225
230
  ```
226
231
 
@@ -231,15 +236,19 @@ Every value is a tagged union, never a bare string, so an unread level can never
231
236
  "ok": true,
232
237
  "data": {
233
238
  "account": { "final_url_suffix": { "state": "set", "value": "utm_source=google" } },
234
- "campaigns": { "overriding": [], "omitted": 12 },
235
- "levels_not_read": ["ad_group", "ad"]
239
+ "campaigns": { "overrides": [], "omitted": 12 },
240
+ "ad_groups": { "read": true, "overrides": [], "truncated": false },
241
+ "ads": { "read": true, "overrides": [], "truncated": false },
242
+ "levels_not_read": []
236
243
  }
237
244
  }
238
245
  ```
239
246
 
240
247
  `set` holds a value · `not_set` means Google carries none at that level · `not_read` means it was never observed. **Only `not_set` is a finding.**
241
248
 
242
- Writing it: `baker ads google campaigns update <id> --final-url-suffix "utm_source=google&utm_agency=baker"` stages a campaign-level override (it *replaces* the account value for that campaign, it does not merge). The **account** level is deliberately not stageable — Google writes it through `CustomerService`, outside the atomic publish batch, so it could not be published atomically nor reversed by discarding the chat. `--level account` stages nothing and returns a structured `error.fix` naming where to set it.
249
+ Writing it: `baker ads google campaigns update <id> --final-url-suffix "utm_source=google&utm_agency=baker"` stages a campaign-level override (it *replaces* the account value for that campaign, it does not merge). The **account** level stages too: `baker ads google account update --customer-id … --final-url-suffix "utm_source=google&utm_agency=baker"` (or `campaigns update --level account --final-url-suffix …`, the same op) applies at publish with the rest of the draft; `""` clears it.
250
+
251
+ **Account settings** — `baker ads google account update --customer-id …` stages the account's own settings: `--final-url-suffix`, `--tracking-template`, `--auto-tagging` / `--auto-tagging=false`, `--call-reporting`, `--call-conversions`, `--video-inventory EXPANDED|STANDARD|LIMITED`. One card per account (staging again amends it), shown before → after; a setting the account already holds is a no-op. Turning auto-tagging off stages with a warning (conversion tracking loses the gclid). The conversion action that counts calls is not writable through Google's API.
243
252
 
244
253
  ---
245
254
 
@@ -313,19 +322,19 @@ baker ads google query --list-presets
313
322
 
314
323
  Read it before interpreting any null. Field coverage is folded page by page on the `--out` path too, so the file summary reports it; `.resource_name` is never reported (the server strips it from every row).
315
324
 
316
- **A page is not the data set.** Without `--all`, a query returns one page capped by `--limit` (default 200, or the `LIMIT` in the statement, whichever is smaller). With `--all` every page is followed, but the statement's `LIMIT` still caps the read — and preflight adds one at `--limit` to any query that arrives without it, so `--all` on its own still stops at 200 rows. When the rows fill whichever cap is in force, or the API returns a cursor the run did not follow, the response carries a `TRUNCATED_RESULTS` warning:
325
+ **A page is not the data set.** Every query is read a page at a time: without `--all` it returns one page of `--limit` rows (the preset's page size, else 200) and, when the account holds more, `pagination.cursor` — pass it back with `--cursor` (same query, same `--limit`) for the next page. `--all` follows every page until the account has no more rows; only a `LIMIT` written in the statement caps it. When the API returns a cursor the run did not follow, or the rows fill the statement's own `LIMIT`, the response carries a `TRUNCATED_RESULTS` warning:
317
326
 
318
327
  ```json
319
328
  {
320
329
  "ok": true,
321
330
  "warnings": [{ "code": "TRUNCATED_RESULTS", "message": "INCOMPLETE READ — ..." }],
322
- "pagination": { "hasMore": true, "cursor": "eyJwYWdl..." },
331
+ "pagination": { "hasMore": true, "cursor": "skip:200.k3x9q2:" },
323
332
  "data": [...],
324
333
  "notice": "Read the \"warnings\" above before using these rows: TRUNCATED_RESULTS."
325
334
  }
326
335
  ```
327
336
 
328
- Never total or count from a response carrying it — re-run with `--all --limit 8192 --out data.csv` (one read returns at most 8,192 rows; split a bigger data set into narrower queries — a shorter date range or one campaign each) and check the re-run came back without the warning. A read that returns one row by construction — `FROM customer` with no segment selected, such as `--preset account-summary` — has no page to fill, so it never carries `TRUNCATED_RESULTS`, nor `CONVERSION_MIX`, since one account-wide row has no campaigns to total or compare. Warnings reach every output format, so no `--output` choice can hide one:
337
+ Never total or count from a response carrying it — re-run with `--all --out data.csv` (it pages through every row, however many) and check the re-run came back without the warning. A read that returns one row by construction — `FROM customer` with no segment selected, such as `--preset account-summary` — has no page to fill, so it never carries `TRUNCATED_RESULTS`, nor `CONVERSION_MIX`, since one account-wide row has no campaigns to total or compare. Warnings reach every output format, so no `--output` choice can hide one:
329
338
 
330
339
  | `--output` | Where warnings land |
331
340
  |------------|---------------------|
@@ -366,9 +375,10 @@ The same lines also go to stderr. A read with nothing to report prints none of i
366
375
  | `--preset` | Named query template (see presets below) |
367
376
  | `--date-range` | Override preset date range: a Google literal (LAST_7_DAYS, LAST_30_DAYS, LAST_MONTH, etc.) or `BETWEEN 'YYYY-MM-DD' AND 'YYYY-MM-DD'` — there is no LAST_90_DAYS |
368
377
  | `--include-paused` | Include paused entities in preset results. Off by default: presets return only actually-serving entities (enabled campaign **and** ad group **and** ad). Google Ads has no single serving flag, so the whole chain must be `ENABLED` for an entity to serve. |
369
- | `--limit` | Max rows per page (default 200, at most 8,192) |
370
- | `--cursor` | Pagination cursor from previous response |
371
- | `--all` | Auto-paginate all results. Exhausts the pages, not the statement's `LIMIT` — pair it with a `LIMIT` in the statement above the expected row count (at most 8,192 — split bigger reads into narrower queries), and a read that still fills its cap is flagged `TRUNCATED_RESULTS` either way |
378
+ | `--gaql` | The GAQL statement as a flag, instead of the positional |
379
+ | `--limit` | Rows per page (default: the preset's page size, else 200; at most 8,192). A page size, not a cap |
380
+ | `--cursor` | `pagination.cursor` from the previous page (same query, same `--limit`) |
381
+ | `--all` | Follow every page until the account has no more rows. Only a `LIMIT` written in the statement stops it early, and a read that fills that `LIMIT` is flagged `TRUNCATED_RESULTS` |
372
382
  | `--out` | Write data to file (format from extension: .csv, .jsonl, .json) |
373
383
  | `--append` | Append to existing file (skip CSV headers) |
374
384
  | `--output` | Output format: `json` \| `csv` \| `jsonl` \| `md` |
@@ -389,9 +399,21 @@ The same lines also go to stderr. A read with nothing to report prints none of i
389
399
  | `negative-list-attachments`| Which campaigns each shared list covers | ALL_TIME |
390
400
  | `search-terms` | Actual user queries triggering ads | LAST_7_DAYS |
391
401
  | `ad-copy-performance` | Ad headline/description effectiveness | LAST_30_DAYS |
392
- | `asset-performance` | PMax asset performance labels | LAST_30_DAYS |
402
+ | `asset-performance` | PMax assets: serving status + metrics | LAST_30_DAYS |
393
403
  | `shopping-products` | Product-level shopping metrics | LAST_30_DAYS |
394
404
  | `account-summary` | Account-level totals | LAST_30_DAYS |
405
+ | `pmax-channels` | PMax spend/results per channel (Search, YouTube, Display, …) | LAST_30_DAYS |
406
+ | `pmax-placements` | Where PMax ads showed (impressions) | LAST_30_DAYS |
407
+ | `placements` | Display / Video / Demand Gen placements with cost and conversions | LAST_30_DAYS |
408
+ | `rsa-assets` | Each RSA headline/description: pin, approval, own metrics | LAST_30_DAYS |
409
+ | `landing-pages` | Final URLs with clicks, cost, speed score, mobile-friendly share | LAST_30_DAYS |
410
+ | `geo-performance` | Results by country and location type (in vs interested in) | LAST_30_DAYS |
411
+ | `age-performance` / `gender-performance` | Results by age range / gender per ad group | LAST_30_DAYS |
412
+ | `ai-max-search-terms` | AI Max search terms with the headline and landing page used | LAST_30_DAYS |
413
+ | `account-budget` | Account budget: limit, served, pending change | — |
414
+ | `recommendations` | Google's open recommendations with impact | — |
415
+ | `campaign-simulations` / `keyword-bid-simulations` | Google's what-if points (budget, tCPA, tROAS, CPC bid) | — |
416
+ | `campaigns`, `budgets`, `ad-groups`, `keywords`, `ads`, `assets`, `asset-groups`, `audiences`, `conversion-actions`, `bidding-strategies`, `labels`, `campaign-criteria`, `ad-group-criteria`, `ad-group-bid-modifiers`, `account-exclusions`, `shared-sets`, `campaign-negative-keywords`, `ad-group-negative-keywords` | Inventories behind the `<thing> list` commands | — |
395
417
 
396
418
  Every preset except the negatives lists and `account-summary` filters to **actually-serving entities** by default — the full status chain (`campaign.status = 'ENABLED' AND ad_group.status = 'ENABLED' AND ad_group_ad.status = 'ENABLED'`, as applicable) — and selects the status columns so you can see them. Pass `--include-paused` to widen to `!= 'REMOVED'` (keeps paused, still drops removed). Raw GAQL (no `--preset`) has no such default: a query over a serving-hierarchy resource without a status filter emits a `SERVING_SCOPE` warning reminding you to add the chain, because Google Ads has no single serving flag.
397
419
 
@@ -407,13 +429,12 @@ The CLI rejects or automatically corrects common GAQL mistakes before hitting th
407
429
 
408
430
  | Pattern | Behavior | Warning/error emitted |
409
431
  |---------|----------|-----------------------|
410
- | Missing LIMIT | Adds `LIMIT 200` | `"Added LIMIT 200. Use --limit to override."` |
411
432
  | Bare `keyword.text` / `keyword.match_type` | Rewrites to `ad_group_criterion.*` (resource-scoped forms like `shared_criterion.keyword.text` pass through untouched) | `"keyword.text → ad_group_criterion.keyword.text"` |
412
433
  | `shopping_performance_view.product_*` | Rewrites to `segments.product_*` | Field renamed |
413
434
  | `campaign.status = 'ACTIVE'` | Rewrites to `= 'ENABLED'` | Enum corrected |
414
435
  | `segments.conversion_action_name` with `metrics.cost_micros` | Rejects; split into separate conversion-action and spend queries | `INCOMPATIBLE_FIELDS` with `fix.action: "split_query"` |
415
436
  | `DURING` with a literal Google does not accept (`LAST_90_DAYS`, `LAST_12_MONTHS`, `THIS_YEAR` …) | Rejects before any request; `DURING` takes only `TODAY`, `YESTERDAY`, `LAST_7_DAYS`, `LAST_14_DAYS`, `LAST_30_DAYS`, `LAST_BUSINESS_WEEK`, `THIS_MONTH`, `LAST_MONTH`, `THIS_WEEK_SUN_TODAY`, `THIS_WEEK_MON_TODAY`, `LAST_WEEK_SUN_SAT`, `LAST_WEEK_MON_SUN` | `INVALID_DATE_RANGE`, with `fix.correctedCommand` spelling the same window as `BETWEEN 'YYYY-MM-DD' AND 'YYYY-MM-DD'` |
416
- | `--limit` outside 1–8,192, or a statement `LIMIT` above 8,192 | Rejects before any request — one read returns at most 8,192 rows | `VALIDATION_ERROR` naming the cap and how to split a bigger read; a statement `LIMIT` also gets `fix.correctedCommand` at `LIMIT 8192` |
437
+ | `--limit` outside 1–8,192 | Rejects before any request — one page holds at most 8,192 rows | `VALIDATION_ERROR` pointing at `--all --out` for bigger reads |
417
438
  | `FROM ad_group_criterion` without `negative` field | Warns | `"ad_group_criterion returns BOTH positive and negative keywords. Use --preset positive-keywords or negative-keywords."` |
418
439
 
419
440
  Auto-fixed queries proceed normally. Warnings appear in the response:
@@ -452,7 +473,7 @@ Google keeps account history in two layers, and neither reaches past 90 days:
452
473
 
453
474
  | Scope | Reach | What you get |
454
475
  |-------------|---------|---------------------------------------------------------------------|
455
- | `detail` | 30 days | What changed, old → new value, and who changed it |
476
+ | `detail` | 30 days | What changed — a `changes` list of `{ field, old, new }` per changed field — and who changed it |
456
477
  | `summary` | 90 days | Which resources were added, changed or removed — no values, no user |
457
478
 
458
479
  Beyond 90 days there is no API. Infer the change from a monthly spend trend per campaign
@@ -477,6 +498,43 @@ baker ads google changes --customer-id 1234567890 --days 60 --scope summary
477
498
 
478
499
  ---
479
500
 
501
+ ### `baker ads google keywords forecast` / `keywords themes`
502
+
503
+ ```bash
504
+ baker ads google keywords forecast --customer-id 1234567890 --text "running shoes:PHRASE,trail shoes:EXACT" --max-cpc 1.2 --location 2724 --language 1003
505
+ baker ads google keywords forecast --customer-id 1234567890 --text "crm software" --match-type BROAD --daily-spend 20 --strategy MAXIMIZE_CONVERSIONS --start 2026-11-01 --end 2026-11-30
506
+ baker ads google keywords themes --customer-id 1234567890 --keywords "trail shoes,road shoes" --ad-groups 111,222
507
+ ```
508
+
509
+ `forecast` returns Google's Keyword Planner forecast (v25) for the whole period under one bidding strategy: `{ source: "keyword_planner_forecast", period, forecast_clicks, forecast_cost_micros, forecast_conversions, forecast_average_cpc_micros, forecast_average_cpa_micros }`. `--max-cpc` alone is Manual CPC; `--daily-spend` is Maximize clicks (or Maximize conversions with `--strategy`). Without `--start/--end` it forecasts the 30 days from tomorrow. `themes` returns which existing ad group each keyword fits (`suggestions`) and the ad groups Google could not read a theme from (`unusable_ad_groups`).
510
+
511
+ ### `baker ads google previews` / `invoices` / `identity-verification` / `brands`
512
+
513
+ ```bash
514
+ baker ads google previews --customer-id 1234567890 --asset-groups 6743268912 # Performance Max, Google's preview page
515
+ baker ads google previews --customer-id 1234567890 --ads 111~222 # a Video campaign's ad, live on YouTube
516
+ baker ads google invoices --customer-id 1234567890 --month 2026-09
517
+ baker ads google identity-verification --customer-id 1234567890
518
+ baker ads google brands --customer-id 1234567890 --prefix nike # brand entity ids for a BRANDS list
519
+ ```
520
+
521
+ Read-only. A refusal that is the account's situation comes back as a structured error the agent carries on from: `NOT_INVOICED` (not billed by monthly invoicing — no invoices through the API), `VERIFICATION_NOT_READABLE` (verification status is only readable through the API on monthly invoicing), `NO_PREVIEW_FOR_AD_TYPE` (only video ads of Video campaigns preview by ad). Each `error.fix` says where the answer lives instead.
522
+
523
+ ### `baker ads google <thing> list`
524
+
525
+ What exists, with the id or resource name each write command takes — one inventory preset read through `query`, so it pages (`--limit`, `--cursor`, `--all --out`) and formats (`--output`) the same way. Paused entities are listed; removed ones are not; `--serving-only` narrows to what serves.
526
+
527
+ ```bash
528
+ baker ads google campaigns list --customer-id 1234567890
529
+ baker ads google keywords list --customer-id 1234567890 --all --out keywords.csv
530
+ baker ads google negative-keywords list --customer-id 1234567890 --level adGroup
531
+ baker ads google keyword-lists list --customer-id 1234567890 --members
532
+ ```
533
+
534
+ Lists exist for `campaigns`, `budgets`, `ad-groups`, `keywords`, `negative-keywords` (`--level campaign|adGroup`), `keyword-lists` (`--members` for the terms), `ads`, `assets`, `asset-groups`, `audiences`, `conversions`, `bidding-strategies`, `labels`, `campaign-criteria`, `ad-group-criteria`, `ad-group-bid-modifiers` and `account-exclusions`.
535
+
536
+ ---
537
+
480
538
  ### `baker ads google keywords discover`
481
539
 
482
540
  Discover keyword ideas from seed keywords or URLs.
@@ -484,6 +542,7 @@ Discover keyword ideas from seed keywords or URLs.
484
542
  ```bash
485
543
  baker ads google keywords discover --customer-id 1234567890 --seeds "running shoes,athletic footwear"
486
544
  baker ads google keywords discover --customer-id 1234567890 --url "https://competitor.com" --limit 50
545
+ baker ads google keywords discover --customer-id 1234567890 --seeds "running shoes" --cursor <next_page_token> # next page
487
546
  ```
488
547
 
489
548
  **Response:**
@@ -592,11 +651,13 @@ Write commands **never touch the Google Ads API at stage time**. Each command st
592
651
 
593
652
  Reference not-yet-created resources with `g_temp_*` refs returned by earlier `create` commands (they map to Google's temporary negative-ID resource names inside one atomic batch).
594
653
 
595
- **Staged ad copy is scanned for AI writing tells** (Google, Meta, and LinkedIn alike). When a headline, description, or primary text carries a construction that reads as machine-written — a negation pivot, a fake-candid opener, a participle hung off a comma, a vague attribution — the stage response carries an `AI COPY TELL` entry in `hints[]`. A headline set where several headlines are one claim with a swapped verb is reported as `synonym-cycling`: an AI tell *and* a real auction defect. These are advisory and can never fail a staged write, deliberately — refusing an ad over a fallible prose heuristic costs more than a tell that ships. Thresholds are stricter than the landing critic's (one occurrence is enough, since a 30-character headline has no room for a cluster), and conventions that are legitimate in ad copy are excluded outright: Title Case headlines, staccato ad rhythm, emoji, and curly quotes.
654
+ **Staged ad copy is scanned for AI writing tells** (Google, Meta, and LinkedIn alike), in whatever language the ad is written. When a headline, description, or primary text carries a construction that reads as machine-written — a negation pivot, a fake-candid opener, a participle hung off a comma, a vague attribution — the stage response carries an `AI COPY TELL` entry in `hints[]`. A headline set where several headlines are one claim with a swapped verb is reported as `synonym-cycling`: an AI tell *and* a real auction defect. Each sentence and each pair of headlines is read by the decision model (Jev, a fraction of a cent, billed with the workspace's next run; answers are kept in `canvas/.cache/`, so the same copy reads the same); when it cannot answer, no tell is reported rather than a guess. These are advisory and can never fail a staged write, deliberately — refusing an ad over a fallible prose heuristic costs more than a tell that ships. Thresholds are stricter than the landing critic's (one occurrence is enough, since a 30-character headline has no room for a cluster), and conventions that are legitimate in ad copy are excluded outright: Title Case headlines, staccato ad rhythm, emoji, and curly quotes.
596
655
 
597
656
  ```bash
598
657
  # Build a Search campaign bottom-up, chaining temp refs
599
658
  baker ads google budgets create --customer-id 1234567890 --name "Search budget" --amount 50
659
+ # Delivery is always STANDARD: --delivery ACCELERATED is refused (Google stores such a budget but will
660
+ # not attach it to a campaign).
600
661
  # → { ref: "g_temp_ab12", ... }
601
662
  baker ads google campaigns create --customer-id 1234567890 --name "Brand — Search" \
602
663
  --channel-type SEARCH --budget-ref g_temp_ab12 --bidding-strategy MANUAL_CPC \
@@ -617,6 +678,22 @@ baker ads google campaigns create --customer-id 1234567890 --name "Brand — Sea
617
678
  # New campaigns are declared as NOT containing EU political advertising (Google requires the
618
679
  # declaration on every create); pass --eu-political-ads only when the campaign does contain it.
619
680
  # On update, --eu-political-ads / --eu-political-ads=false corrects an existing declaration.
681
+ # Text guidelines (AI Max Search and Performance Max) stage on update only:
682
+ baker ads google campaigns update <campaign-id> --customer-id 1234567890 \
683
+ --term-exclusion "cheap" --messaging-restriction "Never promise a delivery date"
684
+ # --term-exclusion (repeatable, up to 25 × 30 chars) and --messaging-restriction (repeatable,
685
+ # up to 40 × 300 chars) each REPLACE the campaign's list; --clear-term-exclusions /
686
+ # --clear-messaging-restrictions empty one. A Search campaign needs AI Max with text
687
+ # customization on (in the same update or before), or staging is refused with the reason.
688
+ # AI Max for Search stages on create and update: --ai-max / --ai-max=false (off turns both
689
+ # features off too), --ai-max-text (text customization, needs AI Max) and
690
+ # --ai-max-url-expansion (final URL expansion, needs text customization). An omitted switch
691
+ # keeps the campaign's value; a combination Google refuses is refused at staging.
692
+ baker ads google campaigns update <campaign-id> --customer-id 1234567890 --ai-max --ai-max-text
693
+ # Bidding is per campaign type — staging refuses a pair Google does not take and names the ones it does.
694
+ # Target impression share (Search only) needs its whole goal on a new strategy:
695
+ baker ads google campaigns update <campaign-id> --customer-id 1234567890 --bidding-strategy TARGET_IMPRESSION_SHARE \
696
+ --impression-share 70 --impression-share-location ABSOLUTE_TOP_OF_PAGE --max-cpc 2
620
697
  baker ads google ad-groups create --customer-id 1234567890 --name "Brand terms" --campaign-ref g_temp_<campaign>
621
698
  baker ads google keywords add --customer-id 1234567890 --ad-group-ref g_temp_<adgroup> --text "brand name" --match-type EXACT
622
699
  # Shared negative keyword list → attach to the campaign
@@ -650,7 +727,7 @@ baker ads google ads create --customer-id 1234567890 --ad-group-ref g_temp_<adgr
650
727
  # Video ad — the YouTube video is its own asset first, then referenced by --video-assets
651
728
  baker ads google assets create --customer-id 1234567890 --file '{"type":"youtubeVideo","youtubeVideoId":"dQw4w9WgXcQ"}'
652
729
  baker ads google ads create --customer-id 1234567890 --ad-group-ref g_temp_<adgroup> --format video \
653
- --video-assets g_temp_<video> --final-url https://example.com
730
+ --video-assets g_temp_<video> --business-name "Acme" --logo-image-assets g_temp_<img_logo> --final-url https://example.com
654
731
  # Demand Gen ad — image refs the same way
655
732
  baker ads google ads create --customer-id 1234567890 --ad-group-ref g_temp_<adgroup> --format demandGen \
656
733
  --headlines "Discover the app" --descriptions "Try it free today" --business-name "Acme" --final-url https://example.com \
@@ -665,6 +742,8 @@ baker ads google ads create --customer-id 1234567890 --ad-group-ref g_temp_<adgr
665
742
  # App campaign ad — images are fields on the ad itself, same asset-ref flow
666
743
  baker ads google ads create --customer-id 1234567890 --ad-group-ref g_temp_<adgroup> --format app \
667
744
  --headlines "Insure your dog" --descriptions "Refunds in 2 hours" --image-assets g_temp_<img1>,g_temp_<img2>
745
+ # Call ads are retired — Google's API has no call ad, so --format call is refused. Stage a responsive
746
+ # search ad and have the user add the phone number as a call asset in Google Ads.
668
747
  # Change a live ad's content — every field you pass replaces its current value, so list what you want to keep
669
748
  baker ads google ads update <adId> --customer-id 1234567890 --format app \
670
749
  --headlines "Insure your dog" --descriptions "Refunds in 2 hours" \
@@ -686,9 +765,16 @@ baker ads google asset-groups attach --customer-id 1234567890 \
686
765
  baker ads google asset-groups detach customers/1234567890/assetGroupAssets/6718302454~402654762568~PORTRAIT_MARKETING_IMAGE \
687
766
  --customer-id 1234567890
688
767
 
768
+ # Audiences — each type carries what Google builds it from (a type alone is refused)
769
+ baker ads google audiences create --customer-id 1234567890 --file '{"name":"Pricing visitors","type":"RULE_BASED","rule":{"include":[{"conditions":[{"operator":"CONTAINS","value":"/pricing"}],"lookbackWindowDays":30}],"prefill":true}}'
770
+ # BASIC takes "conversionActions", LOGICAL "logic", LOOKALIKE "lookalike" — each naming lists/actions
771
+ # that already exist in the account (not g_temp_* refs). `baker ads google audiences create --help` has one example per type.
772
+
689
773
  # Review / undo / amend staged changes
690
774
  baker ads google draft list # readable campaign ▸ ad group ▸ ad tree + completeness advisories
691
775
  baker ads google draft list --json # raw JSON envelope for scripting
776
+ baker ads google draft validate # dry run the whole draft against Google (validateOnly) — per-op verdicts
777
+ baker ads google draft validate --full # every op, with the built request and Google's raw answer
692
778
  baker ads google draft show g_temp_ab12 # full staged payload — the receipt before publish
693
779
  baker ads google draft amend g_temp_ab12 --patch '{"amountMicros": 60000000}' # update in place, never remove+recreate
694
780
  baker ads google draft remove g_temp_ab12 # cascades to dependents
@@ -703,11 +789,21 @@ baker tag-manager draft show --chat <chat-id> gtm_temp_ab12
703
789
  baker actions draft --chat <chat-id>
704
790
  ```
705
791
 
706
- Command groups: `budgets`, `campaigns`, `ad-groups`, `keywords` (add/update/remove), `negative-keywords`, `keyword-lists`, `ads`, `assets` (create/update/attach/detach), `asset-groups` (create/update/attach/detach — Performance Max), `audiences`, `conversions` (create/update/goal/upload), `bidding-strategies`, `labels`, `campaign-criteria` (add/update/remove), and `draft`. Amounts are in major currency units (converted to micros). Money/bids: `--amount`, `--cpc-bid`, `--target-cpa`, `--target-cpc`, `--max-cpc` take major units; `--target-roas` a ratio. `--target-cpc` pairs with `--bidding-strategy TARGET_CPC` and is Demand Gen only. `--max-cpc` sets the max CPC bid ceiling for `TARGET_IMPRESSION_SHARE` / `TARGET_SPEND` / `PERCENT_CPC` and must be paired with `--bidding-strategy`. Less-common ops accept a `--file` payload — a path to a JSON file, or the JSON object written inline (flags override its keys). Updates target a resource name or bare id as the positional argument; a target that names an op staged earlier **amends it in place**.
792
+ **Budgets and portfolios** — `budgets create --total 300` stages a total budget (one amount over the campaign's flight; Search, Performance Max and Demand Gen; the campaign needs `--end-date`; never shared) instead of `--amount`'s daily one, and `budgets update --total` moves it. `campaigns create|update --portfolio-ref <ref>` puts a campaign on a portfolio bid strategy (checked against the campaign type); staging a campaign's own `--bidding-strategy` while it is on a portfolio takes it off (with a warning). `budgets remove <id>` and `bidding-strategies remove <id>` remove a budget / portfolio once no campaign uses it — staging refuses while one does, unless the same draft removes or moves those campaigns; publish removes the campaigns first.
793
+
794
+ **Shopping campaigns** — `campaigns create --channel-type SHOPPING --merchant-id <id>` (`merchant-center list` shows the Merchant Center accounts linked to the account; staging refuses any other) with optional `--feed-label`, `--shopping-priority low|medium|high` and `--local-inventory`; `campaigns update` changes those three (`--clear-feed-label`), never the Merchant Center. `ad-groups create --type SHOPPING_PRODUCT_ADS` creates the ad group with its "All products" listing group, and `ads create --format shoppingProduct` its product ad (no copy — the feed supplies it). A retail Performance Max campaign takes `--merchant-id` / `--feed-label` too.
795
+
796
+ **App, Travel and Hotel campaigns** — App: `campaigns create --channel-type MULTI_CHANNEL --sub-type APP_CAMPAIGN|APP_CAMPAIGN_FOR_ENGAGEMENT --app-id <package or App Store id> --app-store google|apple --app-goal installs|installs-volume|in-app|in-app-install-cost|value [--app-conversions <ids>]` (each goal takes one bidding strategy; App ads are created enabled — Google refuses a paused one, so `ads create --format app` defaults to `ENABLED`). Travel: `--channel-type TRAVEL --travel-account-id <id>` on Maximize conversion value with `--target-roas`. Hotel: `--channel-type HOTEL --hotel-center-id <id>`. Staging asks Google (validate-only) whether the app, the travel account or Hotel Center is there and refuses with what to ask the user when it is not. Smart campaigns are refused (their business setting cannot be validated).
797
+
798
+ **Dynamic Search Ads and frequency caps** — `campaigns create|update --dsa-domain <domain> --dsa-language <code> [--dsa-supplied-urls-only]` (Search only; an update may change one field; it cannot be removed) and `--frequency-cap <cap>/<day|week|month>[/<campaign|ad-group|ad>][/<impressions|video-views>]` (repeatable, replaces the campaign's caps; `--clear-frequency-caps`; Display and existing Video campaigns only).
799
+
800
+ **Performance Max creative controls** — `campaigns create|update` take `--term-exclusion` / `--messaging-restriction` (text guidelines; a Search create needs `--ai-max --ai-max-text`), the generated-asset switches `--generated-text --url-expansion --video-enhancement --image-enhancement --image-extraction --image-design-versions` (each `=false` to turn off), `--brand-guidelines` on create (link the business name and a square logo to the campaign in the same draft) and `--main-color --accent-color --font`. `campaigns enable-brand-guidelines <id> --business-name-asset --logo-assets [--landscape-logo-assets] [--final-url-domain] | --auto-populate` turns brand guidelines on for an existing campaign at publish (no dry run: `draft validate` shows it unchecked).
801
+
802
+ Command groups: `account` (update), `budgets` (create/update/remove), `campaigns`, `ad-groups`, `keywords` (add/update/pause/resume/remove), `negative-keywords` (add/update/remove — `update <resource name> --text --match-type` publishes as a replace of the live negative), `keyword-lists` (create/rename/delete a list — `--type NEGATIVE_KEYWORDS|NEGATIVE_PLACEMENTS|BRANDS|ACCOUNT_LEVEL_NEGATIVE_KEYWORDS`; add/remove its members — keywords, or `--placement/--app/--youtube-channel/--youtube-video/--brand`; attach/detach), `ads`, `assets` (create/update/attach/detach), `asset-groups` (create/update/attach/detach — Performance Max), `audiences`, `conversions` (create/update/goal/upload), `bidding-strategies` (create/update/remove), `labels` (create/update/remove; attach/detach on a campaign, ad group, ad or keyword — `--level --on --label-ref`), `campaign-criteria` (add/update/remove — locations, languages, schedule, devices, radius, carriers, operating systems, and exclusions of placements, apps, YouTube, topics, demographics, content types, IP addresses, web pages, brand lists), `ad-group-criteria` (add/update/remove — placements, topics, demographics, web pages, Demand Gen locations/languages/combined audience; `update --bid-modifier --max-cpc --status`), `ad-group-bid-modifiers` (add/update/remove — one ad group's own device bid), `account-exclusions` (add/remove — account-wide site, app, YouTube, content-type, IP and negative-keyword-list exclusions; `add --placement --youtube-channel --youtube-video --app --content-label --ip` comma-separated, or `--file`), `merchant-center` (list), and `draft`. `audiences attach` takes `--user-list`, `--interest` or `--custom-segment`. Amounts are in major currency units (converted to micros). Money/bids: `--amount`, `--cpc-bid`, `--target-cpa`, `--target-cpc`, `--max-cpc` take major units; `--target-roas` a ratio. `--target-cpc` pairs with `--bidding-strategy TARGET_CPC` and is Demand Gen only. `--max-cpc` sets the max CPC bid ceiling for `TARGET_IMPRESSION_SHARE` / `TARGET_SPEND` / `PERCENT_CPC` and must be paired with `--bidding-strategy`. Less-common ops accept a `--file` payload — a path to a JSON file, or the JSON object written inline (flags override its keys). Updates target a resource name or bare id as the positional argument; a target that names an op staged earlier **amends it in place**.
707
803
 
708
804
  **A device criterion can be added, but never removed** — `campaign-criteria add` with a device criterion works, but Google then rejects `campaign-criteria remove` on it forever, including on a criterion you added yourself (and Baker refuses the removal before publish). Adjust it instead with `campaign-criteria update customers/<cid>/campaignCriteria/<campaignId>~<criterionId> --bid-modifier <n>`: `1` for no adjustment, `1.25` to bid 25% more, `0.9` to bid 10% less, `0` to stop serving on that device. Locations, languages and ad schedules remove normally.
709
805
 
710
- **Images on a Search campaign** — `assets create` with an image publishes it into the advertiser's Google Ads asset library, but Google refuses the asset link that puts an image extension on a Search campaign (`AD_IMAGE` and every marketing-image field type, at campaign, ad group and customer level alike), so `assets attach` **skips** it at stage time and names what to do instead — the op is never stored, the rest of a batch stages normally, and the skipped link is what records which images belong on which campaign. Staging an image, or asking to link one the account already holds, therefore also stages a **Task** — "Add N images to <campaign> in Google Ads" — which applies with the rest of the chat, so the one step Google keeps inside its own interface is tracked rather than mentioned. Attach the image to a Performance Max asset group (`asset-groups attach`) and the Task withdraws itself. The one image a Search campaign does take through a link is the advertiser's logo: `BUSINESS_LOGO`, at campaign or customer level, on a verified account.
806
+ **Images on a Search campaign** — `assets create` with an image publishes it into the advertiser's Google Ads asset library, and `assets attach` with field type `AD_IMAGE` (campaign or ad group level) puts it on a Search campaign. Google opens Search image links per advertiser account, so staging asks Google with a check that changes nothing: an account that takes them stages the link, which applies at publish like a sitelink; one that does not — or that Google could not confirm either way — has the link **skipped** — never stored, the rest of a batch stages normally — and a **Task** is staged instead, "Add N images to <campaign> in Google Ads", which applies with the rest of the chat. An image staged with no accepted link raises that Task too, and it withdraws itself once an accepted link (or a Performance Max `asset-groups attach`) names the image. `MARKETING_IMAGE` and its square/portrait variants are Performance Max asset-group slots and are sent back to `AD_IMAGE`. The image must be 1.91:1 or 1:1; another shape is sent back to be cropped. The advertiser's logo links as `BUSINESS_LOGO`, at campaign or customer level, on a verified account.
711
807
 
712
808
  **Primary vs secondary conversion actions** — `conversions update <id> --primary` makes an action a primary action (automated bidding optimizes toward it); `--no-primary` (or `--primary=false`) demotes it to secondary (reported only) — the spaced form `--primary false` is rejected, since a boolean flag written with a space is set to true and the word dropped. `conversions create` stages a new action as **secondary** unless its `--file` payload sets `"primaryForGoal": true`, so a new action never silently joins the bidding target of a Maximize Conversions or Target CPA campaign. The staged card shows an "Action optimization" row with the before/after role.
713
809
 
@@ -717,16 +813,22 @@ Command groups: `budgets`, `campaigns`, `ad-groups`, `keywords` (add/update/remo
717
813
 
718
814
  **Retiring a conversion action** — `status` accepts `ENABLED` and `REMOVED` only. Google denies `HIDDEN` on every conversion-action write, so to stop bidding toward an action while it keeps measuring use `--no-primary`, and to stop it counting at all use `"status": "REMOVED"`. Conversion actions **Google owns** cannot be changed at all — Smart campaign call/directions actions, the in-ad lead form's submit, YouTube subscriptions and follow-on views, Floodlight and Search Ads 360 imports, store visits, codeless webpage actions. Staging any edit to one, a rename included, is refused up front; there is no manual route either, since the Google Ads interface will not change them.
719
815
 
816
+ **Validating the draft** — `baker ads google draft validate` sends the whole staged draft to Google exactly as publish would (same batching and order), with `validateOnly: true`, so nothing is applied. Each op comes back `accepted`, `refused` (with the reason publish would record), `blocked` (a change it depends on is refused), `unchecked` (only publish can check it — a new asset group, whose text assets are created first) or `applied` (an earlier publish did). Compact by default (counts plus the ops that need attention); `--full` lists every op with its request and Google's raw answer.
817
+
720
818
  **Reviewing the draft** — `baker ads google draft list` renders everything you've staged as a grouped campaign ▸ ad group ▸ ad tree (with non-blocking completeness advisories), the CLI counterpart to the dashboard's Google Ads tab. Pass `--json` for the raw envelope. Once the chat has published it reports the outcome instead — `published · N applied, M failed`, with a `✗` line naming each op that did not land and why. Aim for a fully built campaign — 2–4 ad groups, ≥5 keywords each, 2–4 RSAs with 8–12 headlines, ≥4 sitelinks / ≥3 callouts / ≥1 structured snippet, and ≥1 shared negative list; the advisories flag what's still thin.
721
819
 
722
820
  **Amending a staged op** — `draft amend <ref> (--file patch.json | --patch '<json>')` merges a JSON patch into an already-staged op's payload (objects deep-merge, `null` deletes a key, arrays/scalars replace) and re-validates it in full. Always prefer this over removing and re-staging — it preserves the op's ref (so dependents don't break) and its position in the draft. `draft show <ref>` prints the full staged payload (including warnings/annotations) so you can verify a change looks right before publish.
723
821
 
822
+ **Tracking warnings** — Tag Manager stage, amend, list and show responses provide repair hints whenever warnings remain. The compact `tag-manager draft list` prints the warnings beside their changes. Resolve the intended event role and technical gaps, then read the draft again before finishing; progress and diagnostics are not counted just to clear an alert. Adding and then removing a conversion that was never live cancels that proposal, so it disappears from the review.
823
+
724
824
  **Reading an earlier chat's changes** — `draft list` and `draft show` take `--chat <chat-id>` to read another chat in the same company instead of the current one. Every staged-draft family accepts it: `ads google|meta|linkedin draft list|show`, `tag-manager draft list|show`, `actions draft`, `tags draft`, and `scheduled-agents list|get` (`list` leaves briefs out; `get <id>` or `list --full` returns them). A published draft is kept in full, so this returns what that chat actually wrote — every headline, bid and target — plus each op's outcome and error. Use it when asked to redo or reapply earlier work rather than reconstructing copy from a summary. Read-only: staging, `amend`, `remove` and `clear` stay bound to `BAKER_CHAT_ID`, so one chat can never edit another's draft. To re-run only the changes a publish failed to land, use the chat's **Retry** control in the dashboard — it re-sends exactly those and leaves the ones that applied untouched.
725
825
 
726
826
  **Already-satisfied writes aren't staged** — an update or pause/resume whose every field already holds the requested value on the live account is dropped instead of staged: the envelope returns `"staged": false, "noop": true` with a `reason` ("this ad is already Paused on Google Ads"), plus a hint. This is a success, not an error — there is nothing to publish. Only a positively-read live snapshot can trigger it; a failed or empty read always stages. Batch responses list the dropped ops under `skipped`, and `count` reflects what actually entered the draft.
727
827
 
728
828
  **Batch keyword adds** — `keywords add`, `negative-keywords add`, and `keyword-lists add` take a whole batch in one command: comma-separate `--text` entries and/or pass `--file <list.txt>` (one keyword per line). A `:EXACT`/`:PHRASE`/`:BROAD` suffix per entry overrides the `--match-type` default. Batches stage all-or-nothing as one request (limit 500); each keyword still lands as its own draft op, so it stays individually removable/amendable.
729
829
 
830
+ **Bulk changes — one file, one call** — `draft stage --file ops.json [--customer-id <id>]` stages up to 500 ops of any mix of kinds in one request: the file is a JSON array (or JSON Lines) of whole ops `{"kind","customerId"?,"target"?,"payload"?}`; `--customer-id` fills ops that leave it out. All-or-nothing on validity (an error names `ops[i]`); ops the account already satisfies come back as `skipped`. An op may name a `g_temp_*` ref from an earlier call, not one from the same file. `draft amend --file amends.json` (no ref) amends many staged ops at once from `[{"ref","patch"}, …]`, all or none. The `--file` create commands (`assets create|attach`, `audiences create`, `conversions create`, `bidding-strategies create`, `labels attach`, `campaign-criteria add`, `ad-group-criteria add`) take a JSON array of payloads too. `keywords pause|resume <target>…` (or `--file`, one resource name per line) change many keywords' status in one call, and `keyword-lists remove --list-ref <id> --text "a,b:EXACT"` takes terms out of a shared negative list — the CLI reads the list to find each member (no `:MATCH_TYPE` = every match type), or takes members by resource name / `listId~criterionId`. Five single stages of one kind in a row on a chat earn a `LOOP:` hint pointing at these. A chat's draft holds any number of ops (each is its own record); only one op over `GOOGLE_DRAFT_OP_MAX_BYTES` (512 KB) is refused — the stage or amend stages nothing and answers `OP_TOO_LARGE` with an `error.fix` (split that change).
831
+
730
832
  **Pausing or removing a keyword returns a per-ad-group hint** — `keywords update --status PAUSED|REMOVED` and `keywords remove` come back with hints telling you to base the decision on `keyword_view` rows carrying `ad_group.id` + `ad_group_criterion.criterion_id`, never on campaign totals. A keyword's criterion id is derived from its text + match type, so every copy of the same keyword across ad groups shares one id and any aggregate blends the copy holding the history with the copies that never served. The `keyword-analysis` and `positive-keywords` presets select both ids so one query identifies each copy. A bid or URL edit gets no hint — only a pause/removal can strand history.
731
833
 
732
834
  ---
@@ -863,6 +965,48 @@ baker ads x targeting-constants --constant tv_shows --locale en-US
863
965
 
864
966
  ---
865
967
 
968
+ ### ChatGPT Ads (`baker ads chatgpt`)
969
+
970
+ Read the company's ChatGPT Ads account (OpenAI's ads inside ChatGPT conversations) and how it performs, and stage changes to it that apply when the chat is published. The company connects one account with an API key from its Ads Manager, pasted in the chat's connect card (`request_connection` `{ platform: "chatgpt-ads" }`) or in Brain → Integrations → ChatGPT Ads; the key stays in Baker and never reaches the CLI, so there is no account id to pass.
971
+
972
+ | Subcommand | What it returns |
973
+ |---|---|
974
+ | `account` | Name, currency, timezone, status, and the brand and account reviews that decide whether anything serves |
975
+ | `campaigns` | Campaigns: objective, daily or lifetime budget (currency units), schedule, locations, platforms, serving issues |
976
+ | `ad-groups` | Ad groups (`--campaign-id`): bid strategy, max bid, context hints, serving issues |
977
+ | `ads` | Ads (`--ad-group-id`): the card's copy, image, destination, review outcome, serving issues |
978
+ | `conversions` | Pixels (with the pixel id a landing page installs) and conversions, with the campaigns using each |
979
+ | `insights` | Impressions, clicks, spend, CTR, CPC, CPM, conversions, CPA, ROAS — per `--level account\|campaign\|ad_group\|ad`, over `--days N` or `--since/--until`, `--granularity`, `--segment country\|device\|platform\|product` |
980
+ | `campaigns create\|update\|pause\|resume\|archive` | Stage a campaign change: objective, daily or lifetime budget (currency units), schedule, countries and regions included and excluded, audiences, platforms, tracking template, status, the conversions it measures (`--conversion-event-ids`), a catalog (`--catalog-id`) |
981
+ | `ad-groups create\|update\|pause\|resume\|archive` | Stage an ad group change: context hints, bid strategy, max bid, audience bid adjustments, tracking template, product filters, status |
982
+ | `ads create\|update\|pause\|resume\|archive` | Stage a chat card: title, body, landing page, image and its crop, tracking template, status |
983
+ | `ads create-product` | Stage a catalog ad group's product ad: a title template the catalog's products fill in |
984
+ | `pixels create` | Stage a new website pixel |
985
+ | `audiences` | Custom audiences, with `create\|add\|remove\|archive` — members hashed on your machine, never sent raw |
986
+ | `catalogs` | Product catalogs, with `uploads` (how each upload went) and `products` (preview filters) |
987
+ | `conversions report` | Conversions split into click-through and view-through, with the attribution windows |
988
+ | `history` | What changed in the account, by anyone, with each field before and after |
989
+ | `account update` | Stage the advertiser's brand name or icon (`--favicon-url`) |
990
+ | `conversions create` | Stage a conversion — a standard event (`lead_created`, `order_created`, …) or a custom one by name — counted from the account's pixel |
991
+ | `conversions events` | What the pixel delivered in the last ~15 minutes — proof a site sends the event |
992
+ | `locations <place>` | Regions, cities and markets ChatGPT Ads can target, with the id `--location-ids` takes |
993
+ | `draft` | Review the staged changes as a tree; `show`, `amend`, `remove`, `clear`. After a publish, what applied and what failed |
994
+
995
+ `--full` on the list commands returns ChatGPT Ads' own objects. ChatGPT Ads refuses some metric and breakdown combinations (CPA, conversion rate and sales cannot be broken down; conversions come daily or as one total); `insights` refuses them first and says why.
996
+
997
+ Writes need `BAKER_CHAT_ID`: they stage on the chat and reach ChatGPT Ads only when it is published. New entities are created paused unless `--status active`.
998
+
999
+ ```bash
1000
+ baker ads chatgpt account
1001
+ baker ads chatgpt insights --days 30 --granularity daily --level account
1002
+ baker ads chatgpt insights --campaign-id cmpn_123 --level ad --sort spend
1003
+ baker ads chatgpt campaigns create --name "Spring push" --objective clicks --daily-budget 20 --countries ES
1004
+ baker ads chatgpt ad-groups create --campaign-id chatgpt_temp_ab12 --name "Rooftop owners" --context-hints "solar panels for my house" --max-bid 2.50
1005
+ baker ads chatgpt draft
1006
+ ```
1007
+
1008
+ ---
1009
+
866
1010
  ### Meta Ads (`baker ads meta`)
867
1011
 
868
1012
  Meta Marketing API (Facebook + Instagram). Connect via OAuth in the dashboard, pick which ad accounts to scope to, then call from the CLI. Account ID via `--account-id act_123` or `BAKER_META_AD_ACCOUNT_ID` env var.
@@ -908,6 +1052,11 @@ baker ads meta ads --account-id act_123 --all-statuses # widen to
908
1052
  # "Find the October ad" — Meta matches the name, so only that row is read
909
1053
  baker ads meta ads --account-id act_123 --all-statuses --name-contains octubre --fields id,name,adset_id,creative_id
910
1054
 
1055
+ # "Who will see an ad in this campaign?" — a viewer per ad set: cold/warm/hot, what they
1056
+ # already know (brand/offer/product), placements, and why — from targeting, audiences, objective,
1057
+ # and what each audience's name says (buyers, a lookalike), read in the advertiser's language
1058
+ baker ads meta adsets --campaign-id 6123 --all-statuses --audience --output json
1059
+
911
1060
  # "Tell me about ad 9988"
912
1061
  baker ads meta creatives --creative-id 9988
913
1062
  baker ads meta preview --creative-id 9988 --ad-format MOBILE_FEED_STANDARD --out-file /tmp/p.html
@@ -1067,10 +1216,24 @@ baker ads linkedin analytics --level campaign --campaign-id 1234 \
1067
1216
  # "Top companies seeing this campaign" (ABM feedback loop) — org names auto-resolved
1068
1217
  baker ads linkedin top-companies --campaign-id 1234 --last-days 30
1069
1218
 
1219
+ # "Which companies are our ads reaching?" — one row per company over the whole period,
1220
+ # most engaged first ({org, name, impressions, clicks, engagements, costLocal}).
1221
+ # Defaults: last 12 calendar months, the only selected ad account, top 100 (--full for all).
1222
+ baker ads linkedin companies --account-id 1234
1223
+ baker ads linkedin companies --page 2 # companies 101-200, most engaged first
1224
+ baker ads linkedin companies --by month --min-impressions 50 # months[] per company
1225
+ baker ads linkedin companies --campaign-id 1,2 --by campaign # campaigns[] per company
1226
+ baker ads linkedin companies --with-domains --stats --output csv # + domain/domainSource; read-quality stats
1227
+
1070
1228
  # "Who are these org URNs?" — resolve org URNs to company names (probe leakage)
1071
1229
  baker ads linkedin resolve --urns urn:li:organization:17719,urn:li:organization:19022
1072
1230
  baker ads linkedin resolve --ids 17719,19022 --output csv
1073
1231
 
1232
+ # "What are these companies' website domains?" — for matching against CRM companies.
1233
+ # Per org: vanityName (LinkedIn page), website, domain, domainSource (website | subdomain | none);
1234
+ # ids LinkedIn doesn't know land in `errors`. Shortlinks are never followed.
1235
+ baker ads linkedin resolve --ids 1035,1337 --with-domains
1236
+
1074
1237
  # "Who is in our audience by industry/seniority/function/title — all at once?"
1075
1238
  baker ads linkedin demographics --campaign-id 1234 --last-days 30
1076
1239
 
@@ -1107,7 +1270,8 @@ baker ads linkedin bid-pricing --account-id 503001492 \
1107
1270
  **`analytics`** — the 3-axis workhorse:
1108
1271
  - `--level account` (override: `campaign-group|campaign|creative`)
1109
1272
  - `--intent baseline` field bundle. Intents: `baseline | revenue | funnel | engagement | video | lead-gen | inmail | document | ranking | identity`. List with `--list-intents`.
1110
- - `--pivot none`. Pivots: `none | campaign | campaign-group | creative | company | account | conversion | job-title | job-function | seniority | industry | company-size | country | region | device | placement | serving-location | card-index | objective | conversation-node | conversation-node-button`. List with `--list-pivots`.
1273
+ - `--pivot none`. Pivots: `none | campaign | campaign-group | creative | company | account | conversion | job-title | job-function | seniority | industry | company-size | country | region | dma | device | placement | serving-location | card-index | objective | conversation-node | conversation-node-button`. List with `--list-pivots`.
1274
+ - `--pivot dma` — US Nielsen Designated Market Areas, with region names resolved. LinkedIn's terms: any report built from it writes "DMA®" each time and carries "DMA® is a registered service mark of The Nielsen Company." — the `DMA_ATTRIBUTION_REQUIRED` warning repeats this on every call.
1111
1275
  - `--granularity DAILY`. Auto-forced to `ALL` when pivoting on a `MEMBER_*` dim (LinkedIn rejects DAILY + demographic) — surfaced in stderr + `warnings`.
1112
1276
  - `--last-days 7` unless you pass `--start`/`--end`.
1113
1277
  - Sort: `costInUsd` descending. `--no-sort` to disable.
@@ -1139,6 +1303,7 @@ creatives # default ACTIVE intended-status
1139
1303
 
1140
1304
  analytics # see "Smart defaults" above (3-axis: level × intent × pivot)
1141
1305
  demographics # sweep all firmographic pivots in one call
1306
+ companies # which companies the ads reached, one row per company (--by month|campaign, --with-domains)
1142
1307
  top-companies # pivot=MEMBER_COMPANY shortcut (ABM)
1143
1308
  conversation # pivot=CONVERSATION_NODE_BUTTON shortcut (Sponsored Messaging)
1144
1309
 
@@ -1158,6 +1323,7 @@ audit --format md # deliverable-ready markdown table
1158
1323
  campaign-groups create|update|pause|resume|duplicate # staged writes (see below)
1159
1324
  campaigns create|update|pause|resume|archive|duplicate
1160
1325
  campaigns url-params <ad-set-id> # UTMs for every ad in the ad set
1326
+ campaigns exclude-companies <ad-set-id> --org <ids> # stop an ad set reaching these companies
1161
1327
  account url-params # UTMs for every ad in the whole account
1162
1328
  url-params [--ad-sets] # read what is tagged now, and what lands twice
1163
1329
  creatives create|update|pause|resume|duplicate
@@ -1198,12 +1364,21 @@ baker ads linkedin account url-params --param "utm_source=linkedin&utm_medium=pa
1198
1364
  baker ads linkedin campaigns update 123456 --audience-expansion off --lan off
1199
1365
  baker ads linkedin campaigns update 123456 --bid 7.50 --daily-budget 100 --currency USD
1200
1366
 
1201
- # Matched audiences (CSV with header; email columns SHA-256 hashed locally)
1367
+ # Matched audiences (CSV with header; email columns SHA-256 hashed locally).
1368
+ # Company lists take companyName,domain,organizationUrn — organizationUrn matches the company page exactly
1202
1369
  baker ads linkedin audiences create --name "ABM Tier-1" --type company-list --list-file accounts.csv
1203
1370
 
1371
+ # Stop an ad set reaching customers / open deals — add-only; at publish Baker re-reads the ad set
1372
+ # and merges into its targeting as it stands then. --org takes company ids, ≤200 per call;
1373
+ # a longer list goes into a company-list audience passed as --segment
1374
+ baker ads linkedin campaigns exclude-companies 123456 --org 1035,1441
1375
+ baker ads linkedin campaigns exclude-companies 123456 --segment urn:li:dmpSegment:42
1376
+
1204
1377
  # Conversions with playbook-default windows
1205
1378
  baker ads linkedin conversions create --name "Demo booked" --type LEAD --method INSIGHT_TAG \
1206
1379
  --post-click-window 30 --view-window 7
1380
+ # Windows are 1|7|30|90 days; 180 and 365 only for LEAD, QUALIFIED_LEAD, MARKETING_QUALIFIED_LEAD,
1381
+ # SALES_QUALIFIED_LEAD, PURCHASE, ADD_TO_CART, SUBMIT_APPLICATION — refused at stage time otherwise
1207
1382
 
1208
1383
  # Safe duplication — stage-time read of the source → staged DRAFT create ("Duplicate ad", never "Link to original")
1209
1384
  baker ads linkedin campaigns duplicate 123456 --name "New variant"
@@ -1262,7 +1437,7 @@ Each finding: `{id, area, check, status, severity, evidence, fix: {explanation,
1262
1437
  - OAuth tokens auto-refresh server-side. On 401, reconnect LinkedIn from dashboard → integrations.
1263
1438
  - Account scoping: backend rejects calls against an account not in the picker selection.
1264
1439
  - Pinned API version: `Linkedin-Version: 202606` (declared in `oauth/constants.ts`). Bump as a coordinated change — LinkedIn deprecates monthly with a ~12-month support window. The floor is `TRACKING_PARAM_DYNAMIC_MIN_API_VERSION` (`CREATIVE_NAME` needs 202606), asserted in `ads/linkedin/utils.test.ts`.
1265
- - Cache TTLs: accounts/account-detail 1h, listings 30m, analytics 15m–6h (depends on date range), facets 7d, urn-resolve 1h, bid-pricing/forecast 6h, audience-size 1h.
1440
+ - Cache TTLs: accounts/account-detail 1h, listings 30m, analytics 15m–6h (depends on date range), facets 7d, urn-resolve 1h, org domains 7d, bid-pricing/forecast 6h, audience-size 1h.
1266
1441
  - `--no-cache` on any command skips both cache layers (the CLI's own and the server's).
1267
1442
 
1268
1443
  #### LinkedIn-specific gotchas
@@ -1318,6 +1493,7 @@ baker analytics tracking # do the ad URLs carry the campaign, a
1318
1493
  baker analytics tracking --platform meta # the same for one platform, read from their own ad account
1319
1494
  baker analytics map --platform google --set kw=keyword # name a parameter, seen or not yet
1320
1495
  baker analytics map --platform google --remove kw # take that answer back
1496
+ baker analytics unstage --kind mapping --ref google/kw # take back what this chat staged (mapping|conversion|send|source), or --all
1321
1497
  baker analytics conversions # what counts, what each produced, and which Forms count nothing
1322
1498
  baker analytics conversions --candidates --output md # everything these pages do, and which of it is counted, as tables
1323
1499
  baker analytics conversions --candidates # the same as compact JSON: one line per page event, Form keys folded onto `flows`
@@ -1390,7 +1566,7 @@ Shared flags: `--days <n>` (default 30) or `--start-date` / `--end-date` (`YYYY-
1390
1566
 
1391
1567
  **`tracking` answers "can this account be reported on at all", and it reads the ad account to do it.** `analytics ads` can only describe parameters that have already arrived, which leaves two blind spots that mislead in the expensive direction: a platform with no traffic in the window looks exactly like a platform whose ads carry nothing, and a dimension arriving under a name nobody has mapped looks exactly like one nothing carries. `baker analytics tracking` reads the mapping, the traffic **and** the client's own Google tracking templates / Meta URL parameters, so it can answer for a platform that has not had a click yet. Every dimension comes back in one of four states, and they are four different jobs: `covered` is done; `unmapped` means the URLs already carry it under a name Baker does not read, so run the `mapCommand` in the response — free, and it repairs the traffic already collected; `missing` means nothing carries it; `unknown` means no visit arrived *and* the account could not be read, which is not a finding and must never be reported as untagged.
1392
1568
 
1393
- **`fix` says which level to put the missing pairs at, and who can write it.** Not a field name — the levels that platform accepts tagging at, widest first (the widest is the one to prefer: it covers campaigns nobody has created yet), which of them Baker can stage and which the user has to do by hand, and how each field treats a value already in it. That last part is the one people skip and it differs per platform: a Google **campaign** final URL suffix *replaces* the account one rather than merging, LinkedIn's `url-params` call *sets the ad set's whole parameter set* and Baker cannot read back what is in it, and a Meta creative is immutable so a live ad cannot be retagged through Baker at all. `suffix` is only ever the **missing** pairs, so the rule everywhere is the same: read what is there, work out what is missing, write the union. Where `fix` says Baker has no write surface — Microsoft, TikTok, Reddit, Pinterest, Snapchat — that is a request to hand the user with the exact string and the exact screen, never a reason to stop on the platforms it can write.
1569
+ **`fix` says which level to put the missing pairs at, and who can write it.** Not a field name — the levels that platform accepts tagging at, widest first (the widest is the one to prefer: it covers campaigns nobody has created yet), which of them Baker can stage and which the user has to do by hand, and how each field treats a value already in it. That last part is the one people skip and it differs per platform: a Google **campaign** final URL suffix *replaces* the account one rather than merging, LinkedIn's `url-params` call *sets the ad set's whole parameter set* and Baker cannot read back what is in it, and a Meta creative is immutable so a live ad cannot be retagged through Baker at all. `suffix` is only ever the **missing** pairs, so the rule everywhere is the same: read what is there, work out what is missing, write the union. Where `fix` says Baker has no write surface — Microsoft, TikTok, Reddit, Pinterest, Snapchat — that is a request to hand the user with the exact string and the exact screen, never a reason to stop on the platforms it can write. ChatGPT Ads is one Baker can write: its campaign and ad group `--tracking-template` (`baker ads chatgpt campaigns|ad-groups update`), which should carry `campaign_id={campaign_id}&ad_group_id={ad_group_id}&ad_id={ad_id}` — an id that only rides in `utm_campaign` or `utm_content` is reported as that UTM and never as the campaign or the ad, so `--platform chatgpt --role adgroup_id` has nothing to split by.
1394
1570
 
1395
1571
  **The one recommendation about the ad account itself.** `baker analytics ads --platform <p>` warns when that platform's traffic carries no campaign id, ad group id or ad id — the three tiers every "which ad worked" question is grouped by — and returns the exact URL suffix, in the platform's own macro language, that would start sending the missing ones. It is the only mapping gap that cannot be fixed afterwards: naming a parameter Baker already captures corrects the history, because roles are resolved when a report is read, but a parameter the ads never sent has no stored values for a meaning to apply to. The hint stays quiet for a platform with little traffic, and defers to the untagged-account finding rather than repeating it.
1396
1572
 
@@ -1463,14 +1639,15 @@ baker experiment update --id a3f91c2b --because "…" # a live one
1463
1639
  baker experiment status # the verdict on every test, first; dates as calendar dates
1464
1640
  baker experiment status --id a3f91c2b --full # one test, with everything the dashboard reads and the posteriors
1465
1641
  baker experiment history --landing oferta # every finished test on a page: claim, numbers, decision, learning
1642
+ baker experiment start … --hold # publish both versions with the split off, for the client to approve by link first
1466
1643
  baker experiment pause --id a3f91c2b # everyone sees the original for now
1467
- baker experiment resume --id a3f91c2b # the split picks up with the same sides
1644
+ baker experiment resume --id a3f91c2b # the split picks up with the same sides; a held test starts here, dated from now
1468
1645
  baker experiment finish --id a3f91c2b --keep variant --learning "…" # end it now; say which version stays and what it taught
1469
1646
  baker experiment cancel --id a3f91c2b # call it off: staged → gone, live → ended, original kept
1470
1647
  baker experiment fold # make the page's own content the version that was kept
1471
1648
  ```
1472
1649
 
1473
- **A page has variants. A variant is never a page.** The variant is a real built folder at `src/pages/oferta--1/`, so it gets every quality gate a page gets — which matters, because it is about to be shown to half the client's visitors. What it does not get is a public address: it is absent from the Landings list and the sitemap, names the page's URL as its canonical, and `/oferta--1/` returns **404** at the edge. It is as indexable as the page it is a version of, deliberately — half the crawls of `/oferta/` are served the variant, so a `noindex` on it is a `noindex` on the live page. Both versions answer at `/oferta/`. Assignment happens while the HTML is served, so there is no flicker and nothing for an ad blocker to suppress, and the split is a path rewrite and never a redirect. To look at one side deliberately, `/oferta/?_bkr_preview=a` and `?_bkr_preview=b` serve it exactly, with no assignment and no cookie.
1650
+ **A page has variants. A variant is never a page.** The variant is a real built folder at `src/pages/oferta--1/`, so it gets every quality gate a page gets — which matters, because it is about to be shown to half the client's visitors. What it does not get is a public address: it is absent from the Landings list and the sitemap, names the page's URL as its canonical, and `/oferta--1/` returns **404** at the edge. It is as indexable as the page it is a version of, deliberately — half the crawls of `/oferta/` are served the variant, so a `noindex` on it is a `noindex` on the live page. Both versions answer at `/oferta/`. Assignment happens while the HTML is served, so there is no flicker and nothing for an ad blocker to suppress, and the split is a path rewrite and never a redirect. To look at one side deliberately, `/oferta/?_bkr_preview=a` and `?_bkr_preview=b` serve it exactly, with no assignment and no cookie — and they keep answering while the test is paused and after it ends, up to `fold`, which removes the other version's file. That window is how a client is shown a version at all.
1474
1651
 
1475
1652
  **The goal is one event, named by its key.** `--goal submit:book-a-call` — the same vocabulary the event stream uses (`form:<flow>:<node>:<trigger>`, `submit:<flow>`, `page:<name>`, `exit:<host>`), and `baker analytics conversions --candidates` lists the keys these pages produce. Not a conversion *name*, which groups several keys: a variant that swaps the page's Form for a shorter one converts on a different key, and a name would either count both endings as one outcome or count the new version as never converting at all — `--variant-goal` gives the new version its own event when it has one. The goal is fixed for the life of the test and the verdict is read on it alone. Every other conversion the company counts comes back under `secondary` on every result, with the same interval and a one-word reading, and decides nothing. A conversion counts for a visitor wherever it happens after they were shown a version — on the page, on another page, in a delivered lead, or in a deal the CRM posted a week later by email.
1476
1653
 
@@ -1490,6 +1667,26 @@ baker experiment fold # make the page
1490
1667
 
1491
1668
  **`plan` before building anything.** At a 3% conversion rate a +20% lift needs roughly 14,000 visitors *per variant*; `plan` says how long that is at this page's traffic, lists what this page's visitors actually went on to do, and what a 14, 28, 56 or 90-day window could settle. The duration is a guide and never a finish line — the test ends when the numbers are clear, and a page with no traffic yet still runs, sized on a stand-in that `warning` names (`--baseline 0.05` states the rate to expect). What `plan` refuses is what cannot be measured: no conversion defined, or a goal the page has demonstrably never produced. `--lift` sets the smallest lift worth detecting (default `0.2`) — halving it roughly quadruples the traffic needed.
1492
1669
 
1670
+ ### Site settings (`baker site`)
1671
+
1672
+ The company's site settings — cookie consent, what Baker records, whether search engines show the site, the home page, the time zone, the landing download format, and custom domains. Every change stages on the chat and applies when the chat is published; discarding the chat drops it.
1673
+
1674
+ ```bash
1675
+ baker site show # settings after this chat publishes, and what it staged (before/after)
1676
+ baker site set --consent recovery # bypass | recovery | strict | external
1677
+ baker site set --collection aggregate # full | aggregate | off (+ --payloads on|off with full)
1678
+ baker site set --indexing off # on | off
1679
+ baker site set --home oferta --home-mode redirect # a landing slug (rewrite by default), or --home none
1680
+ baker site set --timezone Europe/Madrid # IANA name
1681
+ baker site set --export-profile @profile.json # landing download format, JSON or @file
1682
+ baker site domain add shop.acme.com # staged; the client adds CNAME shop.acme.com → customers.withbaker.com
1683
+ baker site domain list # status of every domain: staged, pending_verification, active, error
1684
+ baker site domain verify shop.acme.com # re-check a pending domain now
1685
+ baker site unstage consent # take back one staged setting (or --all)
1686
+ ```
1687
+
1688
+ `set` takes several flags in one call and stages all of them or none. A staged setting that cannot apply at publish — a home page whose landing is not published yet, a domain another company registered in the meantime — is skipped with its reason on the chat's row and in `baker chats view` → `effects.siteSettings`; everything else in the chat still publishes.
1689
+
1493
1690
  ### Google Analytics 4 (`baker ga4`)
1494
1691
 
1495
1692
  GA4 commands for multi-channel audits **and** for configuring what the property measures. Playbook-aligned report presets, property health audits, free-form Data API queries, and staged Admin API writes.
@@ -1733,6 +1930,25 @@ baker gsc sitemaps --site-url "https://example.com/"
1733
1930
 
1734
1931
  ---
1735
1932
 
1933
+ ### Making ads (`baker creatives brief` / `ideas` / `make`)
1934
+
1935
+ Ads start from what customers said. Three commands carry the process; the agent writes the ideas and the copy.
1936
+
1937
+ ```bash
1938
+ baker creatives brief [--adset <id>] [--account-id act_<id>] [--no-research] # one read: viewer, account bottleneck, what works, taste, customer quotes, ideas on file
1939
+ baker creatives ideas [--full] # check and rank src/creatives/_ideas/*.md; build_next
1940
+ baker creatives make <idea> [--slug <s>] [--adset <id>] [--reference <adId> [--platform meta|linkedin|tiktok|google]] [--spec <beats.json>] [--from-idea] [--series <lead>]
1941
+ baker creatives translate --slug <ad> --to es-MX,es-CO,pt-BR [--copy-file <json>] [--note "<brief>"] [--again] # the same ad in other languages, one design
1942
+ baker creatives learn [--days <n>] [--dry-run] # what each idea's ads did, written back onto the idea
1943
+ ```
1944
+
1945
+ - `brief` returns a verdict, not raw numbers: `account.bottleneck` is `hook`, `after_click`, `none` or `too_little_data`. A part that cannot be read says `unavailable`; the market searches (the followed brands, and the library for what COMPANY.md's first line says the company sells, in a few words) run at once and get 12 seconds, so a slow ad library leaves them out rather than holding the brief. With few customer quotes on file it runs `research customers` itself alongside the reads (about 30 s) and returns what it wrote; `--no-research` skips that. With no ideas on file, its hint lists the values `awareness`, `angle` and `format` take. `market` says what competitors' ads already say (the followed brands, else the library), how sophisticated the market is, and which awareness stages nobody speaks to.
1946
+ - `ideas` is local and nearly free: two readings of the words go to the decision model (a fraction of a cent, billed with the workspace's next run, answers kept in `canvas/.cache/`). An insight filed as `observed` reads as `interpreted` when its source cannot be opened or its quote is a statistic about people rather than someone's words ("39% de los propietarios…"), and a new idea is warned when its message says what a failed idea said, however reworded or translated. Without an answer, an insight keeps its status and only the same words count as the same message. An idea's `evidence` resolves to `src/content/insights/` entries, or it is an `explore` bet. Numbers must be in the Knowledge or our own customers' quotes (a Reddit, forum or web quote never sources a claim), no two ideas may share an angle and format, and a review or testimonial format needs a quote from our own customer.
1947
+ - `learn` pools each built idea's creatives, judges them against the rest of the account's cost per result and writes `status` (`winner`, `failed`, `inconclusive`, `testing`) and a frozen `result` onto the idea. `ideas` then warns when a new idea repeats a failed one.
1948
+ - `make` builds one idea, on a proven winning ad whenever one fits. A static searches the idea's container on the idea's message for our viewer, then for the container's strongest winners from any category (no query, by winner score), pools the candidates (each ad once, the better search first), checks up to six with `winning-ads fit`, three at a time (the next only once one does not fit), and keeps the best-ranked one that fits as soon as it and every better one have answered. The fit says what made that ad a winner, so what the creative carries of it (`reference.carry`: `whole_ad`, `layout`, `hook` or `angle`); make seeds it into `prompt.json` as `concept`, the canvas keeps the ad as its `original` node, and the renders see it for its structure only at `whole_ad` or `layout`. The first hint names the ad and what it carries (for a `--reference` chosen by hand, only the ad: nothing checked what made it win); if the concept could not be written into `prompt.json`, a hint says so and how to write it from the fit already made. For a new creative it starts scaffolding the best-ranked one while the fits are asked (kept when it fits, stopped and removed whole when another or none is kept). Finding, checking and describing a reference gets 150 seconds; past that the build is stopped and the static starts from the idea (`REFERENCE_SLOW`). A creative whose ad is already written is never built over (`ALREADY_MADE`): edit its `prompt.json`, or pass `--slug` for another ad of the same idea. A video idea takes its beats spec. Either way it records the idea on `_definition.md`, sets `prompt.json`'s `message` and syncs the definition. Drafted copy lines claiming a figure the company never wrote come back first in `hints`. Only when no proven ad fits (or with `--from-idea`) does a static start from the idea itself, and the first hint says why (none in the container, what the ones checked won on that the company lacks, the time ran out, or the proven ad it picked could not be read; a `--reference` chosen by hand that cannot be read fails instead): `prompt.json` holds the message and placeholders, and `canvas run` refuses to render until the scene and copy are written.
1949
+ - `make --series <lead>` makes a static idea as another ad of a series: several messages in one style. Nothing is searched or rendered; the ad's `_definition.md` names its lead (`series: <lead>`), its `prompt.json` starts as the lead's ready version with the idea's message, and only its `copy` is written. Its canvas is written from the lead's on every run: the lead's finished picture with this ad's words, every other size the lead's own size with the same words. `baker canvas run --series <lead>` renders every ad of the series at once, one reviewed and corrected run each, skipping an ad already ready from the lead's current version.
1950
+ - `translate --slug <ad> --to <langs>` makes the ad in other languages as ads of the series it leads: one creative per language or country variant, `<ad>-<lang>` (the BCP-47 tag in lowercase: `-es-mx`), whose `_definition.md` says `series: <ad>` and `language: <lang>` (canonical case, `es-MX`, as the spec's market and the dashboard store it; tags are compared without case) and whose `prompt.json` is the ad's as last drawn with its words translated. Every language is translated in one text call (transcreated, each regional tag in that country's own variety and form of address, `es-MX` and `es-CO` two ads, each line keeping its role and about its length, brand names, prices and numbers kept, a customer's quote and the name on it never translated, and the ad's `message` translated with the lines; the spend is billed with the workspace's next run), or read from `--copy-file`: `{"<lang>": ["line 1", …]}` is every line with no model call, `{"<lang>": {"headline": "…", "3": "…"}}` only the lines given (by role or number), the model writing the rest. `--note` is the agent's brief for the translator (tone, form of address, terms to keep). Each made language lists the lines it `kept` as a customer said them, and the answer warns when the ad's `prompt.json` changed after its last render, since translations are made from the version rendered. The ad may itself be an ad of a series (an angle ad), so angles × languages stay one design. A language already made is left as it is, so adding one later re-renders nothing; `--again` translates it again. Then `baker canvas run --series <ad>` renders them; a new version of the ad reaches every language on the next `--series` run.
1951
+
1736
1952
  ### Competitive Intelligence (`baker research`)
1737
1953
 
1738
1954
  Market and competitor research powered by DataForSEO and AI-powered Google Search grounding. Shows who's competing for keywords, search intent classification, competitor keyword strategies, keyword gaps, landing page performance, and open-ended AI research.
@@ -1831,11 +2047,52 @@ baker research fetch "https://competitor.com" --format html --whole-page
1831
2047
  - Each URL is cached server-side for 24 hours, so a follow-up `--full` call costs no extra credit.
1832
2048
  - Failures carry `error.fix` with `action: "continue_without"` for pages that will not open — finish the rest of the job and report the gap rather than retrying.
1833
2049
 
2050
+
2051
+ A Reddit thread link is not sent to Firecrawl (which refuses Reddit): it is read by the Reddit reader below and returned as the thread written out as a page, with `"provider": "reddit"`. Any other Reddit page is refused with a pointer to `baker research reddit`.
2052
+
2053
+ ---
2054
+
2055
+ ### `baker research reddit subreddits` / `search` / `thread`
2056
+
2057
+ Read Reddit verbatim. Reddit refuses ordinary web reads, Firecrawl, and datacenter addresses, so these commands read it through a ladder of sources — Reddit's own API (when the backend holds `REDDIT_CLIENT_ID` / `REDDIT_CLIENT_SECRET`), the Arctic Shift public archive, then Reddit's public JSON through the proxy tiers — and report which one answered.
2058
+
2059
+ ```bash
2060
+ baker research reddit subreddits "amazon flex uk"
2061
+ baker research reddit search "agency never paid" --subreddit couriersofreddit,AmazonFlexUK --kind comments
2062
+ baker research reddit search "visa" --subreddit UberEATS --kind comments --since 2025-01-01 --limit 10
2063
+ baker research reddit thread "https://www.reddit.com/r/AmazonFlexUK/comments/1abc2de/agency_still_hasnt_paid_me/"
2064
+ baker research reddit thread "https://www.reddit.com/r/running/s/3TzXiyxaMD" --comments 100
2065
+ ```
2066
+
2067
+ | Flag | Description |
2068
+ |------|-------------|
2069
+ | `--subreddit` | Subreddits (up to 10), comma-separated or the flag repeated. Required for `--kind comments`, and for any search when the Reddit API is not configured |
2070
+ | `--kind` | `posts` (default) or `comments` |
2071
+ | `--since` | Only things written on or after `YYYY-MM-DD`; results then come newest first instead of best match first |
2072
+ | `--limit` | Search results, 1-50 (default 25) |
2073
+ | `--limit` (subreddits) | Subreddits to return, 1-25 (default 10) |
2074
+ | `--comments` | Thread comments, 0-200 (default 40), best conversations first |
2075
+ | `--full` | Whole texts instead of 1,500-character previews |
2076
+
2077
+ `subreddits` returns `name`, `subscribers` (size, not activity), `access` (`public` / `restricted` / `private`) and `description` — use it instead of guessing subreddit names. Cached 24h.
2078
+
2079
+ Every search or thread item carries verbatim `text`, `author`, `score`, `created` and a `permalink` built from the ids the source returned. `thread` refuses a link whose post lives in a different subreddit than the link names (`NOT_FOUND`, `fix.action: "reject"`) — the shape of a link invented by a summary. `source` is `reddit_api`, `reddit_web` or `archive`; archive points on posts under ~36h old are as first captured. A link to one comment opens the thread at that comment; a post on a user's profile (`/user/<name>/comments/<id>`) reads like any other thread. Cached 6h; an empty search is not cached.
2080
+
1834
2081
  ---
1835
2082
 
2083
+ ### `baker research customers`
2084
+
2085
+ Finds what real customers say and writes each quote as an insight in `src/content/insights/` (type, quote, source, date, `status: observed`). Sources: the company's reviews library, Reddit, and with `--web` pages from web research. A quote is kept only when it appears word for word in the text that was fetched; the model classifies quotes and never writes them.
2086
+
2087
+ ```bash
2088
+ baker research customers --focus "solar panels" # reviews + Reddit
2089
+ baker research customers --query "payroll mistakes" --web --limit 15
2090
+ baker research customers --dry-run # print, write nothing
2091
+ ```
2092
+
1836
2093
  ### `baker research advertisers "keyword"`
1837
2094
 
1838
- Returns a ranked list of **domains** competing for one keyword in Google, with average SERP position, relevance rating, estimated traffic value and visibility — domain economics, **NOT ad copy**. Despite the name it returns no headlines, descriptions or creative of any kind; for a competitor's actual ad copy use `baker winning-ads content <adId>` (Meta/LinkedIn only — Google SERP ad copy is not available anywhere in Baker).
2095
+ Returns a ranked list of **domains** competing for one keyword in Google, with average SERP position, relevance rating, estimated traffic value and visibility — domain economics, **NOT ad copy**. Despite the name it returns no headlines, descriptions or creative of any kind; for a competitor's actual ad copy use `baker winning-ads content <adId>` (Meta, LinkedIn, TikTok and Google, for brands the library tracks) or, for a one-off Google read, `baker research google-ads <domain> --copy`.
1839
2096
 
1840
2097
  Defaults to `--location us` and `--language en` when omitted; the response carries a `query_context` showing which pair was used, so always set both for non-US or non-English markets.
1841
2098
 
@@ -2265,15 +2522,17 @@ Each external source is its own subcommand. Pick the verb that matches the sourc
2265
2522
  | `baker images get <id>` | Single record | n/a |
2266
2523
  | `baker images upload <file\|url>` | Upload a local file OR remote URL to the library | always on |
2267
2524
  | `baker images delete <id>` | Delete a record | n/a |
2525
+ | `baker images retry <id>` | Process a failed image (status `error`) again | n/a |
2268
2526
  | `baker images describe <id>` | Correct a record's stored name / description / tags | n/a |
2269
2527
  | `baker images normalize <files>` | Local recolor + bg removal + trim + resize | n/a (operates on disk, no upload) |
2270
- | `baker images upscale <imageId>` | Real-ESRGAN super-resolution via backend ($0.05/image, cost-tracked) | n/a (operates on library image) |
2528
+ | `baker images upscale <imageId>` | Real-ESRGAN super-resolution via backend ($0.05/image, cost-tracked). `--remove` drops the upscaled copy and goes back to the original (free) | n/a (operates on library image) |
2271
2529
  | `baker images layerize <imageId>` | Split into editable layers — transparent cutouts per element plus baked-in copy recovered as editable text (costs credits) | n/a (operates on library image) |
2272
2530
  | `baker images layers <imageId>` | Read the layers of an already-split image (free) | n/a (operates on library image) |
2273
2531
  | `baker images crop <file>` | Coordinate-based rectangular extract — local file or URL | n/a |
2274
2532
  | `baker images dimensions <file\|url>` | Read width / height / aspect / format without decoding | n/a |
2275
2533
  | `baker images download <targets>` | Remote URLs and/or library ids → local files, so the local transforms can read them | n/a (writes to disk, no upload) |
2276
2534
  | `baker images tags` | List available image tag names (defaults + company custom tags) | n/a |
2535
+ | `baker images tags create\|update\|delete <name>` | Manage the company's custom tags — one pool shared by images, videos and testimonials (also under `baker videos tags` / `baker testimonials tags`) | n/a |
2277
2536
 
2278
2537
  **Every image record carries `width` and `height`, without `--full`.** They are there to be passed straight to the component that places the image: a remote image rendered with no declared size makes the site build download the file purely to measure it, which is the only reason building a site needs the network at all — and a dropped connection there fails the entire quality-check run for a reason nothing on the page caused.
2279
2538
 
@@ -2628,7 +2887,7 @@ baker images ingest https://images.pexels.com/photos/13219418/pexels-photo-13219
2628
2887
  baker images ingest https://acme.com/hero.png --source firecrawl --external-url https://acme.com/pricing --context "competitor pricing hero"
2629
2888
  ```
2630
2889
 
2631
- `--source` is a closed set, validated before the request so a wrong value costs nothing: `uploaded`, `website`, `google_testimonial`, `trustpilot_testimonial`, `instagram`, `pexels`, `pixabay`, `magnific` (retired provider, kept for existing rows), `brandfetch`, `google_images`, `firecrawl`, `screenshotone`, `iconify`, `giphy`, `pinterest`, `ai_generated`, `layer_edit`. Use `website` for an asset pulled off a company's own site.
2890
+ `--source` is a closed set, validated before the request so a wrong value costs nothing: `uploaded`, `website`, `google_testimonial`, `trustpilot_testimonial`, `instagram`, `tiktok`, `linkedin`, `facebook`, `pexels`, `pixabay`, `magnific` (retired provider, kept for existing rows), `brandfetch`, `google_images`, `firecrawl`, `screenshotone`, `iconify`, `giphy`, `pinterest`, `ai_generated`, `layer_edit`. Use `website` for an asset pulled off a company's own site.
2632
2891
 
2633
2892
  Returns `{ imageId, deduped, contentHash }`. When `deduped: true`, an existing library row is returned — no new bytes are stored. Max ingest size 25MB.
2634
2893
 
@@ -2906,6 +3165,8 @@ baker videos search "product demo"
2906
3165
  baker videos search "tutorial" --tags explainer,ugc --limit 5
2907
3166
  ```
2908
3167
 
3168
+ Each hit carries `format` (e.g. `testimonial`, `trend-skit`), `tone` (`sincere` … `parody`) and `productClaims`: what the clip says about the offer. Hints name any parody result, and any clip catalogued before these fields existed.
3169
+
2909
3170
  ### `baker videos get <id>`
2910
3171
 
2911
3172
  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.
@@ -2962,7 +3223,7 @@ Re-ingesting the same link returns the existing row with `deduped: true` instead
2962
3223
 
2963
3224
  | Flag | Purpose |
2964
3225
  |---|---|
2965
- | `--source` | Provenance. Inferred from the link's host when omitted, and that inference covers 20 video platforms — `youtube`, `instagram`, `tiktok`, `twitter`, `facebook`, `reddit`, `vimeo`, `dailymotion`, `twitch`, `bluesky`, `bilibili`, `vk`, `ok`, `pinterest`, `tumblr`, `snapchat`, `loom`, `streamable`, `newgrounds`, `rutube` — plus `uploaded` and `url`. A link from anywhere else still ingests, recorded as `url`. |
3226
+ | `--source` | Provenance. Inferred from the link's host when omitted, and that inference covers 21 video platforms — `youtube`, `instagram`, `tiktok`, `twitter`, `facebook`, `linkedin`, `reddit`, `vimeo`, `dailymotion`, `twitch`, `bluesky`, `bilibili`, `vk`, `ok`, `pinterest`, `tumblr`, `snapchat`, `loom`, `streamable`, `newgrounds`, `rutube` — plus `uploaded` and `url`. A link from anywhere else still ingests, recorded as `url`. |
2966
3227
  | `--external-id` | Dedupe key — re-running the same link returns the existing video. Derived from the link automatically on the download route. |
2967
3228
  | `--external-url` | Canonical page URL. Defaults to the input for a page URL. |
2968
3229
  | `--download` | Skip the direct attempt and always download. |
@@ -2994,10 +3255,24 @@ Requires `yt-dlp` on `PATH` (present in the Baker Runtime). Private, age-restric
2994
3255
 
2995
3256
  **yt-dlp goes stale, and that looks like a broken video.** Large platforms change their players every few weeks and an out-of-date `yt-dlp` then resolves *no* formats at all, failing with "Requested format is not available" or "Only images are available" rather than anything that names the real cause. The error classifies this case separately and says the link is probably fine. The Runtime image installs `yt-dlp` unpinned at build time (`packages/e2b-template/src/template.ts`), so it ages with the image — if platform ingests start failing across the board, rebuild the template before investigating anything else. Direct file URLs don't depend on `yt-dlp` at all and keep working regardless.
2996
3257
 
3258
+ ### `baker videos fetch <url> --out <dir>`
3259
+
3260
+ Download a video page (YouTube, TikTok, Vimeo, …) to a local `.mp4` without adding it to the library — the download half of `ingest`, with the same proxy fallback and duration check. `--max-seconds` lowers the length limit (default 3 hours); `--timeout-ms` bounds the download. By default it keeps the best quality the site offers; `--max-height 1080` keeps a version whose shorter side is at most that when there is one (a vertical Short keeps its 1080×1920) (a 4K master can be five times the bytes). Returns `{ file, durationSeconds, egress }`. The file is named after the video (e.g. `youtube-abc123.mp4`), so several fetches can share one folder; an existing file is never overwritten — a second copy gets a `-2` suffix. Nothing else is left in the folder.
3261
+
3262
+ ```bash
3263
+ baker videos fetch https://www.youtube.com/watch?v=abc123 --out ./clips
3264
+ ```
3265
+
3266
+ Baker's YouTube source sync runs this same command on a disposable machine, with `--max-height 1080`, because a YouTube file URL only works from the IP that requested it.
3267
+
2997
3268
  ### `baker videos delete <id>`
2998
3269
 
2999
3270
  Delete a video by ID.
3000
3271
 
3272
+ ### `baker videos retry <id>`
3273
+
3274
+ Process a video whose status is `error` again. When the upload itself never finished it answers `status: "error"` with a `message` saying the file has to be uploaded again.
3275
+
3001
3276
  ### `baker videos describe <id>`
3002
3277
 
3003
3278
  The same correction for a clip: fix the stored `name`, `description` or `tags` when the analysis read the video wrong. Only the flags you pass change, `--tags` replaces the whole set, and the search index rebuilds in the background.
@@ -3013,8 +3288,13 @@ List the available video tag names — built-in defaults plus the company's cust
3013
3288
 
3014
3289
  ```bash
3015
3290
  baker videos tags
3291
+ baker videos tags create "summer sale" --description "Assets for the summer sale campaign"
3292
+ baker videos tags update summer-sale --description "Assets for the 2026 summer sale"
3293
+ baker videos tags delete summer-sale
3016
3294
  ```
3017
3295
 
3296
+ `tags create|update|delete` manage the company's custom tags, one pool shared by images, videos and testimonials. Built-in tags cannot be changed. Like every library change from a chat, they apply immediately and are listed on the chat as already applied.
3297
+
3018
3298
  ### `baker testimonials search <query>`
3019
3299
 
3020
3300
  Semantic search testimonials. For landing pages, query the product/offer and USP before filtering by rating so selected quotes match what the landing sells.
@@ -3028,6 +3308,26 @@ baker testimonials search "competitor support" --source trustpilot --sentiment n
3028
3308
  baker testimonials search "[product/offer] for [persona] that delivers [USP/outcome]" --rating-min 4 --language en --output md --full --limit 12
3029
3309
  ```
3030
3310
 
3311
+ ### `baker testimonials sources`
3312
+
3313
+ The review pages testimonials are imported from — Google Maps, Trustpilot, Yelp, Tripadvisor, G2, Capterra, Amazon, App Store, Google Play. Adding or removing one applies immediately and shows on the chat as an already-applied change.
3314
+
3315
+ | Command | What it does |
3316
+ | --- | --- |
3317
+ | `baker testimonials sources add <link \| business name> [--platform <site>] [--every never\|daily\|weekly\|monthly]` | Recognise the page from its link (a business name finds a Google Maps listing) and import its reviews in the background. Idempotent. |
3318
+ | `baker testimonials sources list` | Every source: id, site, status, reviews imported, auto-sync. |
3319
+ | `baker testimonials sources sync <id>` | Import new reviews now. |
3320
+ | `baker testimonials sources schedule <id> --every <interval>` | Set the auto-sync cadence. |
3321
+ | `baker testimonials sources reset <id>` | Unstick a source whose import never finished, so it can import again. Keeps its reviews. |
3322
+ | `baker testimonials sources remove <id>` | Remove the source and every testimonial it imported. |
3323
+
3324
+ `baker testimonials delete <id> [--dry-run]` removes one testimonial (and the images it brought in) from the library. It applies immediately.
3325
+
3326
+ ```bash
3327
+ baker testimonials sources add https://www.trustpilot.com/review/acme.com
3328
+ baker testimonials sources add "Acme Bakery Madrid"
3329
+ ```
3330
+
3031
3331
  ### `baker testimonials get <id>`
3032
3332
 
3033
3333
  Get a single testimonial by ID.
@@ -3071,7 +3371,7 @@ baker testimonials tags
3071
3371
 
3072
3372
  ### Avatars (`baker avatars`)
3073
3373
 
3074
- Reusable AI presenters a company casts into images and video. An avatar holds one settled identity: an **identity sheet** (`sheetUrl`, a generated multi-view turnaround) plus the **subject description** (`subjectDescription`) that person is described with. Every render grounds on the sheet via `--reference` and reuses the subject description verbatim — that pairing is what keeps the same face across a whole campaign.
3374
+ Reusable AI presenters a company casts into images and video — for an actor who has to come back across ads; a person in a single ad needs no avatar. An avatar holds one settled identity: an **identity sheet** (`sheetUrl`, four candid phone photos of the same person in their own setting) plus the **subject description** (`subjectDescription`) that person is described with. Every render grounds on the sheet via `--reference` and reuses the subject description verbatim — that pairing is what keeps the same face across a whole campaign.
3075
3375
 
3076
3376
  `status` is `generating | ready | error | archived`; only `ready` can be cast. `create` returns immediately with `generating` because the identity sheet renders in the background (~30s). `likeness` is `synthetic` (invented from a description) or `licensed` (built from photographs of a real person, which requires `--likeness-confirmed`).
3077
3377
 
@@ -3123,6 +3423,20 @@ next one that shares her name. A `--handle` you pass yourself still conflicts: t
3123
3423
  decision, and moving it quietly would point `@marta` at somebody else. The returned `handle`
3124
3424
  is the one the avatar actually got.
3125
3425
 
3426
+ #### `baker avatars research "<who>"`
3427
+
3428
+ Find a real, named person — a testimonial author, the company's founder, a team member — from a description, and gather their public photos into the image library. `--create` builds their avatar from those photos right away (likeness `licensed`, no face picker), or returns the avatar they already have.
3429
+
3430
+ ```bash
3431
+ baker avatars research "Lautaro Suarez, CTO at Vibiz"
3432
+ baker avatars research "the CEO of Vibiz" --create
3433
+ baker avatars research "Lautaro Suarez" --company Vibiz --role CTO --create --handle lautaro
3434
+ baker avatars research "Ana Botín, presidenta del Banco Santander" --site santander.com
3435
+ baker avatars research "Pau Gasol" --role "basketball player" --context "Spanish"
3436
+ ```
3437
+
3438
+ The answer's `status` is `found` (their `person` record, `photos`, a `subjectDescription` and, with `--create`, the `avatar`), `possible` (`candidates` that matched only in part, each with its `profile` link — pick one with `--profile <link>`) or `not_found` (the `searched` queries, and `possibleSites`: sites whose domain spells part of the company's name; nothing was taken). It reads LinkedIn, the company's own site and, for well-known people, Wikidata. A profile counts as the person only when both the name and the company match it, or, with no company, the name, the role and `--context` (where they are from or based, their field). `--site <domain>` is the company's own website when its domain is not its name spelled out; the client's own is used for its own people. It checks the company's avatars first: a person who already has one (the same full name, and the same company when both name one; for a role alone, the researched avatar holding it) comes back `found` with that `avatar` (`reused: true`) — nothing searched, billed or copied again; an invented face (likeness `synthetic`) or one whose build failed never counts as them. `--profile` (or `--linkedin`) skips that check. When a lookup service does not answer (a timeout, a rate limit), the command fails with `UNAVAILABLE` rather than answering `not_found`, and nothing of that run is kept, so the next try searches again. `--name`, `--role` and `--company` override the description; `--full` adds their about text, location and every search run.
3439
+
3126
3440
  #### `baker avatars update <handle>`
3127
3441
 
3128
3442
  Edit an avatar. Profile flags are merged over the stored profile, so `--persona` alone keeps the subject description intact; an empty value clears the field it names (except `--subject`, which can only be replaced). Changing `--subject` requires `--regenerate-sheet` in the same call — otherwise the sheet still shows the person described before.
@@ -3147,67 +3461,124 @@ baker avatars delete marta --archive
3147
3461
 
3148
3462
  ---
3149
3463
 
3464
+ ### Media Sources (`baker media-sources`)
3465
+
3466
+ Connect a public social account so its posts fill the image and video libraries: **Instagram**, **TikTok**, **YouTube** (videos and Shorts only), **LinkedIn** company pages and **Facebook** pages — or a **Google Drive folder** the user picks from the company's Google Drive connection. Each social sync reaches back up to a year, stops paging at the first page it already has, imports only what the library doesn't have yet, keeps carousels/albums/documents together as a set (`images group` / `videos group`), and passes the post caption to the analysis. A source is named by its key, `<platform>:<handle>`.
3467
+
3468
+ Connecting and removing apply immediately and show on the chat as an already-applied change.
3469
+
3470
+ | Command | What it does |
3471
+ |---|---|
3472
+ | `baker media-sources add <profile URL \| @handle> [--platform <p>] [--every never\|daily\|weekly\|monthly]` | Look the account up (must exist and be public) and connect it; the import starts in the background. `--platform` only for a bare handle. Idempotent. |
3473
+ | `baker media-sources list` | Every source: key, platform, label, auto-sync, and per library `{ status, stored, pending?, failed?, error? }`. |
3474
+ | `baker media-sources sync <key>` | Import new posts now. |
3475
+ | `baker media-sources schedule <key> --every <interval>` | Set the auto-sync cadence. |
3476
+ | `baker media-sources reset <key>` | Unstick a source whose import never finished, so it can sync again. Keeps what it imported. |
3477
+ | `baker media-sources remove <key>` | Disconnect; imported media stays in the library. |
3478
+
3479
+ ```bash
3480
+ baker media-sources add https://www.tiktok.com/@acme
3481
+ baker media-sources add @acme --platform instagram --every daily
3482
+ baker media-sources list
3483
+ ```
3484
+
3485
+ YouTube downloads are the expensive import (a disposable machine per batch), so a YouTube sync takes the newest 25 videos it hasn't stored, skips videos over 20 minutes, and continues with older ones on the next sync.
3486
+
3487
+ A Google Drive folder is never added with `add`: the agent calls `request_connection` with `platform: "google-drive"` and `choose: "media-folder"`, and the user picks the folder in the chat card (or in Baker, Add source → Google Drive) — only the Baker team can for now. `add <drive link>` is refused with that `error.fix`; the card's result carries the new source's `mediaSourceKey`. Once connected it is managed by key like any source. Everything in it and its subfolders is imported in batches of 150 images and 20 videos (60 minutes / 5 GB at most each), new files arrive with each scheduled sync (weekly unless changed), and images are stored resized to 2560 px. Imported images from every source are resized the same way.
3488
+
3489
+ ---
3490
+
3150
3491
  ### Winning Ads (`baker winning-ads`)
3151
3492
 
3152
3493
  Search the **ad-dna** corpus of scored "winning" competitor ads for reference creatives to reproduce (e.g. with `baker canvas`), and manage the brands your library tracks (`follow` / `following` / `feed` / `unfollow`). Each result carries a presigned media URL (~1h TTL), the ad's DNA summary, and scores. The CLI authenticates with the normal `BAKER_API_KEY`; the Baker backend proxies the request to the ad-dna service with a server-held token — no extra credential in the sandbox.
3153
3494
 
3154
- > The corpus has **Meta + LinkedIn** connectors, so `--platform` inputs are limited to `meta,linkedin`. (Older result rows may still carry a legacy platform string.)
3495
+ > The corpus reads **Meta, LinkedIn, TikTok and Google Ads** (`--platform meta,linkedin,tiktok,google`). Google search text ads are `--format text`; only Google display and YouTube ads can be a design reference.
3496
+
3497
+ > Rows are compact by default. `--fields a,b` keeps only those columns in every `--output` on `search`, `feed`, `winners`, `ads`, `advertisers`, `following` and `content`; on ad rows a name only `--full` carries (`angle`, `hook_archetype`, …) comes back on its own, and a name no row carries is refused with `error.fix.validValues`.
3155
3498
 
3156
3499
  > Backend env: the Convex deployment must have `AD_DNA_API_TOKEN` set (`npx convex env set AD_DNA_API_TOKEN …`). `AD_DNA_API_URL` is optional and defaults to `https://ads.withbaker.com`.
3157
3500
 
3158
3501
  > Replaces the old `baker ads google library` tree, which has been removed. Competitor-by-keyword discovery still lives at `baker research advertisers`.
3159
3502
 
3503
+ `baker winning-ads profile <advertiserId>` returns what one brand is doing: an estimated spend in USD (a range over every ad since `since`, the last 30 days, and a minimum, split by channel in `by_platform` — a channel that publishes no delivery is marked `unpriced`; `ads_counted` says how many ads were measured, estimated from similar ads, or not counted for lack of reach), what it launched lately and which of those still run, the concepts it tested and the version it kept, its format and platform mix, top landing pages, audiences and similar brands. `--platform` narrows it, `--full` returns everything. `baker winning-ads launches [--since 7] [--advertiser a,b]` lists what the followed brands (or the named ones) launched in the last N days and which still run. Both say, for every launch, whether it was a `new` idea, another version of one still in `testing`, or a version built on a `winner` the brand had already kept; the profile also counts new ideas per week and `unique_ideas` overall.
3504
+
3505
+ `baker winning-ads landings <advertiserId>` lists every landing page the brand's ads point to, most ads first, with how many ads point there, how many still run, the platforms and when each was last used. `address_only` marks an address only Google search ads print: Google publishes no search ad's destination, so it may not be a real page and is never analysed. `--analyse` studies the top 5 pages not analysed yet (`--pages a,b` to name them): each page's headline, value propositions, offer, pricing, calls to action, proof, audience and sections in order. Analysis takes a few minutes per page and is shared by every company, so a page analysed once is ready for all. `--platform` narrows it, `--full` returns every field.
3506
+
3507
+ `baker winning-ads search-copy <advertiserId>` reads what a brand says on Google Search from its search ads: every headline with its hook (category, brand, free, price_discount, outcome, proof, new_timely, comparison, audience, location, feature, call_to_action, question), its descriptions, sitelinks and callouts. Each is `running` or `dropped` (every ad that carried it has stopped), with how many ads carried it and the longest any ran. `hooks` compares the running headlines' hooks with the dropped ones; `rival_headlines` lists headlines naming a competitor. `part_totals` counts every part of each kind; `--part <headlines|descriptions|sitelinks|callouts> --offset <n>` reads one list a page at a time (`next_offset` until null). `--running` keeps what still runs, `--limit` sets rows per list or page (default 15), `--full` adds dates and every address the ads show (Google publishes no search ad's destination, only that address).
3508
+
3509
+ `baker winning-ads refresh <advertiserId>` re-scrapes a followed brand so its newest ads land in the library. It applies immediately and runs at most once every 6 hours per brand; inside that window it answers `RATE_LIMITED` with an `error.fix` naming when the brand can be refreshed again. While a follow or the last refresh is still collecting the brand's ads, it starts nothing and says so.
3510
+
3160
3511
  ### `baker winning-ads search <query>`
3161
3512
 
3162
- Semantic search (dense recall + BM25 + rerank) → `POST /api/ad-library/winners/search`. The CLI projects each result to a **lean, decision-focused** shape so the agent's context stays small — default fields: `advertiser`, `advertiser_id`, `platform`, `format`, `relevance`, `winner_score`, `summary` (what the ad is about), `media_url`; plus top-level `pool_size`, `match_confidence` (`high|medium|low`), and `below_floor_count` (matches dropped under the relevance floor). `--full` adds DNA detail (`angle`, `target_persona`, `hook_archetype`, `awareness_stage`, `industry`) + longevity (`days_active`, `reach`, `active`, `winner_category`, `media_kind`). `--output json` (default) returns the lean objects; `--output md` prints a table.
3513
+ Semantic search (dense recall + BM25 + rerank) → `POST /api/ad-library/winners/search`. The CLI projects each result to a **lean, decision-focused** shape so the agent's context stays small — default fields: `advertiser`, `advertiser_id`, `platform`, `format`, `relevance`, `winner_score`, `summary` (what the ad is about), `media_url`, and `visual` (what the viewer sees) + `container` (a static's layout pattern) + `mechanism` (why it works) when known; plus top-level `pool_size`, `match_confidence` (`high|medium|low`), and `below_floor_count` (matches dropped under the relevance floor). The proof signals ride along (`winner_category`, `days_active`, `active`), and so does who the ad was made for when the library knows it: `audience_temperature` (`0_cold|1_warm_engaged|2_hot_visited|3_customer`), `funnel_stage`, `advertiser_size`, and `presumes_brand_knowledge` (whether the copy only works on viewers who already know the brand, with `brand_knowledge_reason` when `true`; absent while the ad is not judged yet); `--full` adds DNA detail (`angle`, `target_persona`, `hook_archetype`, `awareness_stage`, `industry`) + `reach` and `media_kind`. By default the search draws from the brands the company follows first (`--scope followed-first`) and tops up from the shared library; every result says `source: followed|library`. `--output json` (default) returns the lean objects; `--output md` prints a table.
3163
3514
 
3164
3515
  > `media_url` is the creative itself: for `static` it's the image, for `video` it's the video file. ad-dna stores **no separate poster** for videos, so a video result has only the video URL.
3165
3516
 
3166
- > Results are already de-junked and winner-first: the corpus auto-drops measured failures (duds, thrash) and ranks `final = relevance^0.25 × winner_score^0.75`. So `--winner-category winner` is usually unnecessary — it's a stricter filter that also removes `untested` (unproven-but-maybe-fresh) ads. Rely on the default + `--min-relevance`; only add `--winner-category` to force-exclude unproven creatives. (Restricting to a single `--advertiser-id` turns the failure-exclusion off — you then see that brand's duds too.)
3517
+ > Results are already de-junked and winner-first: the corpus auto-drops measured failures (duds, thrash) and ranks `final = relevance^0.5 × winner_score^0.5`, so a direct match beats a stronger-but-tangential ad. So `--winner-category winner` is usually unnecessary — it's a stricter filter that also removes `untested` (unproven-but-maybe-fresh) ads. Rely on the default + `--min-relevance`; only add `--winner-category` to force-exclude unproven creatives. (With a query, restricting to a single `--advertiser-id` turns the failure-exclusion and the per-advertiser cap off: you then see that brand's duds too. A search with no query is a browse, which always drops failures and `untested` ads. A company that follows one brand keeps both with `--scope followed`/`followed-first`.)
3167
3518
 
3168
3519
  ```bash
3169
3520
  # Only LinkedIn video ads:
3170
3521
  baker winning-ads search "B2B lead gen demo" --platform linkedin --format video --output md
3171
- # Cross-platform, exclude our own brand + an advertiser we already mined:
3172
- baker winning-ads search "fintech onboarding" --exclude-advertiser adv_ourbrand,adv_usedbefore --output md
3522
+ # Cross-platform, excluding our own brand (an ad we already adapted comes back last, marked already_adapted):
3523
+ baker winning-ads search "fintech onboarding" --exclude-advertiser adv_ourbrand --output md
3173
3524
  # Similar-to a reference ad, recent only:
3174
3525
  baker winning-ads search --ref-ad-id a_12345 --first-seen-after 2026-01-01T00:00:00Z --limit 5 --output md
3526
+ # One varied reference per concept (different brands, containers, hooks) in one call:
3527
+ baker winning-ads search "meal kit for busy parents" --scope followed-first --diverse --limit 5 --output md
3528
+ # The best proven ads in one container, from any category (no query needed):
3529
+ baker winning-ads search --format-pattern comparison_table --platform meta --output md
3530
+ # References made for strangers that don't lean on their brand being known:
3531
+ baker winning-ads search "delivery driver recruitment" --audience-temperature 0_cold --exclude-brand-reliant --output md
3175
3532
  ```
3176
3533
 
3177
3534
  | Flag | Purpose |
3178
3535
  |---|---|
3179
3536
  | `query` (positional) | Free-text natural-language query |
3180
3537
  | `--ref-ad-id <id>` | Find ads similar to this ad (instead of free text) |
3538
+ | `--scope followed-first\|followed\|all` | Which brands to draw from (default `followed-first`: the company's followed brands, topped up from the shared library; the whole library when it follows nobody) |
3539
+ | `--sort winner\|longest_running\|newest` | Order of a no-query browse (default `winner`); ignored when a query is given |
3181
3540
  | `--limit <n>` | Max results 1–100 (**default 10** — shortlist size) |
3182
- | `--max-per-advertiser <n>` | Cap results per advertiser 1–50 (default 3) |
3541
+ | `--max-per-advertiser <n>` | Unset (default): brands spread, each brand's best 3 first and the rest after the other brands, none dropped. `1`–`50`: drop a brand's ads past N (`1` = one reference per brand). `0`: rank order. Brands the query names ("HubSpot video ads") are never capped |
3183
3542
  | `--min-relevance <0-1>` | Relevance floor; trims weak matches |
3184
- | `--platform <list>` | `meta,linkedin` — pass a single value to search **only** that platform |
3543
+ | `--platform <list>` | `meta,linkedin,tiktok` — pass a single value to search **only** that platform |
3185
3544
  | `--format <list>` | `video,static,carousel` |
3186
3545
  | `--winner-category <list>` | `winner,scaled_winner,evergreen,rising,untested,dud,…` (default: all) |
3187
3546
  | `--awareness <list>` | `unaware,problem_aware,solution_aware,product_aware,most_aware` |
3547
+ | `--audience-temperature <list>` | Who the ad was made for: `0_cold,1_warm_engaged,2_hot_visited,3_customer` |
3548
+ | `--funnel-stage <list>` | `TOF,MOF,BOF` |
3549
+ | `--advertiser-size <list>` | `solo,smb,mid,enterprise` |
3550
+ | `--exclude-brand-reliant` | Drop ads whose copy only works if the viewer already knows the brand (ads not judged yet are kept) |
3188
3551
  | `--advertiser-id <list>` | **Restrict to** these advertiser ids (browse one brand's winners) |
3189
- | `--exclude-advertiser <list>` | **Drop** these advertiser ids — your own brand + already-used references |
3190
- | `--country <list>` / `--language <list>` | Filter by country / language codes |
3191
- | `--first-seen-after` / `--first-seen-before` | ISO datetime bounds (recency) |
3552
+ | `--exclude-advertiser <list>` | **Drop** these advertiser ids, usually your own brand |
3553
+ | `--country <list>` | ISO country codes: ads known to run there, plus ads with no country data (every Meta ad) in that country's main language |
3554
+ | `--language <list>` | ISO 639-1 codes (`es,en`) for the language of the ad's copy and voiceover |
3555
+ | `--first-seen-after` / `--first-seen-before` | ISO date (`2026-01-01`, read as the start of that day in UTC) or datetime bounds (recency) |
3192
3556
  | `--hook-archetype <list>` | Narrow to ads on a proven hook **seed** (from `winning-ads hooks`) |
3557
+ | `--format-pattern <list>` | Narrow to statics in these containers (from `winning-ads formats`, e.g. `comparison_table`) |
3558
+ | `--diverse` | One varied page (different brands, containers, hooks) instead of rank order; ignores paging |
3559
+ | `--all-kinds` | Also return Google search text, Shopping and app ads. By default only designed pictures and videos come back; `--format text` (or a query asking for search ads) returns search ads alone |
3193
3560
  | `--output json\|md\|files` | Output format (default json) |
3561
+ | `--fields <list>` | Only these row fields (e.g. `advertiser,container,mechanism`); a `--full`-only field turns `--full` on, an unknown one is refused with `error.fix.validValues` |
3194
3562
  | `--full` | Include DNA detail + longevity |
3195
3563
 
3564
+ A value outside a closed list (`--platform`, `--format`, `--winner-category`, `--awareness`, `--audience-temperature`, `--funnel-stage`, `--advertiser-size`, `--format-pattern`), a non-numeric `--limit`/`--max-per-advertiser`/`--min-relevance`, or a bound that is not a date is refused before the search runs; a refused list value comes with `error.fix.validValues`. References this company already adapted (`already_adapted`) never take a fresh reference's place: `--limit N` returns up to N fresh ones first. The search waits up to 120 s (`followed-first` can run two library searches). A library that does not answer in time is `UNAVAILABLE`, with an `error.fix` to retry once narrower. An empty answer carries a hint naming the one filter to loosen.
3565
+
3196
3566
  Reading the scores: **`relevance`** (0–1) = match of the creative to your query; **`winner_score`** = how proven the ad is in-market. Pick references that are both relevant *and* proven.
3197
3567
 
3198
3568
  ### `baker winning-ads hooks|mechanisms|formats`
3199
3569
 
3200
- Read the **winner-weighted rollup** of the ad-dna corpus → `GET /api/ad-library/seed-catalog` — the hook /
3201
- mechanism / static-format patterns that actually win, ranked by `winner_score`, keyed on the closed seeds/enums
3202
- the extractor emits (no clustering). Ground a creative in a **proven** pattern for the exact segment instead of
3203
- guessing. (Distinct from `winning-ads patterns`, which contrasts two ad cohorts.)
3570
+ Read the pattern rollup of the ad-dna corpus → `GET /api/ad-library/seed-catalog` — the hook /
3571
+ mechanism / static-format patterns that win **inside brands**, among the creatives of the segment you pass
3572
+ (`--platform`, `--awareness`, `--industry`; an unset one means any), keyed on the closed seeds/enums the
3573
+ extractor emits (no clustering). Ground a creative in a **proven** pattern for the exact segment instead of
3574
+ guessing. The rollup is recomputed daily.
3204
3575
 
3205
3576
  ```bash
3206
3577
  # Top winning hook seeds for problem-aware SaaS on Meta:
3207
- baker winning-ads hooks --platform meta --awareness problem_aware --industry saas --output md
3578
+ baker winning-ads hooks --platform meta --awareness problem_aware --industry "Software & SaaS" --output md
3208
3579
  # Then narrow a reference search to that proven hook:
3209
3580
  baker winning-ads search "onboarding demo" --hook-archetype callout --output md
3210
- # Also: mechanisms (persuasion), formats (static ad layouts):
3581
+ # Also: mechanisms (how the product claims to work, not the ad's device), formats (static ad layouts):
3211
3582
  baker winning-ads mechanisms --platform linkedin --output md
3212
3583
  baker winning-ads formats --awareness solution_aware --output md
3213
3584
  ```
@@ -3216,12 +3587,37 @@ baker winning-ads formats --awareness solution_aware --output md
3216
3587
  |---|---|
3217
3588
  | `--platform <p>` | Segment on a single platform |
3218
3589
  | `--awareness <stage>` | Segment on a single awareness stage |
3219
- | `--industry <seed>` | Segment on a single industry seed |
3590
+ | `--industry <seed>` | Segment on a single industry seed from the extractor vocabulary (`seedVocabularies.yaml`), any case; a term naming exactly one seed resolves to it (`saas` → `Software & SaaS`), an ambiguous or unknown one is refused with the valid list |
3220
3591
  | `--limit <n>` | Max keys 1–100 (default 20) |
3221
3592
  | `--full` | Include the per-key segment breakdown |
3222
3593
 
3223
- Each key returns `count`, `mean_winner_score`, and `winner_share`. Ranking is winner-weighted volume, so a
3224
- proven grand-slam pattern floats to the top and a measured dud is floored.
3594
+ Each key returns `lift`, `brands_for`, `brands_against`, `count`, `mean_winner_score` and `winner_share`. The
3595
+ ranking is `lift`: how much more often a brand's creatives using the pattern kept running 60 days than that
3596
+ brand's others, one vote per brand, 0 until five brands have a verdict. `brands_for` / `brands_against` count
3597
+ the brands it won and lost inside. A common pattern is not a winning one: `count` is how often it is used.
3598
+
3599
+ ### `baker winning-ads fit <adId>`
3600
+
3601
+ Before building on a reference: why the ad wins, read against **this** company. One billed vision call, cached per ad
3602
+ under `canvas/.cache/fit/<platform>-<adId>.json` (asking again is free).
3603
+
3604
+ ```bash
3605
+ baker winning-ads fit a_12345 --platform meta
3606
+ ```
3607
+
3608
+ Returns `why_it_wins` in five layers (`disguise`, `voice`, `trigger`, `attention`, `logic`), the `load_bearing`
3609
+ elements that carry them (each with what it `needs`, whether the company has it: `have` | `adapt` | `missing`, and
3610
+ `ours`, what replaces it), a `verdict`: `use`, `adapt` or `reject`, and what made the ad a winner, so what an ad built
3611
+ on it carries (`carry`: `whole_ad`, `layout`, `hook` or `angle`, with `carry_why`; renders see the reference's picture
3612
+ only at `whole_ad` or `layout`). The company's Knowledge, reviews and market, and the library's evidence that the ad won
3613
+ (its winner category, angle, hook and composition), ground the answer. `baker canvas scaffold-static-ad --source-ad-id <adId> --source-platform <platform>` reads the cached
3614
+ answer, writes it into `prompt.json` as `concept`, and refuses a `reject` with `concept_does_not_fit` before anything is
3615
+ billed; the hint names the platform the fit was asked on, so a TikTok or LinkedIn reference is read from its own library.
3616
+
3617
+ | Flag | Default | Description |
3618
+ |---|---|---|
3619
+ | `--platform <meta\|linkedin\|tiktok\|google>` | `meta` | Which platform the id is on. The fit reads a still picture: a video ad (YouTube included) is refused (`VIDEO_REFERENCE`, read it with `content`), and on Google a search text ad, Shopping listing or app-install ad is refused (`NOT_A_DESIGN_REFERENCE`) |
3620
+ | `--full` | off | The whole answer, with the reasoning per element |
3225
3621
 
3226
3622
  ### `baker winning-ads advertisers <brand>`
3227
3623
 
@@ -3232,17 +3628,31 @@ baker winning-ads advertisers "Acme" --output md # find our own adverti
3232
3628
  baker winning-ads advertisers "Deel" --platform meta --output md
3233
3629
  ```
3234
3630
 
3235
- ### `baker winning-ads follow "<domain | profile URL | brand>" --platform meta|linkedin`
3631
+ ### `baker winning-ads follow "<website | brand | link>"`
3236
3632
 
3237
- Add **all** of a brand's ads to your library — every platform (Meta + LinkedIn) and every country → `POST /api/ad-library/follow`. `--platform` is **required**, but it only tells us how to read your `input` (Facebook vs LinkedIn URL); it does **not** limit what we track. A bare domain is best: we resolve both the Meta page and the LinkedIn company from it and track both. Discovery is never region-scoped. The result `status` is one of:
3633
+ Add **all** of a brand's ads to your library — every platform (Meta, LinkedIn, TikTok, Google Ads), every account it runs there, and every country → `POST /api/ad-library/follow`. Paste whatever you have: a website is best — we resolve every Facebook page and LinkedIn company the site links (a brand often runs one per market), its TikTok account, and every Google Ads entity running its ads (never a reseller) — but a brand name or any profile / ad-library link works too. Discovery is never region-scoped. `--platform` is accepted and ignored. The result `status` is one of:
3238
3634
 
3239
3635
  - `following` — the brand is already in the corpus; you're now subscribed (no wait).
3240
3636
  - `added` — a new brand was queued for ingestion; its ads appear as discovery completes (a `hints[]` note flags this).
3241
- - `ambiguous` — the input mapped to multiple brands; pick one from `candidates` and re-run with a more specific domain/URL.
3637
+ - `ambiguous` — the input mapped to multiple brands; pick one from `candidates` and re-run with a more specific website/link.
3638
+
3639
+ `suggestions` lists accounts only *named* like the brand (say, another country's Facebook page its site doesn't link). They are not tracked; `hints[]` carries a ready `add-accounts` command for them.
3242
3640
 
3243
3641
  ```bash
3244
- baker winning-ads follow "deel.com" --platform meta
3245
- baker winning-ads follow "https://www.linkedin.com/company/acme" --platform linkedin --label "Acme (competitor)"
3642
+ baker winning-ads follow "deel.com"
3643
+ baker winning-ads follow "https://www.linkedin.com/company/acme" --label "Acme (competitor)"
3644
+ ```
3645
+
3646
+ ### `baker winning-ads discover "<website | brand | link>"`
3647
+
3648
+ The `accounts` and `suggestions` a follow would find, without following anything → `POST /api/ad-library/discover`. Paid lookups. Each account is compact — `platform`, `name`, `followers`, `ads_url` (its public ad library, to check it is the brand's) and `input` (what to paste into `add-accounts`); `--full` adds every other field the lookup read.
3649
+
3650
+ ### `baker winning-ads add-accounts <advertiser> "<link,link,...>"`
3651
+
3652
+ Track more accounts under a brand you already follow — another market's Facebook page, a second LinkedIn company, another Google Ads entity → `POST /api/ad-library/accounts/add`. Each input is a profile or ad-library link, a Google advertiser id (`AR…`) or a website; up to 20. The response lists `added`, `already_tracked`, and `failed` (with a reason per input); one input failing never blocks the rest. An account that already belongs to a different brand in the library is refused — follow it as its own brand.
3653
+
3654
+ ```bash
3655
+ baker winning-ads add-accounts adv_123 "https://www.facebook.com/barkibu.it,AR01614014350098432001"
3246
3656
  ```
3247
3657
 
3248
3658
  ### `baker winning-ads following`
@@ -3264,7 +3674,7 @@ Winners across **every** brand you follow, in one library → `GET /api/ad-libra
3264
3674
  Response `data`: `winners`, `following_count` (how many brands you follow), `advertiser_ids` (the ids actually queried = follows ∩ requested), and — when relevant — `hints` (e.g. you follow nobody yet, or a requested id isn't in your follows and was skipped). In `--output md`/`files` the table is the payload and the hints are written to **stderr** so they still surface without corrupting the output.
3265
3675
 
3266
3676
  ```bash
3267
- baker winning-ads feed --per-advertiser 5 --output md
3677
+ baker winning-ads feed --output md
3268
3678
  baker winning-ads feed --advertiser adv_123,adv_456 --platform meta --output md
3269
3679
  ```
3270
3680
 
@@ -3272,26 +3682,36 @@ baker winning-ads feed --advertiser adv_123,adv_456 --platform meta --output md
3272
3682
  |---|---|
3273
3683
  | `--advertiser <ids>` | Trim to a comma-separated subset of your followed advertiser ids (ids you don't follow are skipped and reported) |
3274
3684
  | `--platform meta\|linkedin` | Filter to a single platform |
3275
- | `--per-advertiser <n>` | Max winners kept per advertiser 0–50 (`0` = no cap, default 5) |
3685
+ | `--per-advertiser <n>` | Unset (default): brands spread, each brand's best 3 first and the rest after, none dropped. `1`–`50`: keep at most N per brand. `0`: rank order |
3276
3686
  | `--limit <n>` | Total winners across all follows 1–200 (default 40) |
3277
3687
  | `--winner-category <list>` | Restrict to specific winner categories |
3278
3688
  | `--format <list>` | Restrict to specific formats (e.g. `static,video`) |
3689
+ | `--all-kinds` | Also return Google search text, Shopping and app ads (default: designed pictures and videos only; `--format text` returns search ads alone) |
3279
3690
 
3280
3691
  ### `baker winning-ads winners <advertiser>`
3281
3692
 
3282
- Top winning ads for one advertiser id → `GET /api/ad-library/advertiser-winners` (use `feed` for all your follows at once). Same lean winner cards as `search` (add `--full` for DNA + longevity). Supports `--top N` and `--platform meta|linkedin`.
3693
+ Top winning ads for one advertiser id → `GET /api/ad-library/advertiser-winners` (use `feed` for all your follows at once). Same lean winner cards as `search` (add `--full` for DNA + longevity). Supports `--top N`, `--platform meta|linkedin|tiktok|google` (omit it for the top winners across every platform) and `--per-platform`, which adds `by_platform`: each platform's own top winners, since scores don't compare across platforms.
3283
3694
 
3284
3695
  ```bash
3285
- baker winning-ads winners adv_123 --top 15 --output md
3696
+ baker winning-ads winners adv_123 --per-platform --top 5 --output md
3697
+ ```
3698
+
3699
+ ### `baker winning-ads ads <advertiser>`
3700
+
3701
+ Every ad one advertiser has run → `GET /api/ad-library/advertiser-ads`, one row per distinct ad, the same cards as `winners`. `--platform meta|linkedin|tiktok|google` narrows it to one platform (omit it for all together), `--format video,static,carousel,text` to those formats (`text` is a Google search ad), `--running all|running|stopped` to what still runs or what stopped. Rows come strongest first. Pages with `--limit` (1-100, default 24) and `--offset`: the response carries `total` and `next_offset` (`null` on the last page).
3702
+
3703
+ ```bash
3704
+ baker winning-ads ads adv_123 --platform google --running running --output md
3705
+ baker winning-ads ads adv_123 --offset 24
3286
3706
  ```
3287
3707
 
3288
3708
  ### `baker winning-ads content <ad-id>`
3289
3709
 
3290
- 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`).
3710
+ 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`, plus `landing_url`; a Google ad also carries `google_ad_type`). A **static** or **carousel** ad returns the text drawn into the image, each line led by its job and card (`card 2 · headline: …`); a static also returns its `format_pattern`. Add `--full` for how the ad is built (composition, colours, CTA drawn in the image, markers such as price tags or badges), how it persuades (awareness stage, mechanism, pain points, desires, value props, proof, objections handled), what it offers (offer type, pricing, entry price, guarantee, urgency, bonus), and for video its speech, pacing, soundtrack and each overlay's role + position (e.g. `GLOBAL PAYROLL @0.5s [hook, top]`). Fields the ad does not carry are left out. `--platform meta|linkedin|tiktok|google` selects which platform the id is on (default `meta`).
3291
3711
 
3292
3712
  ```bash
3293
3713
  baker winning-ads content adg_123 # transcript + on-screen text + copy
3294
- baker winning-ads content adg_123 --platform meta --full # + speech, pacing, soundtrack
3714
+ baker winning-ads content adg_123 --platform meta --full # + build, persuasion, offer, video speech/pacing/soundtrack
3295
3715
  baker winning-ads content adg_123 --full --output md
3296
3716
  ```
3297
3717
 
@@ -3303,23 +3723,21 @@ Stop following a brand by advertiser id → `POST /api/ad-library/unfollow`. Ret
3303
3723
  baker winning-ads unfollow adv_123
3304
3724
  ```
3305
3725
 
3306
- ### `baker winning-ads brief`
3307
-
3308
- Generate a creative brief grounded in strategically-similar winners → `POST /api/ad-library/brief`. Optionally describe the target creative with `--dna` (a JSON object), steer with `--notes`, and cap references with `--k`. Returns `brief_markdown` + `reference_ad_ids`.
3309
-
3310
- ```bash
3311
- baker winning-ads brief --dna '{"angle":"cost savings","awareness_stage":"solution_aware"}' --notes "B2B, LinkedIn video" --k 8
3312
- ```
3313
-
3314
- ### `baker winning-ads patterns --winners <adIds> --duds <adIds>`
3726
+ ### Decisions about text (`baker decide`)
3315
3727
 
3316
- Mine what separates two cohorts of ads → `POST /api/ad-library/patterns`. Pass a comma-list of winning ad ids (`--winners`, cohort A) and weaker/dud ad ids (`--duds`, cohort B), plus optional `--top-n`. Returns each discriminating DNA field with the cohort it `favors` (`winners`/`duds`), a `score`, and the top values on each side.
3728
+ Typed questions about text, answered by Jev (a decision model) with a probability: a yes/no (`noul`), one option of your set (`choice`) or a level on a scale (`score`). Read-only, text only, never generates prose. Works with a `bk_`, `bkr_` or `bkv_` key, so a published Report can call it.
3317
3729
 
3318
3730
  ```bash
3319
- baker winning-ads patterns --winners a_1,a_2,a_3 --duds a_9,a_8 --top-n 10 --output md
3731
+ baker decide --questions '{"b2b":{"type":"noul","question":"Is `message` written for a business buyer?"}}' --state '{"message":"…"}'
3732
+ baker decide --questions questions.json --items ads.json --state '{"advertiser":"Notion"}' [--full]
3320
3733
  ```
3321
3734
 
3322
- ---
3735
+ - `--questions` (required): `{ "<name>": { "type": "noul", "question" } | { "type": "choice", "question", "options": { "<key>": "<meaning>" } } | { "type": "score", "question", "levels": [lowest, …, highest] } }`.
3736
+ - `--items`: `[{ "id", "text": "…" | { "<field>": "…" } }]`. Every question is asked about every item, which reaches Jev as `` `item` `` inside its own question. Up to 500 items of 4,000 characters, 2,000 questions per call; packed into requests of up to 200 questions sent at once.
3737
+ - `--state`: shared context, or the text itself when there are no items (inline JSON, a file, `-`, or plain text).
3738
+ - Each flag that takes JSON accepts it inline, as a file path, or `-` for stdin.
3739
+ - Output: `{ model, answers | items: [{ id, answers | null }], unanswered, requests, costUsd }`. A noul is `{ noul }`; a choice `{ choice, probability, confidence }`; a score `{ score, level, confidence }`. `--full` adds every option's `probabilities`.
3740
+ - When the model does not answer, the error carries `fix.action: "degrade"`: carry on without the answers rather than abort.
3323
3741
 
3324
3742
  ### Scheduled agents (`baker scheduled-agents`)
3325
3743
 
@@ -3340,6 +3758,7 @@ baker scheduled-agents create --name "Celebrate the first sale" --description "O
3340
3758
  baker scheduled-agents update <id-or-temp_agent_id> --enabled false
3341
3759
  baker scheduled-agents update <id-or-temp_agent_id> --run-at "2026-09-02T07:00:00Z" --prompt "Use the launch checklist"
3342
3760
  baker scheduled-agents update <id-or-temp_agent_id> --trigger experiment.ended
3761
+ baker scheduled-agents update <id-or-temp_agent_id> --slack "#marketing" # each run opens a thread there; also here | me | off
3343
3762
  baker scheduled-agents delete <id-or-temp_agent_id>
3344
3763
  baker scheduled-agents unstage <id-or-temp_agent_id> # everything this chat staged on it
3345
3764
  baker scheduled-agents unstage <id-or-temp_agent_id> --index 2 # one staged change only
@@ -3349,8 +3768,11 @@ baker scheduled-agents run <id>
3349
3768
  baker scheduled-agents templates --list # every recipe this company can run
3350
3769
  baker scheduled-agents templates --category measurement # one group only
3351
3770
  baker scheduled-agents create --template baker-ads --name "Weekly ad review" --description "..." --cron "0 8 * * MON"
3771
+ baker scheduled-agents create --template baker-store-out-of-stock # --name and --description default to the recipe's own
3352
3772
  ```
3353
3773
 
3774
+ With `--template`, `--name`, `--description` and `--prompt` are optional: the recipe's name, line and brief are used, and `--prompt` is appended to the brief. An item on a stock event whose name or line says it acts automatically is refused — every run's ad changes wait for review.
3775
+
3354
3776
  Triggers:
3355
3777
 
3356
3778
  | `--trigger` | Fires when | Narrow it with |
@@ -3414,15 +3836,16 @@ baker scheduled-agents templates --delete weekly-posthog-check
3414
3836
 
3415
3837
  ### Marketing Tags (`baker tags`)
3416
3838
 
3417
- Read the client's marketing/analytics tags (Meta pixel, GA4, Google Ads, GTM, Clarity, Hotjar, PostHog, …) — production tags overlaid with the changes staged in this chat.
3839
+ Read the client's marketing/analytics tags (Meta pixel, ChatGPT Ads pixel, GA4, Google Ads, GTM, Clarity, Hotjar, PostHog, …) — production tags overlaid with the changes staged in this chat.
3418
3840
 
3419
- **The CLI is read-only.** Every tag change (create, edit, delete) goes through the `request_tag_input` tool (`baker_ui` MCP server): the agent proposes one or more changes — each becomes a tab in one blocking approval form — pre-filling the non-secret fields it knows; the user reviews, edits, fills secret fields, and approves or skips each tab. Approved changes stage on the chat and apply when the chat is published; discarding the chat drops them.
3841
+ **The CLI never creates or edits a tag.** Every tag change (create, edit, delete) goes through the `request_tag_input` tool (`baker_ui` MCP server): the agent proposes one or more changes — each becomes a tab in one blocking approval form — pre-filling the non-secret fields it knows; the user reviews, edits, fills secret fields, and approves or skips each tab. Approved changes stage on the chat and apply when the chat is published; discarding the chat drops them.
3420
3842
 
3421
3843
  `BAKER_CHAT_ID` must be set.
3422
3844
 
3423
3845
  ```bash
3424
3846
  baker tags list # effective view: production + staged, full readable config + secret status
3425
3847
  baker tags draft # review the staged changes awaiting publish
3848
+ baker tags unstage tag_temp_abc123 # take back one change this chat staged (or --all)
3426
3849
  ```
3427
3850
 
3428
3851
  Notes:
@@ -3430,8 +3853,9 @@ Notes:
3430
3853
  - **Both reads restate the write path, on stderr.** `list` and `draft` end with the two lines that say the approval form is the only way to change a tag, that it rides on every turn, and that a tag change is never a Task. They are there because a chat concluded from its tool list alone that the form did not exist, never called it, and filed the change as a Task for the user to carry out by hand. stdout stays the list, so piping it is unaffected; `--json` carries the same lines in the envelope's `hints`.
3431
3854
  - **`list` prints each tag's full readable config.** Every non-secret field is shown in full under the tag — the Clarity `projectId`, GA4 `measurementId`, the entire custom `code` snippet, etc. — so the agent reuses a stored value to pre-fill a follow-up change instead of asking the user for something already installed. Secrets are never among these fields. (`--json` emits the same data as the raw envelope.)
3432
3855
  - **Secrets never travel through this CLI or the chat.** Secret fields (`accessToken`, `apiSecret`, `authorizationToken`, `apiKey`, `conversionToken`, `oauthProviderId`) are entered only in the dashboard's secure tag form and flow straight into the staged draft; tool responses only ever name which secret fields are set/pending.
3856
+ - A `chatgptAds` tag takes a Pixel ID. Its optional `apiKey` is the Conversions API key and enables the server copy of page and Form events; the key stays out of the CLI and published page.
3433
3857
  - Staged creates get a server-generated `tag_temp_*` ref (returned in the tool result and printed by `list`). Use it (or a real tag id) as flow side-effect `tagIds` — the published tag keeps resolving under the temp ref.
3434
- - Proposing a delete on a `tag_temp_*` ref drops the staged create instead.
3858
+ - **`unstage` is the one write.** It takes back a change this chat staged — the same as discarding the row in the change review — by the ref `draft` prints, or `--all`. It carries no config and no secret, never touches a live tag, and refuses a ref nothing is staged under. Taking back a staged create drops the secrets entered for it with it.
3435
3859
  - Single-instance types (`code`, `posthog`, `datafast`) reject a second instance against the chat's effective view.
3436
3860
  - Conflicts at publish (tag deleted in the dashboard, config invalid) skip the op with a recorded reason — they never block the publish.
3437
3861
 
@@ -3439,11 +3863,13 @@ Notes:
3439
3863
 
3440
3864
  Read and shape this workspace's Forms (flows). Operates on the local `_data.json` files only; no secret values are ever decrypted or printed.
3441
3865
 
3442
- **A Form has two halves, split by a trust boundary.** Its *structure* — steps, conditions, side effects, field mappings — is yours, and the write subcommands below edit exactly that. Its *confidential* half — connection secrets, OAuth accounts, third-party field definitions (HubSpot, Calendly, HighLevel, SavvyCal) — is configured through the `request_flow_input` tool (`baker_ui` MCP server): the agent authors the surrounding structure and references it by `nodeId`/`sideEffectId`, and the user enters secrets, connects accounts, and picks resources in the dashboard. Those values are written (encrypted, where a secret) into the Form on the chat's branch and go live when the chat is published. Nothing any of these commands does touches a live system.
3866
+ For ChatGPT Ads conversions, add a `chatgptAds` side effect to the Form step, choose a supported event name, and use `baker flows map` to map `advancedMatching` and optional `eventParams` (`value` and `currency`).
3867
+
3868
+ **A Form has two halves, split by a trust boundary.** Its *structure* — steps, conditions, side effects, field mappings — is yours, and the write subcommands below edit exactly that. Its *confidential* half — connection secrets, OAuth accounts, third-party field definitions (HubSpot, Calendly, HighLevel, SavvyCal) — is set with `baker flows connect` whenever the agent knows the answer (an email's recipients and subject always; a connected platform's resource; a webhook with no token), and otherwise through the `request_flow_input` tool (`baker_ui` MCP server): the agent authors the surrounding structure and references it by `nodeId`/`sideEffectId`, and the user enters secrets, connects accounts, and picks resources in the dashboard. Those values are written (encrypted, where a secret) into the Form on the chat's branch and go live when the chat is published. Nothing any of these commands does touches a live system.
3443
3869
 
3444
3870
  ```bash
3445
3871
  baker flows list # every Form + how many confidential fields still need setup
3446
- baker flows show contact # confidential fields + whether the destination changed under this Form
3872
+ baker flows show contact # confidential fields, where each sealed destination sends (never a credential), and drift
3447
3873
  baker flows show contact --full # include the full flow tree (secret values redacted)
3448
3874
 
3449
3875
  baker flows schema # both vocabularies + the customForm field types
@@ -3452,6 +3878,16 @@ baker flows schema side-effect googleSpreadsheet
3452
3878
 
3453
3879
  baker flows add-node contact --type text --name "Thanks"
3454
3880
  baker flows add-side-effect contact --node <nodeId> --type googleSpreadsheet # triggerId comes from the step
3881
+ baker flows connect contact --side-effect se-hubspotForm-1 --hubspot-form <formId> # attach the connected HubSpot account + form, no card
3882
+ baker flows connect contact --all --hubspot-form <formId> # every hubspotForm step still unwired, one call
3883
+ baker flows options calendly # what a step/destination can use: calendly, savvycal, highlevel, highlevelForm, googleSpreadsheet
3884
+ baker flows connect contact --node book --resource <id> # fill a Calendly/SavvyCal/GoHighLevel step yourself
3885
+ baker flows connect contact --side-effect se-gsheet-1 --resource <spreadsheetId> # a sheet Baker can reach (--tab to pick the tab)
3886
+ baker flows connect contact --side-effect se-gsheet-1 --new-sheet "Contact leads" --columns "Name,Email" # create one and wire it
3887
+ baker flows connect contact --side-effect se-pipedrive-1 # Pipedrive / GoHighLevel contact: the account is the destination
3888
+ baker flows connect contact --side-effect se-email-1 --email "ana@x.com, luis@x.com" --subject "New lead — Contact" # an email destination: the agent states it, never a card
3889
+ baker flows connect contact --side-effect se-httpWebhook-1 --url https://hooks.example.com/lead # a webhook that needs no token
3890
+ baker flows connect contact --all --type googleSpreadsheet --same-as madrid:se-gsheet-1 # copy another Form's destination
3455
3891
  baker flows map contact --from map.json --write # a whole mapping at once — past ~3 fields, use this
3456
3892
  baker flows map contact --map A=nombre --write # one or two fields
3457
3893
  baker flows map contact --hubspot-form <guid> --from map.json --write # HubSpot: field types read off the form
@@ -3463,7 +3899,10 @@ baker flows normalize contact --write # repairs a nulled optional;
3463
3899
  Notes:
3464
3900
 
3465
3901
  - **`schema` takes the kind as an argument because `hubspot` is both** — a node type (embeds a form on the page) and a side-effect type (fires a behavioural event). They are different things with one name.
3466
- - **Configure the destination before you map.** `columnMapping`/`fieldMapping` describe a *specific* destination — sheet columns, a portal's field names — that only `request_flow_input` can see, and that call returns it. A mapping written first is valid JSON that ships and drops every submission; `verify` blocks the half-wired result.
3902
+ - **`connect` wires a HubSpot form without asking the user.** When HubSpot is connected and the agent knows which form is meant, `baker flows connect <slug> --hubspot-form <formId> [--side-effect <id>]` attaches the account and the form to a `hubspotForm` side effect (`--all`: every one in the Form still missing it, never repointing one already wired) — the same thing the chat card writes, sealed by Baker so the config never passes through the agent. It refuses a form with spam prevention (captcha) on, and returns `destination.formGuid` for `map --hubspot-form`. The card (`request_flow_input`) stays for an unconnected account or an ambiguous choice.
3903
+ - **`options` + `connect` do the same for every other connected platform.** `baker flows options <kind>` lists what the connected account holds (Calendly events, SavvyCal links, GoHighLevel calendars and forms, the Google spreadsheets Baker can reach); `connect --node|--side-effect <id> --resource <id>` writes exactly what the card would. Pipedrive and a GoHighLevel contact take no resource. `--new-sheet` creates a spreadsheet in the connected Google account (Google only lets Baker reach sheets it created or the user chose), `--same-as <slug>:<id>` copies another Form's wired destination, `--all --type <kind>` fills every unwired one of a kind. Nothing connected answers `NOT_CONNECTED` with the `request_connection` fix; several accounts connected need `--account <id>`.
3904
+ - **`--email` / `--url` replace a destination whole, so they leave alone what only the user can set.** `--all --email` passes over an email that replies to the visitor's own answer (and says so in its hints); naming one is refused. A webhook that sends a credential keeps its address until the user re-enters both through `request_flow_input`. Unreadable settings are also left alone by default. For one unreadable email, `--replace-unreadable --email <complete list> --subject <text>` replaces the old setup when the user intends replacement, without revealing it. Never use this to add to an unknown recipient list; it cannot override `--all`, a known visitor reply, or a webhook. `flows show` reads back only destinations sealed for this company.
3905
+ - **Configure the destination before you map.** `columnMapping`/`fieldMapping` describe a *specific* destination — sheet columns, a portal's field names — that only `connect` or `request_flow_input` can set, and both return it. A mapping written first is valid JSON that ships and drops every submission; `verify` blocks the half-wired result.
3467
3906
  - **`map` reads the record shape off the side effect's type.** One property name, four record shapes: a Sheet is addressed by column letter, HubSpot by property name, Pipedrive and GoHighLevel by field key, Crmble by field id. That is why there is no `--key`, and why `--entity` and `--custom`/`--standard` are required rather than defaulted.
3468
3907
  - **A mapping is usually eight to fifteen fields, so `--from` is the main door.** It takes `{ "map": { "<destField>": "<value>" } }` from a file or stdin; `--map` flags are for one or two. Both produce identical output.
3469
3908
  - **Most CRM fields are not fed by a form field.** The value grammar is a bare form field (`nombre`), `field:Step.name` when two steps share a name, `tracking:utm_source`, `global:Name`, `code:exposed_value`, a `"literal"`, or any of those joined with `+`. Tracking names are validated against the engine's own `TRACKING_FIELD_IDS`, read out of the workspace rather than copied — attribution columns are the most common CRM mapping there is, and they were unexpressible before.
@@ -3529,6 +3968,8 @@ baker actions create --name "Build hero" --priority high --tags landing,creative
3529
3968
  # fuller fan-out hint: re-read the draft and merge actions that touch the same system into one.
3530
3969
  # If no --tags are passed, create emits a MISSING --tags hint — the action is invisible to the backlog's tag filter.
3531
3970
  # If no --priority is passed, create emits a MISSING --priority hint — the action ranks as normal/medium in the do-first order.
3971
+ # If the name or description holds a shell name such as /bin/bash or bash where a `$0` stood (double quotes let the
3972
+ # shell expand `$` amounts), create leads with a hint to fix it with `baker actions update <tempId>` in single quotes.
3532
3973
  baker actions update <action-id> --name "Better name" --tags google-ads,strategy # --tags REPLACES the set; '' clears
3533
3974
  baker actions update <action-id> --priority urgent # urgent|high|medium|low; 'none' clears
3534
3975
  baker actions update <tempId> --description "..." # a tempId from `create` in THIS chat edits the still-staged task in place
@@ -3537,10 +3978,17 @@ baker actions complete <action-id-or-tempId> --note "What was done" # stages
3537
3978
  # --note is REQUIRED (≥20 chars): what changed, where, and the outcome. It is the durable, client-facing
3538
3979
  # record surfaced by `baker actions log`. A missing/too-short note is rejected with a VALIDATION_ERROR.
3539
3980
  baker actions discard <action-id> --reason "obsolete"
3981
+ baker actions reopen <action-id> # stages — a completed Task goes back to the backlog on publish
3982
+
3983
+ # Assigning — staged, applies on publish. --to is a userId or email from `members`, or `none`
3984
+ baker actions members # teammates a Task can be handed to (freelancers hidden unless you are a superadmin)
3985
+ baker actions assign <id-or-tempId> --to maria@empresa.com
3986
+ baker actions assign <id> --to none # unassign
3540
3987
 
3541
3988
  # Tags — taxonomy is built-in defaults + company custom tags; --tags is STRICT (unknown slugs are rejected)
3542
3989
  baker actions tags list # available tags (defaults + company custom); errors → stderr (inline-safe)
3543
3990
  baker actions tags create --slug pmax --description "Performance Max work" # mint a custom tag
3991
+ baker actions tags update --slug pmax --description "Performance Max campaigns" # what it means; applies at once
3544
3992
  baker actions tags delete --slug pmax # remove a custom tag (default tags can't be deleted)
3545
3993
 
3546
3994
  baker actions link --blocker <id-or-tempId> --blocked <id-or-tempId>
@@ -3552,7 +4000,7 @@ baker actions draft --output md # one row per staged op (json defaul
3552
4000
  baker actions draft --output md --fields kind,name,ref,tags # pick the columns
3553
4001
  baker actions draft --output md --full # add each op's payload (description, note, reason, blocker)
3554
4002
  baker actions draft remove temp_hero # drop a staged create (cascades its complete/link ops)
3555
- baker actions draft remove <id> --op complete # drop a staged op on a real action (--op update|complete|discard)
4003
+ baker actions draft remove <id> --op complete # drop a staged op on a real action (--op update|complete|discard|assign|reopen)
3556
4004
  baker actions draft clear # drop everything staged in this chat
3557
4005
  ```
3558
4006
 
@@ -3590,26 +4038,33 @@ Permissions enforced server-side:
3590
4038
  - Only `claim`/`release` mutate live state.
3591
4039
  - `claim` returns action details (`id`, `name`, `description`) plus `recommendedSkills` — a fast model reads the action's name/description/tags against the local `.claude/skills/` catalog and returns the skills (`{ name, reason }`) most worth loading for the work, echoed as `/skill — reason` lines in `hints`. It is best-effort: if the catalog is absent or the model call fails, the claim still succeeds and falls back to a generic review hint. Every claim also carries a routing hint: actions often span several surfaces, so load every skill that owns part of the work and read the tool doc (`__tooling__/docs/tools/baker/<family>.md`) for each baker CLI family the work needs.
3592
4040
  - `complete` and `unlink` require the action to be claimed by the current chat — otherwise the API returns `FORBIDDEN` with a hint to claim first. Completing is the record that the work is done, so it stays claim-gated.
4041
+ - `assign` and `reopen` do not require a claim either, and are blocked the same way. `assign --to` is resolved only among the teammates the key's person can see (`members`), so a hidden freelancer cannot be named by id or email; restaging replaces the earlier assignment. At publish an assignee who lost access to the company is skipped as a conflict, never a failed publish.
4042
+ - `reopen` refuses a Task that is not completed, at stage time and again at publish.
4043
+ - `tags update` changes only what a custom tag means; the slug stays, because every Task carrying it stores it.
3593
4044
  - `update` and `discard` do NOT require a claim — a chat may edit or retire an action it never claimed. They are blocked only when another chat currently holds the claim (`CONFLICT` with that chat's title).
3594
4045
  - Claiming an action already claimed by another chat returns `CONFLICT` with the other chat's title — pick a different action.
3595
4046
  - All staged ops are reverted automatically when the chat is discarded.
3596
4047
 
3597
4048
  ---
3598
4049
 
3599
- ### `baker mcp connected | list | add | remove`
4050
+ ### `baker mcp connected | catalog | list | add | remove | verify | disable`
3600
4051
 
3601
4052
  See everything a chat can reach — managed integrations, custom servers, and connected platforms — and manage **custom MCP servers** by pointing the agent at any remote HTTPS MCP endpoint. Registered tools appear as `mcp__<name>__*` on the next chat message.
3602
4053
 
3603
4054
  ```bash
3604
4055
  baker mcp connected
4056
+ baker mcp catalog --search notion
3605
4057
  baker mcp list
3606
4058
  baker mcp add --name weather --url https://mcp.example.com/mcp
3607
4059
  baker mcp add --name acme --url https://acme.dev/mcp --header "Authorization: Bearer sk-…"
3608
4060
  baker mcp add --name mine --url https://my.dev/mcp --scope user
3609
4061
  baker mcp remove --name weather
4062
+ baker mcp verify --name weather # check it now (the dashboard's "Check now")
4063
+ baker mcp disable --name weather # stop its tools loading; only a person turns it back on (org-wide servers: a person only)
3610
4064
  ```
3611
4065
 
3612
4066
  - `connected` returns three lists. `managed` and `custom` are MCP tools the agent calls directly; `platforms` are the accounts the company signed in to (HubSpot, Google Ads, GA4, Search Console, Tag Manager, LinkedIn Ads), each with its login and the resources it may read. A platform has **no** `mcp__*` tools — it is read through its own command family — so it is listed here to keep the agent from concluding a working connection is missing.
4067
+ - `catalog --search <name> [--limit 1-20]` searches the managed apps a user can connect when a vendor has no MCP server of its own. Each app reports its slug (`app`), how the user connects it (`sign-in` or `key`; apps that need no sign-in cannot be connected and are left out) — never a managed copy of one of Baker's own connections: those come back under `platforms`, to ask for with `{ platform }` and whether this chat already reaches it (`connected`).
3613
4068
  - `--url` must be the **full MCP endpoint** (commonly ending in `/mcp`), HTTPS and public; loopback / private / cloud-metadata hosts are rejected. A bare domain frequently accepts the OAuth sign-in and then serves no tools, so every address is handshake-checked after it changes: `connected` and `list` report `usable: false` plus a `problem` (and a `suggestedAddress` when the server advertises a better endpoint) for a server that isn't serving tools, or `lastCheck: failed` when the last check only hit an outage — that one may work anyway, and a later turn re-checks it automatically. Repointing a server at a different host clears its saved sign-in: a token belongs to the host that issued it, so the server has to be connected again.
3614
4069
  - Scope is two independent choices — audience (just you / everyone) × level (this company / whole org):
3615
4070
  - `--scope company` (default) — everyone in this company.
@@ -3656,7 +4111,40 @@ baker history list --category ads --full # ad writes with raw metadata
3656
4111
 
3657
4112
  ---
3658
4113
 
3659
- ### `baker hubspot forms list | view | submissions` · `baker hubspot workflows list | view` · `baker hubspot pipelines list` · `baker hubspot contacts summary | lookup | properties | fields` · `baker hubspot deals properties | fields` · `baker hubspot meetings list | view`
4114
+ ### `baker credentials list | put | delete`
4115
+
4116
+ Credentials are a company's stored API keys, tokens and passwords. The CLI never returns a value: each credential has a placeholder, `{{credential:NAME}}`, that a request inside a Baker Session can carry in any header, URL or body. The Session's HTTPS proxy fills in the value only for the hosts the credential is bound to, and puts the placeholder back in responses.
4117
+
4118
+ ```bash
4119
+ baker credentials list # name, placeholder, allowedHosts, label
4120
+ baker credentials list --full # + source, lastUsedAt
4121
+ printf %s "$KEY" | baker credentials put stripe-key --host api.stripe.com --label "Stripe key"
4122
+ baker credentials delete stripe-key
4123
+ ```
4124
+
4125
+ - `put` reads the value from **stdin only** — a `--value` flag or a positional value is refused, because argv is visible to other processes. One trailing newline is trimmed; empty stdin is refused. `--host` is repeatable (or comma-separated) and accepts `*.example.com` wildcards.
4126
+ - `put` and `delete` act only on credentials the agent created. Credentials a person added are managed in the dashboard (Settings → Credentials).
4127
+ - The company must have credentials switched on; otherwise every command answers `FORBIDDEN`.
4128
+
4129
+ ---
4130
+
4131
+ ### `baker setup-links create | list`
4132
+
4133
+ A setup link lets whoever holds the company's accounts connect them without a Baker login. `create` makes one at once and returns its `url` (valid 30 days); the link is also listed on the chat as already made. Revoke links in the dashboard (Settings → Setup links).
4134
+
4135
+ ```bash
4136
+ baker setup-links create --connect google-ads,meta --tags meta # sign in to accounts, add tracking tags
4137
+ baker setup-links create --keys chatgpt-ads --domains shop.acme.com # paste a key, set a domain's DNS
4138
+ baker setup-links create --apps slack --mcp crm=https://mcp.example.com/sse
4139
+ baker setup-links list # active links: asks, progress, url, expiresAt
4140
+ ```
4141
+
4142
+ - Every flag is repeatable or comma-separated; at least one is required. Unknown tag, connection or key-account names are refused with the valid list. A `--domains` hostname must already be one of the company's domains.
4143
+ - `list` returns what each link asks for in the same words `create` takes. A read-only key sees no `url`.
4144
+
4145
+ ---
4146
+
4147
+ ### `baker hubspot forms list | view | submissions` · `baker hubspot workflows list | view` · `baker hubspot pipelines list` · `baker hubspot contacts summary | lookup | properties | fields` · `baker hubspot companies search` · `baker hubspot deals list | properties | fields` · `baker hubspot meetings list | view`
3660
4148
 
3661
4149
  Read-only view of the company's **connected HubSpot account** — the forms and meeting links (calendars) that live there, the leads those forms received, and the workflows, deal pipelines, and contact records a lead runs through afterwards. Fetched from HubSpot live on every call; nothing is cached and nothing is ever written back.
3662
4150
 
@@ -3678,6 +4166,8 @@ baker hubspot pipelines list # where a lead lands, and the st
3678
4166
  baker hubspot contacts summary --days 30 # are recent leads being worked? (counts only)
3679
4167
  baker hubspot contacts lookup lead@example.com # was this one lead picked up?
3680
4168
  baker hubspot contacts properties # what a HubSpot-read source can watch, with each value
4169
+ baker hubspot companies search --domains acme.com,globex.io --with-deals # CRM accounts by domain, with their deals
4170
+ baker hubspot deals list --pipeline default --modified-since 2026-01-01 # deals: amount, stage, closed won
3681
4171
  baker hubspot meetings list # every calendar + whether booking redirects
3682
4172
  baker hubspot meetings view <slug> # booking-form fields for one calendar
3683
4173
  baker hubspot meetings view <slug> --as-node # the form.external blob for a flow node
@@ -3688,7 +4178,9 @@ baker hubspot meetings view <slug> --as-node # the form.external blob for a f
3688
4178
  **`forms submissions`:** positional `<formId>`; `--days <1-365>` (default 30) sets the window; `--full` adds each submission's field values. The default response is counts and dates only — `total`, `inWindow`, `lastSubmittedAt`, and per-day counts — because submissions carry the lead's own contact details. `truncated: true` means the page budget ran out before the window was covered, so `inWindow` is a floor rather than an exact count.
3689
4179
  **`workflows list` flags:** `--search <text>`, `--enabled-only`. Disabled workflows are listed too — a form wired to one that is off looks connected and does nothing.
3690
4180
  **`workflows view`:** positional `<workflowId>` (the id from `workflows list`, and the one in HubSpot's own URL — the command translates it to HubSpot's separate internal flow id for you). Returns the enrolment criteria and whether it re-enrols (`null` = HubSpot does not report it for that criteria type, so it is unknown rather than off), plus every step with the property it writes. On a `LIST_BRANCH` step, branches come back in **evaluation order** (`order: 1` wins over `order: 2`) — a broad branch above a narrower one silently swallows the records the narrow one was written for, and HubSpot reports nothing. A `STATIC_BRANCH` matches on a value rather than top down, so its `order` is display only. `steps` is **not** in execution order; follow `nextActionId`. `--full` adds the raw payload, the only place the branch filter criteria (which property is compared to which value) can be read.
3691
- **`pipelines list`:** no flags; stages come back in the order they run in HubSpot.
4181
+ **`pipelines list`:** no flags; stages come back in the order they run in HubSpot, each with `isClosed` and `isWon` (closed with probability 1).
4182
+ **`companies search` flags:** `--domains <a.com,b.com>` (up to 100; a URL or `www.` is reduced to the bare domain), `--names <"Acme, Inc.;Globex">` (semicolon-separated), `--with-deals`, `--limit <1-500>` (default 100), `--output json|csv|jsonl|md`. Rows: `{ id, name, domain, linkedinPage, deals? }`; `notFound` lists the searched domains no CRM company carries; `truncated: true` means more matched than `--limit`.
4183
+ **`deals list` flags:** `--pipeline <id,id>`, `--modified-since <YYYY-MM-DD>`, `--limit <1-1000>` (default 200), `--after <cursor>`, `--output json|csv|jsonl|md`. Rows, oldest change first: `{ id, name, amount, currency, pipelineId, stageId, stageLabel, isClosed, isWon, createdAt, closedAt, modifiedAt, companyIds }` — `amount: null` is none entered. `next` is the `--after` for the following page; `restartFrom` appears when HubSpot's 10,000-result search ceiling is reached (read on with `--modified-since <restartFrom>`); `companiesReadable: false` means the connection cannot read companies, so no deal carries `companyIds`.
3692
4184
  **`contacts summary` flags:** `--days <1-365>` (default 30) over contact creation date. Returns counts only — total contacts, how many have an owner, and the lifecycle-stage split — never a name or an email. `truncated: true` means the page budget ran out, so the counts are floors.
3693
4185
  **`contacts lookup`:** positional `<email>`; returns that contact's lifecycle stage, owner, the account (company) it was filed under, and associated deals with their stage. `dealsReadable: false` / `companyReadable: false` mean the connection does not cover deals or accounts, so an empty `deals` or a null `company` is unknown rather than absent. `found: false` means no contact with that email — the lead never reached the CRM, which is a finding rather than an error.
3694
4186
  **`contacts properties`:** no flags; returns every contact property with a closed list of values — `{ name, label, groupName, options: [{ value, label }] }` — the ones a HubSpot-read source can watch. Names are HubSpot's internal ones; hand them to `baker analytics sources --create-hubspot --property <name> --value <value>` as printed. No personal data.
@@ -3720,6 +4212,137 @@ No write surface against HubSpot itself — a form's fields, redirect, or consen
3720
4212
 
3721
4213
  ---
3722
4214
 
4215
+ ### Shopify buyer audiences
4216
+
4217
+ `baker shopify audiences create --segment best|loyal|at_risk|new|one_time|all --to meta|google --account-id <id>` stages a Meta customer list audience or a Google Ads Customer Match list made of one of the store's buyer segments (`--name`, `--refresh daily|off`). At Publish Baker makes the audience and fills it with the buyers who accept marketing (email when subscribed to email, phone when subscribed to SMS), hashed; with `daily` it reads the segment again every day. The answer is counts only: `people`, `withConsent`, `willSend`. `baker shopify audiences list` shows every audience and its last update; `refresh <id>` updates one now; `remove <id>` stages stopping it and deleting it from the platform (`--keep-on-platform` only stops it).
4218
+
4219
+ ### Shopify sign-up offers
4220
+
4221
+ `baker shopify signup-offers` lists active, ended and this chat's staged offers, permissions, codes given and recent refused sign-ups. Stage an offer with `baker shopify change signup-offer set welcome-10 --name "Welcome" --percent 10 --valid-days 14 --max-codes 1000`; use `--amount` instead of `--percent` for a fixed amount, `--products` or `--collections` for eligibility, and `--minimum`, `--ends`, `--prefix` or `--combines` for its terms. `--json` / `--file` also accept the terms. `baker shopify change signup-offer end welcome-10` stops giving new codes after Publish; existing codes keep their terms. Both changes require the user's approval.
4222
+
4223
+ Forms use the `shopifyCustomer` side effect: map its `customer` fields with `baker flows map`, name an optional offer handle and show the result with `<ShopifySignupCode form="newsletter" />`. Email and SMS consent each require a separate optional, visible, initially unticked checkbox. Every lead receives at most one code for an offer; Shopify shows one discount per lead.
4224
+
4225
+ ### `baker shopify store | products | product | collections | stock | sales | buyers | buyer | countries | best-sellers | cohorts | segments | promotions | stock-holds`
4226
+
4227
+ Read-only view of the company's **connected Shopify store**, from **Baker's own copy** of its catalog and the store's own orders — never a live call to Shopify. Baker keeps the copy current from the store (a change usually shows within about a minute) and checks the whole catalog daily; product photos are already in the image library.
4228
+
4229
+ ```bash
4230
+ baker shopify store # name, currency, home country, counts, prices by country, the Products page link
4231
+ baker shopify products --search "snowboard" # what the store sells: price, sale price, stock, where it can be bought
4232
+ baker shopify products --stock out_of_stock # --stock in_stock|partly_out|out_of_stock, --collection <handle|id>
4233
+ baker shopify products --limit 10 --cursor <c> # page with meta.next_cursor (25 by default, up to 100)
4234
+ baker shopify product <id|handle|link> # one product: options with buyUrl, prices by country, stock, photos, videos
4235
+ baker shopify product <handle> --content # its whole details: description, custom fields, subscriptions, translations
4236
+ baker shopify product <handle> --field "Size chart" # one custom field, whole
4237
+ baker shopify product <handle> --quantity 2 # buy links that put 2 of the option in the cart
4238
+ baker shopify product <id> --out-of-stock # only the options that are out of stock (page with --variants-cursor)
4239
+ baker shopify collections --search "summer" # collections, with the handle --collection takes (--full: descriptions, images)
4240
+ baker shopify stock --limit 20 # every product with every option out, and every product with some out
4241
+ baker shopify sales --days 7 --by campaign # what sold and which campaigns brought it (--by day|product|campaign|landing)
4242
+ baker shopify sales --all --by weekday # when buyers order, in the store's time (--by hour|month|month_of_year)
4243
+ baker shopify buyers --days 90 --by channel # who buys: new and returning, repeat rate, segments, value per acquisition
4244
+ baker shopify buyer 1042 # one numbered customer: profile, every order, what they bought
4245
+ baker shopify countries --country DE # where buyers are: by country, a country's regions (--region for cities)
4246
+ baker shopify best-sellers --view repeat # what sells (--view products|variants|trend|together|repeat|momentum)
4247
+ baker shopify segments # best, loyal, at risk, new, one-time, with this store's own rules
4248
+ baker shopify cohorts --months 12 # buyers by first-order month: who came back, spend per buyer
4249
+ baker shopify promotions <id|handle|link> # what advertises a product: ads (via page, buy link, landing), landings, Shopping
4250
+ baker shopify stock-holds --product <id> # ads Baker paused because the product ran out: what to resume, and what to tell the user
4251
+ ```
4252
+
4253
+ **`meta.store`** is on every answer: `connection` (`connected` · `needs_reconnect` · `disconnected`), `catalog` (`starting` · `importing` · `ready` · `failed`, with `importProgress` while importing), `unreadable` (products Baker couldn't read; `unreadableCapped` past 100), `checksFailing` (Baker couldn't check the store for changes lately), `storefront` (`online_store`, or `own` for a store that sells through its own site) and `lastActivity` (when Baker last heard from the store — not a promise the copy is current). `hints[]` turn each of these into what to tell the user.
4254
+ **`store`:** `--full` adds the admin link and every country. `canChange` (`products`, `publishing`, `discounts`, `redirects`, `marketing`) says which kinds of store change the store allows Baker to stage; a `false` one is allowed by the user in Shopify. `capped: true` means a count stopped at its ceiling — `outOfStock` and `partlyOut` at 100, `collections` at 500 — so that count reads "that many or more"; a hint says which.
4255
+ **`products`:** `--search <words>`, `--collection <handle|id>`, `--stock`, `--limit <1-100>`, `--cursor`; `--full` adds vendor, type, option count, photo link, visibility and status. Every price, here and in `product` (`price`, `compareAt`, each option's price, `pricesByCountry`), is one string with its currency beside each amount: `"USD 785.95"`, `"USD 9.95 – USD 49.95"`. A row's `availability` is absent when the product is on the store, `link_only` when it is buyable only through its link, `not_for_sale` (with `status`) when it can't be bought on the online store.
4256
+ **`product`:** positional `<ref>` — the numeric id, the handle, a store product link (`…/products/<handle>`, even an all-digit handle), a Baker Products link, a Shopify admin product link (`admin.shopify.com/store/<shop>/products/<n>`), or `gid://shopify/Product/<n>`. Returns options (50 per page, `--variants-cursor` for more; 250 with `--full`), `outOptions` (up to 50, `outOptionsMore` for the rest), `pricesByCountry` when the store sells at more than one price, `photos[]` (`imageId` + a Baker-hosted `url`, never a Shopify link), `url` (the product's page in Baker) and `storeUrl` (the product's page on the store — never a buy button's link). `--full` adds the whole description, SEO, category, the admin link and each option's SKU, barcode, feed id and prices by country. `NOT_FOUND` carries a hint to search instead.
4257
+
4258
+ **Details and buy links (`product`):** every option carries `buyUrl` — its buy link for ads, emails and messages (a landing places `<ShopifyProduct product="…" />` instead, which shows the options, price and stock and hands the shopper to the store), following the store's own setting (`buyDestination`: `checkout`, `store_cart` or `product_page`) — with `buyUrlKind`, `unavailable` when it can't be bought, and `notes` for what a link can't do (`password_protected`, `subscription_only`, `subscription_optional`, `own_storefront`, `details_pending`). `--quantity <1-99>` sets how many it adds. `videos[]` lists the product's videos in the library (`videoId`, `playbackId`); `contentSummary` names what else Baker keeps. `--content` adds `content`: the formatted description, every custom field by its name (long values previewed, `truncated: true`), the rating, subscriptions, bundle, YouTube/Vimeo and 3D links, translations, and `unreadable` for content the store owner hasn't let Baker read yet. `--field <name|namespace.key>` returns one field whole. `--full` adds `--content`, all three links per option and each option's weight, unit price, shipping and tax. `store` adds `buyDestination`, `passwordProtected`, `languages`, `policies` (`--full`: their text, and the store's brand).
4259
+ **`collections`:** `--search <words>`; each row has `handle`, Shopify's product count and, on a store with an online store, `onStore`.
4260
+ **`stock`:** `--collection`, `--limit <1-200>` (default 50 per list), `--full` (SKUs and Baker links). Options that keep selling at zero or don't track stock are not out of stock.
4261
+ **`sales`:** `--days <1-395>` (default 30), `--by day|product|campaign|landing` (default `day`), `--limit <1-100>`, `--full` (each row's source, medium, campaign, click platform, landing or product id). `data.totals` (`orders`, `sales`, `added`, `refunded`, `net`) in `data.currency`; `data.rows[]` with a `label` and, by campaign or landing, a `credit` (`campaign` · `order` · `landing` · `unattributed` · `before_baker` · `other`), adding up to the totals. A sale counts when it is paid, at what was received; by day and product each fact counts when it happened, by campaign and landing each order paid in the period carries what was added and refunded on it since. `meta.salesReporting` (`meta`, `google`: `baker` · `shopify_channel` · `undecided`) says who reports the sales to the ad platforms; `meta.history` is `importing` while past orders still come in.
4262
+
4263
+ **`sales --by weekday|hour|month|month_of_year`:** orders placed in the period by when they were placed, in the store's timezone (`data.window.timezone`), from the store's whole order history — `--days` up to 3650 or `--all` (only these breakdowns take them).
4264
+
4265
+ **Buyers, countries and best sellers** (`buyers`, `buyer`, `countries`, `best-sellers`, `cohorts`, `segments`) read the store's whole order history as Baker keeps it. Every answer carries `meta.window` (local `from`/`to`, `days` or `"all"`, the store's `timezone`), `meta.history` (`state`: `importing` · `done` · `failed` · `none`; `reach`: `all` or `60_days`; the first day held) and `meta.basis` (`all_orders`, or `last_60_days` when the store lets Baker read only its last 60 days — then a buyer whose earlier orders are older counts as new). The period is orders **placed** in it, with what was refunded on them since. A buyer is a numbered customer (`Customer #1042`) with a country, region and city — never a name or an email; guest orders count in money, never as buyers. Shared flags: `--days <1-3650>` (default 365) or `--all`, `--country <ISO2>`, `--full`.
4266
+
4267
+ - **`buyers`:** `data.summary` (buyers, new and returning, guest orders, orders, net, average order, new/returning orders and net, repeat rate, median days to a second order), `data.segments`, `data.thresholds`, `data.buyers[]` (`--sort net|orders|recent|first`, `--limit`, `--offset`, `--segment best|loyal|at_risk|new|one_time`, `--channel <key>`), and with `--by campaign|channel` `data.acquisition.rows[]` (buyers whose first order is in the period, first-order net, lifetime net per buyer, average orders, repeat rate). Baker keeps no ad spend.
4268
+ - **`buyer <number>`:** `1042`, `#1042` or `Customer #1042`. A buyer erased by their store or merged into another answers `NOT_FOUND` "This buyer is no longer in Baker."
4269
+ - **`countries`:** `data.total`, `data.rows[]` (orders, buyers, new buyers, net, average order, share, growth against the period before, the currency paid in), `data.unknown` (no readable address) and `data.other` (past `--limit`), adding up to the total; `--country` for its regions, `--region` for a region's cities, `--product <id|handle>` for one product's orders and money.
4270
+ - **`best-sellers`:** `--view products` (default; units ordered beside refunded units, orders, buyers, revenue, refunds, refund rate, growth; `--sort units|revenue|orders`) · `variants` (with `--product`) · `trend` · `together` · `repeat` · `momentum`.
4271
+ - **`cohorts`:** `--months <3-36>` (default 12); retention at months 1, 2, 3, 6, 12 and net per buyer by month 12 (`--full`: every month).
4272
+ - **`segments`:** each segment's buyers, share, net, average orders, days since the last order, top countries and first products, and its rule in this store's numbers.
4273
+
4274
+ **`promotions`:** positional `<ref>` like `product`. Reads the product, scans this workspace's `src/pages/*` (import graph, ≤ 300 pages, files ≤ 256 KB) for its buy link, `/products/<handle>`, an option id or a `<ShopifyProduct>` / `<ShopifyBuyButton>` naming it (evidence `buy_button`), then `POST /api/shopify/promotions` with the linked landings. `data.ads[]` (`platform`, `level` ad|asset_group, `name`, `campaign`, `status` active|paused, `via` product_page|buy_link|landing), `data.landings[]` (`published`, `via` links_to_product|visitors_went_on), `data.shopping[]`, `data.catalog_ads`, `data.checked` (`not_checked` names a platform it could not read). A read made outside a Session says so in `hints`: only a read that carried a Session and named an ad lets “Restart ads when a product is back” later stage turning a pause from it back on, for review. `--full` adds ids, accounts, campaign / ad group / ad set ids, the Google `resource`, the URL and `data.remembered`.
4275
+
4276
+ **`stock-holds`:** `--product <ref>`, `--all` (resolved holds too). Compact: `data.resume[]` — the one command to stage per ad Baker may turn back on now (`{ ad, command }`); `data.tell_user[]` — one sentence per ad, and per piece of out-of-stock work still waiting for review, written for the marketer; `data.pages[]` — landings the out-of-stock run changed, with the `baker chats diff … --surface landings` read of those changes. With `--product` it speaks only of that product's ads, including the ones Baker did not pause when it ran out. `--full` adds the codes behind each sentence: `holds[]` with each `verdict` read live from the ad platform, `pending_sessions[]`, `recently_closed[]`, `not_paused[]`, `pages_changed[]`.
4277
+
4278
+ **Refusals:** `NO_SHOPIFY_CONNECTION` — no store is connected; `error.fix.action` is `request_connection` (`{ platform: "shopify" }`). `SHOPIFY_NOT_AVAILABLE` — Shopify can't be connected on this Baker yet; `error.fix.action` is `continue_without`. Both exit 1.
4279
+
4280
+ ---
4281
+
4282
+ ### `baker shopify change | draft | changes | undo | tracking`
4283
+
4284
+ **Changes to the connected store, staged on the Session (`BAKER_CHAT_ID`)** and made only when the user publishes it — reviewed first in the chat's Store tab. Nothing here calls Shopify to change anything mid-Session; every applied change can be put back by `undo`, through another publish.
4285
+
4286
+ ```bash
4287
+ baker shopify change product update the-snowboard --title "The Snowboard" --seo-title "…" --add-tags summer
4288
+ baker shopify change variant update 456 --sku SNOW-L --barcode 4006381333931 --weight 1.5 --weight-unit kg # kg, g, lb or oz
4289
+ baker shopify change variant update 456 --price 18.95 --sale-price none # money: the user says yes first
4290
+ baker shopify change field set product:the-snowboard custom.care --value "Hand wash"
4291
+ baker shopify change collection create --title "Summer boards" --products a,b # shown on the online store unless --hidden
4292
+ baker shopify change collection hide summer-boards # also update, show, delete, add, remove
4293
+ baker shopify change discount create --code SPRING10 --percent 10 --ends <iso> # money
4294
+ baker shopify change discount stop SPRING10 # money
4295
+ baker shopify change discount dates SPRING10 --starts <iso> --ends never # money
4296
+ baker shopify change redirect create --from /old --to /new
4297
+ baker shopify change redirect remove /old
4298
+ baker shopify change campaign add --platform google-ads --account … --campaign … # show a Baker campaign in Shopify Marketing
4299
+ baker shopify change measurement reconnect # the fix `tracking audit` names
4300
+ baker shopify change product update --json '[{"kind":"shopify.product.update","target":"<handle>","payload":{"addTags":["summer"]}}, …]'
4301
+ baker shopify draft list # also show <ref>, remove <ref>, clear
4302
+ baker shopify draft amend <ref> --patch '{…}'
4303
+ baker shopify changes --limit 20 # applied changes, each with its changeId
4304
+ baker shopify undo <changeId>… # stage putting them back, as one batch
4305
+ baker shopify tracking audit # read-only: is the store's tracking right?
4306
+ ```
4307
+
4308
+ **Money.** A price, a sale price or a discount code changes money: the first call exits 1 with `NEEDS_USER_APPROVAL` and `error.fix.askUserQuestion` — the exact question to ask with AskUserQuestion. When the user answers "Yes, make these changes", run the same command again; a declined one is refused, still carrying the question in case the user asks for it again later. `amend` of a price asks again, and publish refuses any money change whose lines are not the approved ones.
4309
+
4310
+ **Stage-time checks.** Each change is read against the live store when it is staged (the "before" the Store tab shows), checked, and refused with a reason the agent can act on — a collection whose undo could not rebuild it (rules, an image, custom fields, a discount or menu pointing at it), descriptions with scripts or frames, unknown products. Publish re-reads each target first: a change made in Shopify since staging is not overwritten; the op is marked failed with why. Up to 100 changes per call, 250 per Session.
4311
+
4312
+ **Undo.** `changes` lists what Baker applied (`changeId`, `undoable`, why not). `undo` stages the inverses in one batch, read from the store as it was before each change; a money undo asks once for all its lines. A deleted collection comes back with the same address, content and products (as a new collection).
4313
+
4314
+ **`tracking audit`:** reads only. `data.checks[]` (`measurement`, `theme_switch`, `visitor_on_orders`, `sales_reporting`, `consent`, `storefront`) each with a `status` and a `fix` that is `staged` (a command to stage), `merchant` (something the user does in Shopify) or `decision` (a choice only the user makes).
4315
+
4316
+ **Refusals:** `SHOPIFY_PERMISSION_NEEDED` — the store has not allowed this kind of change yet; `error.fix.action` is `request_connection` (`{ platform: "shopify", reason }`) and the user allows it in Shopify. `NEEDS_USER_APPROVAL` — the money answer: `fix.action` `ask_user` with the question, or `continue_without` after a no. A payload with a key its kind doesn't take (`--json`, `draft amend --patch`) is refused naming the keys it takes. All exit 1.
4317
+
4318
+ ---
4319
+
4320
+ ### `baker accounts influenced | engagement | warm | company | campaigns | crm`
4321
+
4322
+ Account insights: which companies the LinkedIn ads are warming, and which CRM deals they influenced. Reads Baker's nightly copy of LinkedIn and the CRM, so it is fast and covers up to 24 months; nothing is changed anywhere. Every company has it once a LinkedIn ad account is chosen; before the first nightly read the reports are empty — read live with `baker ads linkedin companies`. `--start`/`--end` must be real calendar days within 24 calendar months; a bad flag or an unsupported `--output` exits 1 with `INVALID_ARGS`.
4323
+
4324
+ ```bash
4325
+ baker accounts influenced # influenced pipeline, last 12 calendar months
4326
+ baker accounts influenced --basis revenue --start 2026-01-01 --end 2026-06-30
4327
+ baker accounts influenced --full # every deal in the period, influenced or not
4328
+ baker accounts engagement --last-months 3 --limit 50 # companies reached, New/Rising, CRM match and deals
4329
+ baker accounts engagement --last-months 3 --limit 50 --page 2 # the next 50 (pageInfo.total counts them all)
4330
+ baker accounts engagement --campaign-id 123456 --output csv
4331
+ baker accounts warm --output csv > warm.csv # companyName,domain,organizationUrn
4332
+ baker accounts company acme.com # or a LinkedIn company id / urn:li:organization:N
4333
+ baker accounts campaigns --last-months 6 --output md # per campaign: reached, engaged, influenced pipeline (--basis revenue)
4334
+ baker accounts influenced --model awareness --threshold 10 --output csv # another rule, this call only
4335
+ baker accounts warm --min 0 # every New/Rising company, below the threshold too
4336
+ baker accounts crm status # deals from HubSpot or sent by the client; what arrived, linked Google Sheets
4337
+ baker accounts crm sheet # create and link the deals sheet in the client's Google Drive
4338
+ baker accounts crm check --file deals.csv # judge a deals or company CSV offline, nothing sent
4339
+ baker accounts crm contract # the send-your-CRM contract, as Markdown for a developer
4340
+ ```
4341
+
4342
+ A deal is **influenced** when its company reached the company's influence rule — by default 3+ LinkedIn ad engagements (Awareness: 7+ impressions; Intent: 3+ clicks) — in the 12 months up to and including the month the deal was created (`--basis pipeline`, deals not won) or closed won (`--basis revenue`). It is a labelled rule, not credit: campaign rows overlap, and money is reported per currency, never summed across currencies. Deals whose company match is only a guess come back under `lowConfidence` and stay out of `summary` until the match is confirmed in Measure → Accounts. Without the deals set up there — HubSpot's pipelines and won/lost stages, or Another CRM — `influenced` and `campaigns` exit 1 with `ACCOUNTS_NOT_SET_UP`. A CRM Baker cannot read sends its companies and deals to `POST /v1/accounts/crm` (a `crm_ingest` key, `X-Baker-Mode: test` to rehearse, up to 500 records per request, records naming a person refused), is uploaded as a CSV in Measure → Accounts, or lives in a Google Sheet linked there that Baker reads every night (a row deleted from the sheet leaves Baker). A file is a deals export or a company list — a company needs only its website or LinkedIn page; `crm check` runs the same rules offline and `crm contract` prints the whole contract. The CLI never sends deals or links a sheet of the client's; `crm sheet` creates the empty deals sheet in their Google Drive and links it, applied at once (asking again returns the same sheet). `--model engagement|awareness|intent` and `--threshold N` on `influenced`, `campaigns` and `warm` read with another rule for that call only; the saved setting is unchanged. A **warm** account is New or Rising, reached the company's influence threshold this period on the rule's metric (`--min N` overrides, `--min 0` drops it), and has no open deal, no won deal and no deal lost in the last 6 months; `engagement` and `warm` rows carry `dealStatus` (`won` > `open` > `lost` > `none`), `engagement --output csv` adds `lastMonth` and `previousImpressions/Clicks/Engagements`, and `campaigns` / `company` carry each campaign's `campaignName` (null when LinkedIn could not name it). `company` returns `months` (the company's totals), `byCampaign` (the same numbers per campaign — overlapping, never add them) and its `deals` with `touchValue` and `influenced`. `warm --output csv` is the file `baker ads linkedin audiences create --type company-list --list-file` takes.
4343
+
4344
+ ---
4345
+
3723
4346
  ### `baker chats list | view | transcript | diff`
3724
4347
 
3725
4348
  Read-only inspection of **other Sessions (chats) on the same account** — so an agent can reuse prior work ("like we did in that other chat, but for X"). Same-account only; never crosses a company boundary and has no write surface.
@@ -3788,7 +4411,7 @@ Starting a batch returns 202 + a `generationId`; the CLI polls until the takes s
3788
4411
  ```bash
3789
4412
  baker studio generate "a friendly golden retriever in a bright modern living room" --aspect-ratio 16:9
3790
4413
  baker studio generate "hero shot of a matte black water bottle on wet marble, studio light" \
3791
- --model google/gemini-3-pro-image-preview --image-size 2K --aspect-ratio 4:5 --count 3
4414
+ --model google/gemini-3-pro-image --image-size 2K --aspect-ratio 4:5 --count 3
3792
4415
  baker studio generate "home office hero, warm minimalist, natural light, 35mm photo" \
3793
4416
  --reference "https://i.pinimg.com/…/pin1.jpg,./src/brand/logos/product.png,j57abc123def456ghi789"
3794
4417
  baker studio generate "founder-style selfie, kitchen background" --skill ugc-selfie-hook
@@ -3796,13 +4419,13 @@ baker studio generate "flat geometric mascot, brand palette" \
3796
4419
  --model recraft/recraft-v4.1-pro-vector --rgb-colors "[[10,10,10],[255,80,0]]" --bg-rgb "[255,255,255]"
3797
4420
  ```
3798
4421
 
3799
- **Models** (`--model`, default `google/gemini-3.1-flash-image-preview`):
4422
+ **Models** (`--model`, default `google/gemini-3.1-flash-image`):
3800
4423
 
3801
4424
  | Model | Best for | Aspect ratios | Sizes |
3802
4425
  |---|---|---|---|
3803
- | `google/gemini-3.1-flash-image-preview` **(default)** | Fast iteration, reference editing, extreme aspect ratios (Nano Banana flash) | standard **+** `1:4` `4:1` `1:8` `8:1` | `0.5K`–`4K` |
4426
+ | `google/gemini-3.1-flash-image` **(default)** | Fast iteration, reference editing, extreme aspect ratios (Nano Banana flash) | standard **+** `1:4` `4:1` `1:8` `8:1` | `0.5K`–`4K` |
3804
4427
  | `google/gemini-3.1-flash-lite-image` | Cheapest tier (Nano Banana 2 Lite) — half the flash price per output token. High-volume variants and any 1K-sufficient frame, portrait ad formats included | standard **+** `1:4` `4:1` `1:8` `8:1` | `1K` **only** |
3805
- | `google/gemini-3-pro-image-preview` | Highest fidelity (Nano Banana Pro) — strong prompt adherence and reference grounding | standard set | `1K` `2K` `4K` |
4428
+ | `google/gemini-3-pro-image` | Highest fidelity (Nano Banana Pro) — strong prompt adherence and reference grounding | standard set | `1K` `2K` `4K` |
3806
4429
  | `openai/gpt-image-2` | Photoreal + cleanest in-image text — ad/landing reproduction | standard set **minus** `4:5` `5:4` | *(none — derived from the ratio)* |
3807
4430
  | `openai/gpt-5.4-image-2` | **Legacy** — accepted so canvases authored before the gpt-image-2 switch still re-run. Don't pick it for new work. | standard set | `1K` `2K` `4K` (ignored upstream) |
3808
4431
  | `recraft/recraft-v4.1-pro-vector` | Vector/flat/SVG-style with palette control. Writes **SVG**, so its output cannot be fed back as a reference | `1:1` `4:3` `3:4` `16:9` `9:16` **only** | *(none — refuses `image_size`)* |
@@ -3811,7 +4434,7 @@ Standard aspect ratios: `1:1` `2:3` `3:2` `3:4` `4:3` `4:5` `5:4` `9:16` `16:9`
3811
4434
 
3812
4435
  `openai/gpt-image-2` accepts neither `image_size` nor the `4:5` / `5:4` portrait ratios — OpenAI picks the pixel dimensions from the ratio. To ship a 4:5 Meta feed ad from it, render the hero at `3:4` and adapt with `image_aspect_adapt`; the scaffolders do this for you.
3813
4436
 
3814
- `google/gemini-3.1-flash-lite-image` renders at `1K` and nothing else — `2K` / `4K` is a provider 400 (`resolution: not supported. Accepted: 1K`), not a soft clamp. It costs half of `google/gemini-3.1-flash-image-preview` per output image token ($0.00003 vs $0.00006), so it is the cheap-iteration tier for images. Keep hero stills and reference sheets that need `2K`+ on the flash or pro tier.
4437
+ `google/gemini-3.1-flash-lite-image` renders at `1K` and nothing else — `2K` / `4K` is a provider 400 (`resolution: not supported. Accepted: 1K`), not a soft clamp. It costs half of `google/gemini-3.1-flash-image` per output image token ($0.00003 vs $0.00006), so it is the cheap-iteration tier for images. Keep hero stills and reference sheets that need `2K`+ on the flash or pro tier.
3815
4438
 
3816
4439
  `recraft/recraft-v4.1-pro-vector` is the narrowest of the roster in three ways, all read off its live endpoint schema: it renders **five** ratios (no `4:5` / `5:4` / `2:3` / `3:2` / `21:9`, so a portrait ad shape snaps to `3:4`), it has **no `image_size`** — the provider accepts one and ignores it, which is why it is refused here — and it takes **exactly one** reference image against 14-16 elsewhere. Its output is an SVG, and the image models read only JPG, PNG and WebP, so a Recraft take cannot be used as a reference for the next render.
3817
4440
 
@@ -3821,7 +4444,7 @@ A knob the chosen model does not advertise is **refused, not silently dropped**
3821
4444
 
3822
4445
  | Flag | Description |
3823
4446
  |---|---|
3824
- | `--model` | Model id (default `google/gemini-3.1-flash-image-preview`) |
4447
+ | `--model` | Model id (default `google/gemini-3.1-flash-image`) |
3825
4448
  | `--aspect-ratio` | Output aspect ratio (default `1:1`) |
3826
4449
  | `--count` | Takes to render, 1–8 (default 1) |
3827
4450
  | `--reference` | Comma-separated visual references, in order: **library image ids**, **local file paths**, and/or **image URLs**. Paths and URLs are added to the media library first and passed as ids — the studio takes library ids only, so the client can see what grounded the render and the reference stays reusable. The biggest quality lever there is. |
@@ -3885,7 +4508,7 @@ baker studio animate "she keeps walking, camera stays with her, then she stops a
3885
4508
  - **480p / 720p only** — 1080p and 4K are real Seedance tiers but not on this route.
3886
4509
  - **Sound costs nothing extra** here, unlike every other model that offers it.
3887
4510
  - **No watermark** unless `--watermark` asks for one; it cannot be removed after the render.
3888
- - **No people from an uploaded picture.** A privacy pre-check refuses any recognisable face in the input image — invented characters included, and images from ByteDance's own image model included. Clips that hit it are re-rendered automatically on Gemini Omni (reference mode) or Veo (frame mode).
4511
+ - **No people from an uploaded picture.** A privacy pre-check refuses any recognisable face in the input image — invented characters included, and images from ByteDance's own image model included. A reference-mode clip that hits it is re-rendered automatically on the same model through a second route that accepts faces (about twice the price); a frame-mode clip is reported as refused and never moved to another model — re-run it on Veo.
3889
4512
 
3890
4513
  | Flag | Description |
3891
4514
  |---|---|
@@ -3929,16 +4552,20 @@ Compact output per take is `{ slot, status, imageId | videoId, url, inLibrary, e
3929
4552
  | `lifestyle-scene` | `openai/gpt-image-2` | `4:5` | 3 | **yes** |
3930
4553
  | `cinematic-still` | `google/gemini-3-pro-image-preview` | `16:9` | 3 | no |
3931
4554
  | `brand-icon` | `recraft/recraft-v4.1-pro-vector` | `1:1` | 4 | no |
3932
- | `background-plate` | `google/gemini-3.1-flash-image-preview` | `4:5` | 3 | no |
4555
+ | `background-plate` | `google/gemini-3.1-flash-image` | `4:5` | 3 | no |
3933
4556
 
3934
4557
  A skill fills only what you did **not** pin — `--model`, `--aspect-ratio` and `--count` always win. Which means a skill can change the model, and the model decides what the rest of the flags may be: `--skill brand-icon` lands on Recraft, so it takes one reference image rather than fourteen, refuses `--image-size`, and writes SVG that cannot be fed back in as a reference. The CLI resolves the skill's model before checking the reference count, so an over-long `--reference` list is refused before anything is uploaded.
3935
4558
 
3936
4559
  ## Creative Canvas
3937
4560
 
4561
+ Static-ad guidance prefers a ready company Avatar whenever an actor is needed, chosen for the buyer context. A real founder or testimonial author uses their verified likeness; if it cannot be found, use initials or no face. The shared creative instructions also call for deliberate complete-logo selection, one consistent primary CTA treatment, and deterministic rendering of recognizable interfaces. Screen guidance distinguishes laptops and notebook computers from paper notebooks beside a phone.
4562
+
3938
4563
  The creative canvas runs declarative JSON pipelines. You author a graph of nodes (text, image, video, audio, composition, control-flow), `baker canvas run` it, and every node's output drops to disk under `canvas/<run_id>/` so you can watch progress and consume the artifacts.
3939
4564
 
3940
4565
  Auth: same `BAKER_API_KEY` + `BAKER_API_URL` as the rest of the CLI.
3941
4566
 
4567
+ Static resizes, fixes and series continuations keep why the original ad works and the elements its concept depends on, alongside the previous image. Older specs carry their persuasion mechanism and its reason instead. Automatic spec repairs preserve the concept, mechanism, message, brand (including the CTA color) and native interface; they can correct the execution without changing the idea. Change those choices explicitly in `prompt.json` when the brief changes.
4568
+
3942
4569
  ### Quick start
3943
4570
 
3944
4571
  ```bash
@@ -3948,9 +4575,9 @@ baker canvas validate my-canvas.json
3948
4575
  # legal config to review before billing — a frame that describes a recurring element
3949
4576
  # (a pet/product) but doesn't wire its reference (VIDEO_REFERENCE_MISSING), or a scene
3950
4577
  # span that exceeds the assigned model's max clip duration (VIDEO_SPAN_EXCEEDS_MODEL),
3951
- # or the same ad rendered once per aspect ratio instead of one hero + a terminal
3952
- # image_aspect_adapt (IMAGE_PARALLEL_FORMAT_RENDERS — separate renders drift in type,
3953
- # crop, and color, so the sizes stop matching).
4578
+ # or the same ad rendered once per aspect ratio instead of one hero + cuts or a
4579
+ # terminal image_aspect_adapt (IMAGE_PARALLEL_FORMAT_RENDERS — separate renders drift
4580
+ # in type, crop, and color, so the sizes stop matching).
3954
4581
  # A generator prompt is measured with its {{slots}} filled from text nodes and local
3955
4582
  # prompt.json/text ingests: past the model's character cap it is refused as
3956
4583
  # PROMPT_OVER_MODEL_LIMIT, naming the node and the overshoot, before anything bills.
@@ -3962,6 +4589,8 @@ baker canvas run my-canvas.json
3962
4589
  # Advisory warnings print as `[warn] <CODE>: …` before anything bills.
3963
4590
  # Only nodes reachable from the declared output execute (orphaned nodes are skipped,
3964
4591
  # not billed). Add --keep-runs N to prune old r_* run dirs, keeping the N newest.
4592
+ # The JSON ends with `verdict` (static ads), `run_id` and `files` (each delivered size's
4593
+ # local file by its label), so `| tail` keeps them.
3965
4594
  # A failed run reports `error.code: "input_invalid"` when the backend refused a node's
3966
4595
  # own input (it fails the same way until the input changes), `"runtime"` otherwise.
3967
4596
 
@@ -4025,7 +4654,7 @@ baker canvas set-prompt my-canvas.json s0_start --text-file ./hook-frame.txt
4025
4654
  `baker canvas scaffold-ad` renders everything with a known, fixed appearance locally,
4026
4655
  because a video model garbles a wordmark every time:
4027
4656
 
4028
- - **`brand.logo`** — the repo path to the mark. It is copied into the overlay composition
4657
+ - **`brand.logo`** — the repo path to the mark. It is copied into the caption composition
4029
4658
  and drawn as a real `<img>`: small top-left for the whole ad, large on the closing card.
4030
4659
  - **`end_card`** — on by default whenever the spec carries a brand. The last beat's PICTURE
4031
4660
  becomes a flat brand colour plate (rendered by ffmpeg, not generated), with the mark and
@@ -4039,15 +4668,41 @@ because a video model garbles a wordmark every time:
4039
4668
  order, page-furniture whites and blacks dropped) and `src/brand/logos/` for the mark
4040
4669
  (vector first, plain mark over a mono/white/icon variant). Pass them yourself only when
4041
4670
  the ad needs something other than the client's default.
4042
- - **The spoken language is read off the script** when the spec does not declare it, so a
4043
- Spanish ad is cast from Spanish voices without anyone having to say so.
4671
+ - **The spoken language is read off the script** when neither the spec (`voice.language`, a
4672
+ BCP-47 tag) nor the creative's `_definition.md` (a translation's `language`) declares it, in
4673
+ any language, so a Spanish ad is cast from Spanish voices without anyone having to say so.
4674
+ The same read, in any language and in a line that mixes two, finds a line where the speaker
4675
+ introduces themselves with nobody on camera, a shot brief that asks for readable text (both
4676
+ refused), a cast who works for the business, each voice description's gender, and the words
4677
+ a caption card may not end on. It is a fraction of a cent, kept until the spec changes and
4678
+ billed with the next run; when it cannot be read, nothing is refused and the hints say so.
4679
+ - **`market` is where the ad is set**, stated on every frame. Omitted, it is the country of a
4680
+ regional language (`es-MX` → Mexico), the voice's or the translation's. A language alone
4681
+ names no country (Spanish is not Spain): with neither, the frames name no place and a hint
4682
+ asks for one.
4044
4683
  - **`sound`** (per beat) — one sound effect for that shot, rendered by ElevenLabs
4045
4684
  alongside the music bed. Most beats should not have one: it belongs where the picture
4046
4685
  shows a specific physical event (a drill, a door, rain on glass), not on a talking head
4047
4686
  or a calm lifestyle shot. A bed plus two well-placed effects beats eight.
4048
- - **A real brand mark is required.** `scaffold-ad` refuses to build without one in
4049
- `src/brand/logos/` (SVG or PNG): the mark is drawn on every frame and on the closing
4050
- card, so a stand-in ships an ad wearing the wrong brand.
4687
+ - **The logo is optional.** With no mark in `src/brand/logos/` (and none confirmed for the
4688
+ company's own domain), the ad is built without the corner mark, and one hint says so and
4689
+ how to add one: put the client's logo file in `src/brand/logos/` and re-run. Meta shows
4690
+ the page name and picture above every ad either way. The closing card still carries the
4691
+ call to action when the spec has a `cta`; with neither a mark nor a `cta`, the last beat
4692
+ keeps its footage. A typed stand-in (an SVG wordmark in live `<text>`) is still refused,
4693
+ and `--no-brand` still means no brand layer at all: no mark and no closing card.
4694
+ - **Captions and the mark sit inside the safe area** (`videoSafeArea` in
4695
+ `src/engine/scaffold/platform-safe-area.ts`, the one owner of those margins). On 9:16 the
4696
+ caption cards end above the Reels interface at the bottom and the mark starts below the
4697
+ Stories and Reels header; 4:5 and 1:1 use modest feed margins. The closing card is centred
4698
+ in the same area. Every composition is sized to the spec's
4699
+ `format.aspect_ratio`, so a 4:5 ad lays its captions out on a 4:5 frame.
4700
+ - **Captions and the closing button are set in the brand's face**: a font file in
4701
+ `src/brand/fonts/`, else the typeface `tokens.json` or `BRAND.md` names, loaded as a web
4702
+ font. With neither, they keep the default heavy sans.
4703
+ - **A spoken ad's captions print the script.** The voice track is transcribed only to time
4704
+ the words: each caption word is the `say` line as written, at the moment it was said, so
4705
+ a word the recogniser mishears never reaches the screen.
4051
4706
  - **An avatar with no pinned voice cannot speak on camera.** Their lines would be read by
4052
4707
  a separately cast voice, which is dubbing and looks like it.
4053
4708
  - **`voiceover: false`** — a music-led ad. No voice, no transcription: the `say` lines
@@ -4058,10 +4713,17 @@ because a video model garbles a wordmark every time:
4058
4713
  every beat they appear in on that avatar's identity sheet and copies its subject
4059
4714
  description verbatim, so the ad shows the same face as the rest of the company's work.
4060
4715
  This is the whole journey: `baker avatars create` → `cast.avatar` in the spec → render.
4061
- - **`cast.description`** — a person described in words, when there is no avatar.
4716
+ - **`cast.description`** — a person described in words, when there is no avatar: a candid portrait is generated from it and every beat grounds on that one invented person.
4062
4717
 
4063
4718
  Each of these also removes a generation: the closing beat costs no image and no clip.
4064
4719
 
4720
+ **The spec is kept, and the canvas is checked.** `scaffold-ad` writes the spec it built
4721
+ from beside the canvas as `ad.spec.json` (with the cast it resolved), so a later change
4722
+ edits that file and re-runs with `--spec` on it and the same `--slug`. With `--slug` it
4723
+ also writes the creative's `_definition.md` and syncs its workflow graph, as
4724
+ `scaffold-video` does. The canvas is validated before the command reports success: a
4725
+ blocking issue is recorded in the canvas and the command exits `2` with the issues.
4726
+
4065
4727
  #### The finished video is reviewed
4066
4728
 
4067
4729
  When a run's output is a video, `baker canvas run` samples twelve frames across it and
@@ -4121,7 +4783,7 @@ A re-run with no changes hits the cache for every node — total runtime drops t
4121
4783
  "type": "text_generate",
4122
4784
  "inputs": { "topic": "$ref:topic.text" },
4123
4785
  "params": {
4124
- "model": "google/gemini-3.1-flash-image-preview",
4786
+ "model": "google/gemini-3.1-flash-image",
4125
4787
  "prompt": "Write a 6-word tagline for {{topic}}."
4126
4788
  }
4127
4789
  }
@@ -4152,6 +4814,8 @@ The top-level `output` declares the canvas's final result — the engine returns
4152
4814
 
4153
4815
  Every node shares the envelope `{ id, type, inputs?, params? }`. `inputs` wires other nodes' output slots in; `params` configures the node. The validator enforces required input kinds and per-model param shape **before** anything is billed.
4154
4816
 
4817
+ A node may also carry `tags` (`{ scene?, role?, region?, on_camera? }`) stating what it is in the ad (scene 3, region 1, the presenter's clip, the spine). `scaffold-video` stamps them and `canvas validate` reads them in place of id conventions like `s3_r1_clip`. They are descriptive only: tags never enter the cache key, so adding or editing them never re-renders or re-bills a node. Canvases without tags validate as they always did.
4818
+
4155
4819
  `baker canvas catalog` prints a one-line index of every node; `baker canvas catalog --node <id>` prints that node's always-current machine-readable schema (params, inputs, outputs, every model), and `--full` prints every node's. The tables below mirror the catalog at time of write.
4156
4820
 
4157
4821
  In every table:
@@ -4206,6 +4870,44 @@ Local ffmpeg passthrough. Write the argv you'd type, declare outputs, the engine
4206
4870
 
4207
4871
  ---
4208
4872
 
4873
+ ##### `video_concat`
4874
+
4875
+ Join clips and stills into one continuous video: the spine of a video ad. Every input is normalized onto one raster (size, frame rate, square pixels, `yuv420p`), cut or held to its timeline length, and joined with hard cuts or `xfade` transitions. Local ffmpeg, free. Because the length and size live in params, `canvas validate` sums the spine against the timeline (`VIDEO_TIMELINE_TOTAL_MISMATCH`) before anything renders.
4876
+
4877
+ **Inputs:** one video or image per clip, wired as `c0`, `c1`, … in timeline order (deep validation flags a missing or extra slot).
4878
+
4879
+ **Params:**
4880
+
4881
+ | Name | Type | Required | Notes |
4882
+ |---|---|---|---|
4883
+ | `width`, `height` | integer | yes | output raster; must be even (libx264 `yuv420p` rejects odd sizes) |
4884
+ | `fps` | integer | no | default `30` |
4885
+ | `seam_dedup` | `head` \| `tail` \| `off` | no | default `head`: which copy of a repeated splice frame survives on a `continues_frame` clip; the timeline length never changes |
4886
+ | `clips` | array | yes | per clip: `duration_s` (its timeline length), `length` (`source` default: play as rendered; `trim`: cut out of a longer input from `offset_s`; `still`: hold an image), `fit` (`fill` default crops, `pad` letterboxes a screenshot), `transition` (`{ type, duration_s }`, the xfade out of this clip; the clip carries that much extra footage), `continues_frame` |
4887
+
4888
+ The joined length is the sum of the clips' `duration_s` (a `source` clip counts what is wired into it). **Outputs:** `video` (mp4).
4889
+
4890
+ ---
4891
+
4892
+ ##### `av_mux`
4893
+
4894
+ Put the soundtrack under a finished picture: the last step of a video ad. The video is stream-copied, the audio loudness-normalized, upmixed to stereo and encoded to AAC. Local ffmpeg, free.
4895
+
4896
+ **Inputs:** `video`, `audio`.
4897
+
4898
+ **Params:**
4899
+
4900
+ | Name | Type | Required | Notes |
4901
+ |---|---|---|---|
4902
+ | `loudnorm` | `{ i, tp, lra }` \| `false` | no | default `{ i: -14, tp: -1.5, lra: 11 }` (the social target); `false` keeps the mix level |
4903
+ | `audio_bitrate` | string | no | default `"192k"` |
4904
+ | `sample_rate` | integer | no | default `48000` |
4905
+ | `length` | `"shortest"` \| `{ total_s }` | no | default `shortest`: end with the shorter stream; `{ total_s }` cuts both at that second |
4906
+
4907
+ **Outputs:** `video` (mp4).
4908
+
4909
+ ---
4910
+
4209
4911
  ##### `imagemagick`
4210
4912
 
4211
4913
  Local ImageMagick passthrough. Identical schema and safety contract to `ffmpeg`; uses `magick` (v7+) or `convert` (v6) on PATH.
@@ -4266,6 +4968,14 @@ Open record keyed by the composition's `meta.json` inputs.
4266
4968
 
4267
4969
  ---
4268
4970
 
4971
+ ##### `static_prefilter`
4972
+
4973
+ Local; no credits (one box-reader call per variant, about $0.005, which rides on the run record's review spend). Ranks a static ad's variants before the judge by the deterministic checks (blocking defects, then every line of the ad spec's copy read back with its box, the brand's logo found, the button's words, every word and the logo inside the master's band) and passes on only the best-ranked, so the judge only breaks an exact tie. A variant the reader could not read ties with the best. **Inputs:** `images` (the variants' whole list), `spec` (the ad spec, `$ref:prompt.asset`). **Params:** `formats` (the delivered sizes). **Outputs:** `images` (the best-ranked variants, in an order seeded by the pictures rather than the render order; never empty: when every variant fails, the one with the fewest defects passes as `flagged`), `order` (which variant each of `images` is), `dropped` (each variant set aside, with its index and reasons), `flagged` (the variant kept when all failed), `image` (the chosen picture: when a variant rendered at another shape, a 3:4 for a 4:5, would lose a word or the button to a cut about its centre, it comes cut to the master's frame where every word stays whole, else as rendered) and `usage` (the reader's spend). Scaffolded by `scaffold-static-ad` between the variants and the judge.
4974
+
4975
+ ##### `static_safe_fit`
4976
+
4977
+ Local; no credits (one box-reader call, about $0.005, which rides on the run record's review spend). Brings a finished 9:16's words and logo inside the band Reels or Stories leave clear without rendering again: reads where every line and the logo sit and, when any reaches past the band, shrinks the whole picture a little (to 85% at most) and shifts it, carrying its edge into the uncovered margin, only when that edge is plain ground; where the review blocks a line under Reels' or TikTok's buttons down the right (an ad set that runs there and not in Stories), the fit clears them too (a busy edge would read as a blurred duplicate, so the picture is passed on as rendered). The image model cannot move an element by a few points on request, so this replaces re-rendering for small misses. **Inputs:** `image`. **Params:** `format`, `placements` (the ad set's, as the sizes were planned). **Outputs:** `image` (same pixel size), `fit` (`{ scale, x, y }`, or null when passed on unchanged), `reason` (`not_read`, `inside_band`, `too_far` when clearing the band needs more than the 85% shrink, or `busy_edge` when the edge it would carry is not plain ground) and `usage`. Scaffolded by `scaffold-static-ad` after every 9:16 Reels or Stories covers (`safe_<size>`, tagged `static_safe_fit`). It never sets the picture over a blurred or mirrored copy of itself: a layout `too_far` or `busy_edge` is passed on as it is, the run review names what Reels or Stories covers, and `baker canvas run` lays that size out again by itself.
4978
+
4269
4979
  ##### `font_specimen`
4270
4980
 
4271
4981
  Render specimen text in a given font file to a **PNG** — black `#000` text on a white `#fff` background — via headless Chromium at 2× device-scale. Produces a **typeface reference image**: wire `image` into `image_generate`'s `reference` input so the model replicates the font when generating images that contain text.
@@ -4303,7 +5013,7 @@ Example pipeline:
4303
5013
  { "id": "font", "type": "ingest", "params": { "source": "url", "url": "https://cdn.example.com/brand.woff2", "expect": "font" } }
4304
5014
  { "id": "specimen", "type": "font_specimen", "inputs": { "font": "$ref:font.asset" }, "params": {} }
4305
5015
  { "id": "gen", "type": "image_generate", "inputs": { "reference": "$ref:specimen.image" },
4306
- "params": { "model": "google/gemini-3.1-flash-image-preview", "prompt": "Launch poster headlined 'SALE ENDS FRIDAY' using the exact typeface from the reference image" } }
5016
+ "params": { "model": "google/gemini-3.1-flash-image", "prompt": "Launch poster headlined 'SALE ENDS FRIDAY' using the exact typeface from the reference image" } }
4307
5017
  ```
4308
5018
 
4309
5019
  ---
@@ -4361,8 +5071,8 @@ None.
4361
5071
  | `model` | enum | yes | one of `~google/gemini-flash-latest`, `~google/gemini-pro-latest` |
4362
5072
  | `prompt` | string | yes | non-empty |
4363
5073
  | `system` | string | no | system prompt |
4364
- | `response_format` | enum | no | `text` (default) or `json_object`. Set `json_object` when a downstream `{{slot}}` consumes the output as JSON (e.g. the ad-blueprint transform) so the model returns clean JSON with no markdown fences or prose. |
4365
- | `web_search` | boolean | no | When `true`, routes through OpenRouter's `:online` web plugin so the model searches the live web before answering. Use it on the ad-blueprint transform so it adapts copy to the target brand's real facts (current pricing, the trust signals it actually has) instead of guessing. Adds web-search cost. |
5074
+ | `response_format` | enum | no | `text` (default) or `json_object`. Set `json_object` when a downstream `{{slot}}` consumes the output as JSON (e.g. a hand-built transform step) so the model returns clean JSON with no markdown fences or prose. |
5075
+ | `web_search` | boolean | no | When `true`, routes through OpenRouter's `:online` web plugin so the model searches the live web before answering. Use it on a text step that must adapt copy to the target brand's real facts (current pricing, the trust signals it actually has) instead of guessing. Adds web-search cost. |
4366
5076
  | `temperature` | number | no | 0–2 |
4367
5077
  | `max_tokens` | number | no | positive int |
4368
5078
 
@@ -4400,7 +5110,7 @@ Generate images via OpenRouter. The Zod schema is a permissive union; per-model
4400
5110
  | `reference` | image **or** image[] | no | `image/png`, `image/jpeg`, `image/webp`, `image/gif` |
4401
5111
 
4402
5112
  `reference` accepts a single image or an **array** of images. Wire several to combine references in one
4403
- generation — e.g. a subject reference sheet + a font specimen + the original ad. Each image is
5113
+ generation — e.g. a subject reference sheet + a product shot + a font specimen (never someone else's ad: the model copies what it is shown). Each image is
4404
5114
  forwarded to the model as a separate input, in array order (the provider accepts multiple). Wire an
4405
5115
  array as a literal list: `"reference": ["$ref:subject.sheet", "$ref:type.image"]`.
4406
5116
 
@@ -4410,7 +5120,9 @@ array as a literal list: `"reference": ["$ref:subject.sheet", "$ref:type.image"]
4410
5120
  |---|---|---|
4411
5121
  | `images` | image[] | `image/png \| image/jpeg \| image/webp` (provider-determined) |
4412
5122
 
4413
- **Cost** — 5 credits estimate per call (backend charges actual OpenRouter usage).
5123
+ **Cost** — 5 credits estimate per image asked for (`n`, default 1), 20 on Gemini 3 Pro Image (as `image_aspect_adapt` quotes it) and 20 at quality `max`; the backend charges the call's actual OpenRouter usage.
5124
+
5125
+ **Several candidates from one call** — `n` (integer, default 1) asks one call for several images, returned together in `images`. Each image is billed as if it were its own call (`n: 3` costs three images); what it saves is requests and waiting. Only models that declare it take it (today `openai/gpt-image-2.5-sunburst`, up to 10); any other model refuses it before anything is billed. Wire the whole array into `image_select` to keep the best.
4414
5126
 
4415
5127
  > `seed` is not supported — OpenRouter's `image_config` schema has no seed slot for image-gen.
4416
5128
 
@@ -4440,15 +5152,15 @@ No `image_size`: OpenAI derives the pixel dimensions from the ratio, so a size i
4440
5152
 
4441
5153
  ###### Model: `openai/gpt-5.4-image-2` (legacy)
4442
5154
 
4443
- Kept only so canvases authored before the gpt-image-2 switch still validate and re-run unchanged — `baker canvas rerun` replays a recorded canvas verbatim. It keeps the full standard ratio set (including `4:5` / `5:4`) and still accepts `image_size`, which the provider ignores. Prefer `openai/gpt-image-2` for anything new.
5155
+ Kept only so canvases authored before the gpt-image-2 switch still validate and re-run unchanged — `baker canvas run --slug` replays a recorded canvas verbatim. It keeps the full standard ratio set (including `4:5` / `5:4`) and still accepts `image_size`, which the provider ignores. Prefer `openai/gpt-image-2` for anything new.
4444
5156
 
4445
- ###### Model: `google/gemini-3.1-flash-image-preview`
5157
+ ###### Model: `google/gemini-3.1-flash-image` (legacy id `google/gemini-3.1-flash-image-preview` still accepted)
4446
5158
 
4447
5159
  Gemini flash image model — the default. Fast iteration, reference editing, extreme aspect ratios, and the `0.5K` size for cheap previews.
4448
5160
 
4449
5161
  | Name | Type | Required | Notes |
4450
5162
  |---|---|---|---|
4451
- | `model` | literal | yes | `"google/gemini-3.1-flash-image-preview"` |
5163
+ | `model` | literal | yes | `"google/gemini-3.1-flash-image"` |
4452
5164
  | `prompt` | string | yes | non-empty |
4453
5165
  | `aspect_ratio` | enum | no | EXTREME AR |
4454
5166
  | `image_size` | enum | no | `0.5K \| 1K \| 2K \| 4K` |
@@ -4464,20 +5176,20 @@ Gemini flash image model — the default. Fast iteration, reference editing, ext
4464
5176
  | `aspect_ratio` | enum | no | EXTREME AR — keeps `4:5` / `5:4`, so Meta/Instagram portrait formats come straight out of it |
4465
5177
  | `image_size` | enum | no | `1K` only — `2K` / `4K` is a hard provider 400 (`resolution: not supported. Accepted: 1K`), so `validate` rejects it before the call costs anything |
4466
5178
 
4467
- Reach for it for high-volume variants and any frame `1K` covers. Send hero stills and the `image_reference_sheet` subject sheet — anything that needs `2K`+ — to `google/gemini-3.1-flash-image-preview` or `google/gemini-3-pro-image-preview`. Registered for `image_generate` only; `image_reference_sheet` and `image_aspect_adapt` do not accept it.
5179
+ Reach for it for high-volume variants and any frame `1K` covers. Send hero stills and the `image_reference_sheet` subject sheet — anything that needs `2K`+ — to `google/gemini-3.1-flash-image` or `google/gemini-3-pro-image`. Registered for `image_generate` only; `image_reference_sheet` and `image_aspect_adapt` do not accept it.
4468
5180
 
4469
5181
  ```json
4470
5182
  { "id": "variant_a", "type": "image_generate",
4471
5183
  "params": { "model": "google/gemini-3.1-flash-lite-image", "prompt": "Portrait feed ad for a citrus energy drink …", "aspect_ratio": "4:5", "image_size": "1K" } }
4472
5184
  ```
4473
5185
 
4474
- ###### Model: `google/gemini-3-pro-image-preview`
5186
+ ###### Model: `google/gemini-3-pro-image` (legacy id `google/gemini-3-pro-image-preview` still accepted)
4475
5187
 
4476
5188
  "Nano Banana Pro" — heavier, higher-quality Gemini.
4477
5189
 
4478
5190
  | Name | Type | Required | Notes |
4479
5191
  |---|---|---|---|
4480
- | `model` | literal | yes | `"google/gemini-3-pro-image-preview"` |
5192
+ | `model` | literal | yes | `"google/gemini-3-pro-image"` |
4481
5193
  | `prompt` | string | yes | non-empty |
4482
5194
  | `aspect_ratio` | enum | no | STD AR |
4483
5195
  | `image_size` | enum | no | `1K \| 2K \| 4K` |
@@ -4521,7 +5233,7 @@ The sheet prompt is baked into the backend — there is no `prompt` param. `subj
4521
5233
 
4522
5234
  | Name | Type | Required | Notes |
4523
5235
  |---|---|---|---|
4524
- | `model` | enum | yes | one of `google/gemini-3-pro-image-preview`, `google/gemini-3.1-flash-image-preview` |
5236
+ | `model` | enum | yes | one of `google/gemini-3-pro-image`, `google/gemini-3.1-flash-image` (their `-preview` ids still accepted) |
4525
5237
  | `subject_description` | string | yes | exact subject wording — reuse verbatim downstream |
4526
5238
  | `subject_type` | enum | yes | `character \| person \| product` — picks the template + default views |
4527
5239
  | `views` | string[] | no | 2–6 view labels; defaults per `subject_type` (below) |
@@ -4548,7 +5260,7 @@ Default views per `subject_type`:
4548
5260
 
4549
5261
  Guidance:
4550
5262
  - 3–4 full-body views at `16:9` is the sweet spot for characters; more views shrink each figure and lose face detail.
4551
- - Use `google/gemini-3.1-flash-image-preview` while iterating; switch to `google/gemini-3-pro-image-preview` at `2K`+ for final 6-view sheets.
5263
+ - Use `google/gemini-3.1-flash-image` while iterating; switch to `google/gemini-3-pro-image` at `2K`+ for final 6-view sheets.
4552
5264
  - Feed the whole sheet downstream as one `reference` input — don't crop individual views.
4553
5265
 
4554
5266
  **Example** — two photos of a sneaker → product sheet → consistent hero shot:
@@ -4561,14 +5273,14 @@ Guidance:
4561
5273
  { "id": "b", "type": "ingest", "params": { "source": "url", "url": "https://example.com/sneaker-side.jpg", "expect": "image" } },
4562
5274
  { "id": "sheet", "type": "image_reference_sheet",
4563
5275
  "inputs": { "references": ["$ref:a.asset", "$ref:b.asset"] },
4564
- "params": { "model": "google/gemini-3-pro-image-preview",
5276
+ "params": { "model": "google/gemini-3-pro-image",
4565
5277
  "subject_description": "navy leather low-top sneaker with white sole and gold logo",
4566
5278
  "subject_type": "product",
4567
5279
  "style": "matte black studio background, soft even key light",
4568
5280
  "aspect_ratio": "16:9", "image_size": "2K" } },
4569
5281
  { "id": "hero", "type": "image_generate",
4570
5282
  "inputs": { "reference": "$ref:sheet.sheet" },
4571
- "params": { "model": "google/gemini-3-pro-image-preview",
5283
+ "params": { "model": "google/gemini-3-pro-image",
4572
5284
  "prompt": "The navy leather low-top sneaker with white sole and gold logo from the reference sheet, hero shot on a rain-soaked neon street at night" } }
4573
5285
  ],
4574
5286
  "output": { "node": "hero", "output": "images" }
@@ -4581,7 +5293,7 @@ Guidance:
4581
5293
 
4582
5294
  Adapt ONE creative into multiple aspect ratios in a single step — the standard fan-out when a hero creative must ship to every placement (Meta: 9:16 stories, 1:1 feed, 4:5, 16:9…). The AI recomposes the layout per format: identical subject, faces, products, text (verbatim, same typography), logos, colors, and style; the scene is extended/restructured for the new canvas — never stretched, cropped, or letterboxed.
4583
5295
 
4584
- The adaptation prompt is baked into the backend and is direction-aware (a wider target gets horizontal recomposition guidance, a taller one vertical). Formats that already match the source ratio (within ~2%) pass the source asset through unchanged — no model call, no cost.
5296
+ The adaptation prompt is baked into the backend and is direction-aware by orientation: a portrait source adapted to landscape (or the reverse) is recomposed; a same-orientation change only extends the scene at the sides or at the ends, every element kept where it is. Optional `copy` (the ad's exact lines: each kept once, spelled as given) and `safe_areas` (per format, the fraction of each edge the platform's interface covers) are set by the static scaffold. Formats that already match the source ratio (within ~2%) pass the source asset through unchanged — no model call, no cost.
4585
5297
 
4586
5298
  **Inputs**
4587
5299
 
@@ -4593,7 +5305,7 @@ The adaptation prompt is baked into the backend and is direction-aware (a wider
4593
5305
 
4594
5306
  | Name | Type | Required | Notes |
4595
5307
  |---|---|---|---|
4596
- | `model` | enum | yes | one of `google/gemini-3-pro-image-preview`, `google/gemini-3.1-flash-image-preview` |
5308
+ | `model` | enum | yes | one of `google/gemini-3-pro-image`, `google/gemini-3.1-flash-image` (their `-preview` ids still accepted) |
4597
5309
  | `formats` | enum[] | one of `formats`/`platform` | 1–6 unique target ratios from `1:1 \| 2:3 \| 3:2 \| 3:4 \| 4:3 \| 4:5 \| 5:4 \| 9:16 \| 16:9 \| 21:9` — output order follows this list |
4598
5310
  | `platform` | enum | one of `formats`/`platform` | a placement preset that expands to that platform's canonical on-grid set: `meta_feed` (4:5), `meta_feed_stories` (4:5, 9:16), `meta_all` (4:5, 9:16, 1:1), `linkedin` (1:1), `google_demandgen` (1:1, 4:5, 16:9), `tiktok` (9:16), `youtube` (16:9). Mutually exclusive with `formats`. Off-grid platform targets (e.g. Google 1.91:1) are not rendered here — crop them from the nearest rendered ratio with `baker images crop`. |
4599
5311
  | `guidance` | string | no | hints appended to the baked prompt, e.g. `"keep the CTA button fully visible"` |
@@ -4610,7 +5322,7 @@ The adaptation prompt is baked into the backend and is direction-aware (a wider
4610
5322
  Guidance:
4611
5323
  - Reach for `platform` when you want a placement's whole set without naming ratios (e.g. `"platform": "meta_feed_stories"`); use `formats` when you need an exact, custom ratio list. Set one, never both.
4612
5324
  - List the source's own ratio in `formats` too when you want the full set in one output — it passes through for free.
4613
- - Use `google/gemini-3.1-flash-image-preview` while iterating; switch to `google/gemini-3-pro-image-preview` for final-quality adaptation.
5325
+ - Use `google/gemini-3.1-flash-image` while iterating; switch to `google/gemini-3-pro-image` for final-quality adaptation.
4614
5326
  - Indexes are positional: `$ref:adapt.images#1` is always the second entry of `formats`.
4615
5327
 
4616
5328
  **Example** — one 9:16 story creative fanned out to feed and landscape:
@@ -4620,12 +5332,12 @@ Guidance:
4620
5332
  "schema": "baker-canvas/1",
4621
5333
  "nodes": [
4622
5334
  { "id": "hero", "type": "image_generate",
4623
- "params": { "model": "google/gemini-3-pro-image-preview",
5335
+ "params": { "model": "google/gemini-3-pro-image",
4624
5336
  "prompt": "Story-format ad for a citrus energy drink: can centered, bold OFFER text top, logo bottom",
4625
5337
  "aspect_ratio": "9:16", "image_size": "2K" } },
4626
5338
  { "id": "adapt", "type": "image_aspect_adapt",
4627
5339
  "inputs": { "source": "$ref:hero.images#0" },
4628
- "params": { "model": "google/gemini-3-pro-image-preview",
5340
+ "params": { "model": "google/gemini-3-pro-image",
4629
5341
  "formats": ["9:16", "1:1", "16:9"],
4630
5342
  "guidance": "keep the offer text and logo fully visible" } }
4631
5343
  ],
@@ -4723,13 +5435,13 @@ Search the web for real images instead of generating them. A backend LLM agent p
4723
5435
 
4724
5436
  ##### `image_select`
4725
5437
 
4726
- Pick the best `count` images out of 2+ candidates with a vision LLM, judged against a prompt. The output is a passthrough subset of the input refs — no new pixels are generated, so selection is cheap. Use it after fanning out several `image_generate` variants (or an `image_search` batch) to keep only the strongest before expensive downstream steps.
5438
+ Pick the best `count` images out of the candidates with a vision LLM, judged against a prompt. The output is a passthrough subset of the input refs — no new pixels are generated, so selection is cheap. Use it after an `image_generate` run with `n` > 1, several `image_generate` variants, or an `image_search` batch, to keep only the strongest before expensive downstream steps. A single candidate (a generation run at `n: 1`) passes through unjudged: no call, no charge, no `reasoning`.
4727
5439
 
4728
5440
  **Inputs**
4729
5441
 
4730
5442
  | Slot | Kind | Required | Accepted MIMEs |
4731
5443
  |---|---|---|---|
4732
- | `images` | image[] | yes — at least 2 | `image/png`, `image/jpeg`, `image/webp`, `image/gif` |
5444
+ | `images` | image[] | yes — at least 1 | `image/png`, `image/jpeg`, `image/webp`, `image/gif` |
4733
5445
 
4734
5446
  Wire either a whole upstream array (`"$ref:gen.images"`) or a literal list collecting refs from several nodes (`["$ref:a.images#0", "$ref:b.images#0"]`).
4735
5447
 
@@ -4740,7 +5452,7 @@ Wire either a whole upstream array (`"$ref:gen.images"`) or a literal list colle
4740
5452
  | `model` | enum | yes | one of `~google/gemini-flash-latest`, `~google/gemini-pro-latest` (`pro` for harder aesthetic judgement) |
4741
5453
  | `prompt` | string | yes | selection criteria, e.g. `"most professional product shot, clean background"` |
4742
5454
  | `count` | int | no | images to select, ≥ 1 (default 1); must not exceed the wired image count |
4743
- | `temperature` | number | no | 0–2, defaults to 0 |
5455
+ | `temperature` | number | no | 0–2; unset by default, so the Gemini judge keeps its own default |
4744
5456
  | `max_tokens` | number | no | positive int |
4745
5457
 
4746
5458
  **Outputs**
@@ -4748,7 +5460,7 @@ Wire either a whole upstream array (`"$ref:gen.images"`) or a literal list colle
4748
5460
  | Slot | Kind | MIME |
4749
5461
  |---|---|---|
4750
5462
  | `images` | image[] | per input — exactly `count` refs, in the model's preference order, so `images#0`…`images#count-1` are always safe to wire |
4751
- | `reasoning` | text | `text/plain` — why the winners won, compared against the rejected candidates |
5463
+ | `reasoning` | text | `text/plain` — why the winners won, compared against the rejected candidates; absent when a single candidate passed through |
4752
5464
 
4753
5465
  **Cost** — 1 credit estimate per call (backend charges actual OpenRouter usage).
4754
5466
 
@@ -4760,9 +5472,9 @@ Fail-fast: an invalid model selection (wrong count, duplicate or out-of-range pi
4760
5472
  {
4761
5473
  "schema": "baker-canvas/1",
4762
5474
  "nodes": [
4763
- { "id": "a", "type": "image_generate", "params": { "model": "google/gemini-3.1-flash-image-preview", "prompt": "hero shot of a ceramic mug, warm morning light" } },
4764
- { "id": "b", "type": "image_generate", "params": { "model": "google/gemini-3.1-flash-image-preview", "prompt": "hero shot of a ceramic mug, studio softbox" } },
4765
- { "id": "c", "type": "image_generate", "params": { "model": "google/gemini-3.1-flash-image-preview", "prompt": "hero shot of a ceramic mug, dramatic side light" } },
5475
+ { "id": "a", "type": "image_generate", "params": { "model": "google/gemini-3.1-flash-image", "prompt": "hero shot of a ceramic mug, warm morning light" } },
5476
+ { "id": "b", "type": "image_generate", "params": { "model": "google/gemini-3.1-flash-image", "prompt": "hero shot of a ceramic mug, studio softbox" } },
5477
+ { "id": "c", "type": "image_generate", "params": { "model": "google/gemini-3.1-flash-image", "prompt": "hero shot of a ceramic mug, dramatic side light" } },
4766
5478
  { "id": "best", "type": "image_select",
4767
5479
  "inputs": { "images": ["$ref:a.images#0", "$ref:b.images#0", "$ref:c.images#0"] },
4768
5480
  "params": { "model": "~google/gemini-pro-latest", "prompt": "most premium-feeling lighting, no harsh shadows" } }
@@ -4794,7 +5506,7 @@ Accepted ref-image MIMEs vary by model — see per-model sections below.
4794
5506
  |---|---|---|
4795
5507
  | `video` | video | `video/mp4` |
4796
5508
 
4797
- **Cost** — 50 credits estimate per call.
5509
+ **Cost** — priced per model from the shared contract table (duration x resolution x audio; the fal route and source-clip premium when they apply), the same table the backend charges from. One clip may not cost more than $20: the backend refuses a dearer one before it runs, and no clip the registry allows reaches it.
4798
5510
 
4799
5511
  ---
4800
5512
 
@@ -4858,7 +5570,7 @@ Ref-image MIMEs: `image/png`, `image/jpeg`, `image/webp`.
4858
5570
  |---|---|---|---|
4859
5571
  | `model` | literal | yes | `"bytedance/seedance-2.0"` |
4860
5572
  | `prompt` | string | yes | non-empty |
4861
- | `duration` | enum | no | `4, 5, 6, 8, 10, 12, 15` (gaps trigger 400s) |
5573
+ | `duration` | enum | no | every whole second `4`-`15` |
4862
5574
  | `resolution` | enum | no | `480p \| 720p \| 1080p` |
4863
5575
  | `aspect_ratio` | enum | no | `1:1, 3:4, 9:16, 4:3, 16:9, 21:9, 9:21` |
4864
5576
  | `generate_audio` | boolean | no | native audio track |
@@ -4901,18 +5613,18 @@ Ref-image MIMEs: `image/png`, `image/jpeg`, `image/webp`, `image/gif` (via OpenR
4901
5613
 
4902
5614
  > **Switching a clip's model has cross-node constraints** — validate after every model edit. `validate` gates all of these before any billed run:
4903
5615
  > - **One aspect_ratio for the whole canvas.** Every `video_generate` node must agree (`VIDEO_ASPECT_MISMATCH`), or the composite silently crops/letterboxes. Switching one clip to Veo (which only offers `16:9`/`9:16`) forces every other clip to a shared ratio too.
4904
- > - **`duration` is a per-model hard enum.** Seedance `4,5,6,8,10,12,15`; Veo `4,6,8` only. A value outside the model's set is rejected; a scene whose natural span exceeds the model's max is flagged (`VIDEO_SPAN_EXCEEDS_MODEL`, advisory) — a 9.2s scene can't fit Veo's 8s cap and would truncate; split the scene or keep it on a longer-max model.
5616
+ > - **`duration` is a per-model hard enum.** Seedance 2.0 `4`-`15`, Seedance 2.5 `4`-`30`, Kling `3`-`15`, Omni `3`-`10`, Veo `4,6,8` only. The scaffolders snap clip lengths to the same registry sets. A value outside the model's set is rejected; a scene whose natural span exceeds the model's max is flagged (`VIDEO_SPAN_EXCEEDS_MODEL`, advisory) — a 9.2s scene can't fit Veo's 8s cap and would truncate; split the scene or keep it on a longer-max model.
4905
5617
  > - **`person_generation` differs.** Veo **image-to-video** accepts only `allow_adult` (the sole legal value for keyframe-driven modes, and the only one permitted in the EU/UK); `allow_all` is text-to-video only. The scaffolder sets `allow_adult` on every Veo clip automatically (`VIDEO_PERSON_GENERATION_MISSING` warns if it's missing on a hand-edited canvas).
4906
5618
  >
4907
5619
  > A scaffolded canvas carries this table inline at `metadata.todo.model_constraints`.
4908
5620
 
4909
- > **Per-model prompt & param profiles (applied automatically).** The scaffolder shapes each clip/frame prompt to the model's documented best practice and sets its request-param levers — see `packages/cli/src/engine/scaffold/lib/prompt-profiles.ts`:
5621
+ > **Per-model prompt & param profiles (applied automatically).** The scaffolder shapes each clip/frame prompt to the model's documented best practice and sets its request-param levers — see `packages/cli/src/engine/lib/prompt-profiles.ts`:
4910
5622
  > - **Seedance 2.0** (default video): dialogue in quotes with delivery in **prose** (not `[brackets]`), a `Preserve the composition and colors` keyframe hold, and an **affirmative** stability tail — `Keep it clean and stable: crisp anatomically-correct hands, one consistent face, smooth steady motion` — instead of an `Avoid: …` list (Seedance has no `negative_prompt` param, and inline negation tends to summon the artifact it names). Prompt kept to the ~120-word quality budget (`VIDEO_PROMPT_OVER_BUDGET` warns past ~120).
4911
5623
  > - **Veo 3.1** (fallback): `person_generation=allow_adult` + a caption-suppressing `negative_prompt` set as params; durations snapped to `{4,6,8}`.
4912
5624
  > - **gpt-image** (default frames): clean-plate exclusions placed **LAST** (OpenAI weights early text as composition), an explicit `photorealistic` cue, identity-critical reference **first**, and `quality=high` (gpt-image-2 forces high input fidelity automatically — the `input_fidelity` param is deliberately **not** sent). Gemini keeps exclusions inline; Recraft drops the photoreal cue.
4913
5625
  > - New advisories: `VIDEO_RAW_FACE_KEYFRAME` (a Seedance clip animating a RAW ingested image — real-person 422 risk), `IMAGE_PROMPT_PROFILE_MISSING`, `VIDEO_PROMPT_PROFILE_MISSING`.
4914
5626
 
4915
- > **Content-policy blocks are deterministic, not flaky — and now auto-recover.** ByteDance's Seedance rejects any first/last frame that reads as a real-person likeness (even an AI-generated face) — surfaced as `content_policy_blocked` (HTTP 422/`E005` "flagged as sensitive", **non-retryable on the same model**). This is ByteDance's upstream filter, so it fires on **any** reseller (Replicate or otherwise) — a provider swap does **not** route around it. **When a Seedance `video_generate` clip is blocked, the backend automatically re-renders that same clip on `google/veo-3.1-fast` (`person_generation: allow_adult`) — the one curated model that clears the filter — so the run still completes.** The Seedance attempt is never billed (its output was never finalized); only the Veo render is charged. To skip that wasted first attempt entirely (and control look/cost), scaffold a photoreal-cast ad directly on Veo up front (`--real-face`, which also covers AI-generated photoreal faces), or make the source frame less photorealistic.
5627
+ > **Content-policy blocks are deterministic, not flaky.** ByteDance's Seedance rejects a first/last frame or reference that reads as a real-person likeness (even an AI-generated face), surfaced as `content_policy_blocked` (HTTP 422/`E005` "flagged as sensitive", **non-retryable on the same route**). A blocked clip is **not** moved to another model: one guided only by reference images is re-rendered on the same Seedance model through the fal route, which accepts faces (priced at that route, about twice the rate); one that starts from a frame fails with `content_policy_blocked`, nothing billed, and is re-run on `google/veo-3.1` (`person_generation: allow_adult`). To skip the wasted attempt, scaffold a photoreal-cast ad on Veo up front (`--real-face`, which also covers AI-generated photoreal faces), or make the source frame less photorealistic.
4916
5628
 
4917
5629
  ---
4918
5630
 
@@ -4931,7 +5643,7 @@ None.
4931
5643
  | `audio` | audio | provider-determined (mp3 by default) |
4932
5644
  | `timestamps` | json | `application/json` — present only when `with_timestamps: true` |
4933
5645
 
4934
- **Cost** — `max(1, ceil(text.length × 0.0015))` credits.
5646
+ **Cost** — ElevenLabs characters x $0.00022 (0.022 credits a character), rounded up to whole credits; the backend charges the same rate on the character count ElevenLabs reports.
4935
5647
 
4936
5648
  ###### Model: `elevenlabs/eleven_v3`
4937
5649
 
@@ -4963,7 +5675,7 @@ None.
4963
5675
 
4964
5676
  ##### `video_lipsync`
4965
5677
 
4966
- Lip-sync a video to an audio track via Sync Labs `sync/lipsync-2` (Replicate).
5678
+ Lip-sync a video to an audio track. Model `replicate/sync-lipsync-2`, which runs Sync Labs `sync/lipsync-2` on Replicate.
4967
5679
 
4968
5680
  **Inputs**
4969
5681
 
@@ -4976,7 +5688,7 @@ Lip-sync a video to an audio track via Sync Labs `sync/lipsync-2` (Replicate).
4976
5688
 
4977
5689
  | Name | Type | Required | Notes |
4978
5690
  |---|---|---|---|
4979
- | `model` | literal | yes | `"fal/veed-lipsync"` |
5691
+ | `model` | enum | yes | `"replicate/sync-lipsync-2"`. The legacy id `"fal/veed-lipsync"` is still accepted and runs the same model. |
4980
5692
 
4981
5693
  **Outputs**
4982
5694
 
@@ -4984,7 +5696,7 @@ Lip-sync a video to an audio track via Sync Labs `sync/lipsync-2` (Replicate).
4984
5696
  |---|---|---|
4985
5697
  | `video` | video | `video/mp4` |
4986
5698
 
4987
- **Cost** — 20 credits per call.
5699
+ **Cost** — $0.40 per minute of the longer input (40 credits for a minute, the quote's assumption when the clips carry no duration). Runs as a background job.
4988
5700
 
4989
5701
  ---
4990
5702
 
@@ -5002,8 +5714,8 @@ Strip a video's background → transparent alpha WebM (VP9) or MOV (ProRes 4444)
5002
5714
 
5003
5715
  | Name | Type | Required | Default | Notes |
5004
5716
  |---|---|---|---|---|
5005
- | `model` | literal | no | `"fal/veed-video-background-removal"` | — |
5006
- | `edge_refinement` | boolean | no | `true` | — |
5717
+ | `model` | enum | no | `"replicate/birefnet-video"` | The legacy id `fal/veed-video-background-removal` is still accepted and runs the same model. |
5718
+ | `edge_refinement` | boolean | no | `true` | Refines soft edges against background bleed (the model's `refine_fg`). Off bills the lower rate. |
5007
5719
  | `output_codec` | enum | no | `"vp9"` | `vp9 \| h264` |
5008
5720
 
5009
5721
  **Outputs**
@@ -5012,7 +5724,7 @@ Strip a video's background → transparent alpha WebM (VP9) or MOV (ProRes 4444)
5012
5724
  |---|---|---|
5013
5725
  | `video` | video | `video/webm` (VP9 + alpha) or `video/mp4` (H264 RGB + alpha) |
5014
5726
 
5015
- **Cost** — 50 credits per call.
5727
+ **Cost** — $0.012 per 30 frames with edge refinement, $0.008 without, at 30fps (36 credits for the 30s the quote assumes). Runs as a background job.
5016
5728
 
5017
5729
  ---
5018
5730
 
@@ -5033,6 +5745,7 @@ Word-level transcription of a video OR a bare audio track (the `video` input acc
5033
5745
  | `language` | string | no | auto | ISO 639-1, 2–8 chars (e.g. `en`, `es`) |
5034
5746
  | `transcriber` | enum | no | `groq` | `groq` (Whisper) \| `deepgram` (Nova-3, adds the `rich` output) |
5035
5747
  | `keyterms` | string[] | no | — | Up to 30 names the recogniser must not mangle — a product, a person, a phrase the script coins. The **company's own name is always applied server-side**, so it never needs listing here |
5748
+ | `script` | string | no | — | The words the track was asked to say. Given, the `transcript` output is the script's words, spelled as written, at the times the recogniser heard them: aligned locally (misheard words, fillers, accents and numbers said as words are all tolerated), never sent to the recogniser. `scaffold-ad` sets it on every spoken ad |
5036
5749
 
5037
5750
  **Outputs**
5038
5751
 
@@ -5043,7 +5756,7 @@ Word-level transcription of a video OR a bare audio track (the `video` input acc
5043
5756
 
5044
5757
  **Known names are spelled the way the brand spells them.** Every term (the company's own name, plus `keyterms`) is biased into the request — Deepgram `keyterm`, Whisper `prompt` — and its spelling is then enforced on the words that come back, because bias is a probability and a caption prints the mistake at full size over the ad. Verified against the take that shipped wrong: the same Spanish clip returns "Cubicap" bare and "Cubicup" with the name applied. Corrections are listed on the output's `brand_spelling_corrections` metadata, so a rewrite is visible rather than silent. A short name (under 6 characters) gets the bias only — at that length ordinary words sit one edit away, and rewriting those to protect a brand would corrupt the line. **Never patch a mishearing inside a caption composition**: that fix dies with that one creative.
5045
5758
 
5046
- **Cost** — 2 credits estimate; backend reports actual based on duration (Groq: $0.04/hr, 10s minimum; Deepgram Nova-3: $0.0043/min).
5759
+ **Cost** — Groq: $0.04/hr, 10s minimum; Deepgram Nova-3: $0.0043/min. Quoted on one minute of audio; the backend charges the transcribed duration.
5047
5760
 
5048
5761
  **Requirements** — `ffmpeg` on PATH for audio extraction (already in the E2B sandbox image). Without it the node still works but uploads the full video and Groq's 100 MB cap applies.
5049
5762
 
@@ -5174,7 +5887,7 @@ Place and mix several audio clips onto one timeline — a music bed plus timed v
5174
5887
 
5175
5888
  ##### `image_background_remove`
5176
5889
 
5177
- Strip background → transparent PNG. Powered by `men1scus/birefnet` (Replicate). (Note: mask-only output is no longer produced.)
5890
+ Strip background → transparent PNG. Model `replicate/birefnet`, which runs `men1scus/birefnet` on Replicate. The model takes an image and a resolution only: it has no variants and returns no mask.
5178
5891
 
5179
5892
  **Inputs**
5180
5893
 
@@ -5186,19 +5899,18 @@ Strip background → transparent PNG. Powered by `men1scus/birefnet` (Replicate)
5186
5899
 
5187
5900
  | Name | Type | Required | Default | Notes |
5188
5901
  |---|---|---|---|---|
5189
- | `model` | literal | no | `"fal/birefnet-v2"` | — |
5190
- | `model_variant` | enum | no | `"General Use (Light)"` | `General Use (Light) \| General Use (Heavy) \| Matting \| Portrait \| DIS \| HRSOD \| COD` |
5902
+ | `model` | enum | no | `"replicate/birefnet"` | The legacy id `fal/birefnet-v2` is still accepted and runs the same model. |
5191
5903
  | `operating_resolution` | enum | no | — | `1024x1024 \| 2048x2048 \| 2304x2304` |
5192
- | `mask_only` | boolean | no | `false` | return binary mask instead of alpha-cut |
5904
+ | `model_variant` | enum | no | — | **Deprecated, ignored.** Accepted so older canvases still parse. |
5905
+ | `mask_only` | boolean | no | — | **Deprecated, ignored.** The cutout is always returned; there is no mask. |
5193
5906
 
5194
5907
  **Outputs**
5195
5908
 
5196
5909
  | Slot | Kind | MIME |
5197
5910
  |---|---|---|
5198
5911
  | `image` | image | `image/png` (with alpha) |
5199
- | `mask` | image | `image/png` (only when `mask_only: true`) |
5200
5912
 
5201
- **Cost** — 1 credit per call.
5913
+ **Cost** — 1 credit per call. Runs as a background job.
5202
5914
 
5203
5915
  ---
5204
5916
 
@@ -5221,7 +5933,7 @@ Generate music from prompt OR score an existing video clip.
5221
5933
  | `audio` | audio | provider-determined |
5222
5934
  | `timestamps` | json | `application/json` (optional) |
5223
5935
 
5224
- **Cost** — `max(1, ceil(seconds × 2.2))` credits (defaults to 30s if `music_length_ms` unset).
5936
+ **Cost** — $0.022 per second of audio (2.2 credits a second), 30s when `music_length_ms` is unset; rounded up to whole credits once, not per second. `elevenlabs/video-background-music-v1` runs as a background job.
5225
5937
 
5226
5938
  ###### Model: `elevenlabs/music-v1`
5227
5939
 
@@ -5279,7 +5991,7 @@ None.
5279
5991
 
5280
5992
  Same as `tts`.
5281
5993
 
5282
- **Cost** — `max(1, ceil(total_chars × 0.0015))` credits.
5994
+ **Cost** — total characters x $0.00022 (0.022 credits a character), rounded up to whole credits.
5283
5995
 
5284
5996
  ---
5285
5997
 
@@ -5308,7 +6020,7 @@ None.
5308
6020
  |---|---|---|
5309
6021
  | `audio` | audio | provider-determined |
5310
6022
 
5311
- **Cost** — `max(1, ceil(seconds × 2.2))` credits.
6023
+ **Cost** — $0.022 per second of audio (2.2 credits a second), 5s when `duration_seconds` is unset.
5312
6024
 
5313
6025
  ---
5314
6026
 
@@ -5383,7 +6095,7 @@ Cache policies (via `--cache-policy`):
5383
6095
  ### Safety limits
5384
6096
 
5385
6097
  - **Asset size cap: 2 GiB per asset.** Applies to every download (remote node outputs, `ingest` fetches, yt-dlp output) and to local files ingested via `source: "path"`. Oversized assets fail the node with a `file_too_large` / byte-limit error.
5386
- - **Per-request cost cap: $10 of estimated provider cost.** The backend pre-estimates cost for billable nodes (`video_generate` on Seedance, `tts`, `dialogue`, `music`, `sound_effect`) and rejects a single execution above the cap with `cost_limit_exceeded`. Split long renders or long scripts into smaller nodes.
6098
+ - **Per-request cost cap: $20 of estimated provider cost for a `video_generate` clip, $10 for every other node.** The backend pre-estimates cost from the shared contract table (for a Seedance clip, on the dearest route it may take) and rejects a single execution above the cap with `cost_limit_exceeded`. No clip the registry allows exceeds $20; split long audio or long scripts into smaller nodes.
5387
6099
  - **Retries never double-charge.** Every remote node execution carries a run-scoped idempotency key; if a request is retried after a transient failure, the backend replays the stored response instead of dispatching (and billing) the provider again. A fresh run (including `--cache-policy bypass`) always generates fresh.
5388
6100
 
5389
6101
  ---
@@ -5394,6 +6106,8 @@ Cache policies (via `--cache-policy`):
5394
6106
 
5395
6107
  Parse + schema-check + per-node deep validation (composition `meta.json` is consulted for `hyperframe_*`). Prints a structured result with a per-node cost preview. Exits non-zero on issues. Never calls Convex, never spends credits.
5396
6108
 
6109
+ **The canvas file is brought up to date first.** Parts of a creative's canvas are projections of its blueprint: each video scene's frame and clip prompts (from `scenes/sNN.json`), a static ad's render prompts, judge criteria and type specimen (compiled from the ad spec in `prompt.json`), and the derived style file the frames read (`prompt.style.json`). `validate`, `run`, `critique` and `set-prompt` share one preflight that applies every projection and **writes the result back to the canvas file** (only when something changed; `canvas_updated` says so; `sync-definition`, which the file watcher runs over every creative, applies it without writing), so the file on disk, the graph the dashboard shows, the graph the critic scores and the graph that renders are the same. A scene edit that changes timing or the number of scenes is structural and still asks for a re-scaffold. Every preflight failure prints `{ ok: false, error: { code, message, fix? } }` and exits 2.
6110
+
5397
6111
  ```bash
5398
6112
  $ baker canvas validate my-canvas.json
5399
6113
  {
@@ -5407,7 +6121,7 @@ $ baker canvas validate my-canvas.json
5407
6121
  }
5408
6122
  ```
5409
6123
 
5410
- On error you get an array of `ValidationIssue`s, each with `path`, `code`, `message`, and optional `did_you_mean`.
6124
+ On error you get `{ ok: false, error: { code: "validation", issues } }`, each issue with `path`, `code`, `message`, and optional `did_you_mean`.
5411
6125
 
5412
6126
  **Absolute asset paths are healed, not reported.** A `source: "path"` ingest (or a `hyperframe_render`
5413
6127
  composition) written as an absolute path inside the workspace is rewritten to its canvas-relative form
@@ -5438,7 +6152,8 @@ Pre-spend creative critic — **advisory, never blocks**. Scores a scaffolded cr
5438
6152
  so a weak spot gets fixed for free. It branches on modality: a **video** creative is scored on **hook strength**,
5439
6153
  **sound-off first-frame legibility**, **retention risk**, and **mechanism clarity**; a **static ad** on **baked-text
5440
6154
  legibility risk**, **identity grounding** (Rule 1 — every subject on a real reference), **brand-type fidelity** (a
5441
- font specimen wired), and **mechanism clarity**. Reads the canvas only (no generation, no Convex). Ground a video
6155
+ font specimen wired), and **mechanism clarity**. Scores the canvas after the shared preflight (so scene and copy
6156
+ edits are what gets scored); no generation, no Convex. Ground a video
5442
6157
  hook against a proven pattern with `baker winning-ads hooks`.
5443
6158
 
5444
6159
  ```bash
@@ -5451,25 +6166,67 @@ $ baker canvas critique my-static-ad.json
5451
6166
  "dimensions": [ { "dimension": "text_legibility_risk", "score": 1, "note": "2 copy block(s) — reads cleanly" }, … ] }
5452
6167
  ```
5453
6168
 
5454
- #### `baker canvas run <file.json> [flags]`
6169
+ #### `baker canvas viewer-check <slug>`
6170
+
6171
+ Would the person a static ad reaches understand it? A model plays that viewer — what they already know and
6172
+ where they scroll — and says, from `prompt.json`'s copy and scene alone, what is offered, to whom and what to do
6173
+ next; the decision model (Jev) compares the answer with `prompt.json`'s `message` (or, for a message that names no
6174
+ product, what the company sells), in whatever language each is written (`scripts/viewer-read-evals`). The ad fails
6175
+ when the viewer can only guess what is offered, is sure of something else, or, for a viewer who does not know the brand, when the words say they already did something with it (a
6176
+ cart, a trial, a donation, a registration); who it is for is reported but never fails an ad alone. The viewer is the `viewer` recorded in
6177
+ `_definition.md`, else the one its `targetAdSet` reaches (as `baker ads meta adsets --audience` reads it), else
6178
+ the persona and `awareness` from the Knowledge, else a cold stranger. Not understood → exit 2 with
6179
+ `viewer_check_failed` and a `fix` that starts with the one rewrite answering a missed offer (`Do this: make the biggest words say "<message>"`) and names what the viewer missed. After two failures of one ad in a chat turn the check no longer holds it back: the command answers `ok: true` with `understood: false` and what was missed, and `baker canvas run` renders it, printing the miss. The casting check is asked alongside the viewer. Answers are cached per copy, scene and viewer.
6180
+ Before either, a static ad whose scene quotes words, or names a price or a share, that the copy (or `scene_text`) does not hold is refused (`scene_words_not_in_copy`): the picture would print them and a correction round would erase them. An unreadable `prompt.json` names each wrong field in a line and shows that field's shape. `baker canvas run` makes the same check on a static ad before it spends (skipped for `--fix`, an autofix round, `--use-variant`, a re-layout and resumed runs, never for an edit of the words;
6181
+ `--skip-viewer-check` renders without it; a model outage lets the render through with a warning). `--full` adds
6182
+ the viewer's whole answer and how the viewer was chosen.
6183
+
6184
+ ```bash
6185
+ $ baker canvas viewer-check club-whiteboard
6186
+ { "ok": false, "error": { "code": "viewer_check_failed",
6187
+ "message": "Not rendered: a cold viewer who knows nothing about the brand yet, on automatic placements would not understand this ad.",
6188
+ "fix": "Seen by a cold viewer …, the words and picture alone do not say what is offered. …", "missed": [ … ] } }
6189
+ ```
6190
+
6191
+ #### `baker canvas run <file.json> [flags]` / `baker canvas run --slug <slug> [flags]`
5455
6192
 
5456
6193
  Validate, then execute the graph. Blocks until done. Logs one line per node. Returns `{ ok, run_id, outputs_dir, output, stats }` on stdout.
5457
6194
 
6195
+ `--slug <slug>` runs a creative by name instead of by file: its `src/creatives/<slug>/<slug>.canvas.json` when this workspace has it, else its files restored from the newest run that recorded a portable snapshot (sha-verified, into `src/creatives/<slug>/`), then the normal run flow. Local files are never overwritten unless `--force-remote` is passed. This replaces `baker canvas rerun <slug>`, which still works as a deprecated alias.
6196
+
6197
+ A completed render the review calls ready (`verdict` `ready` or `ready_with_notes` for a static ad, no blocking finding for a video) sets `status: rendered` in the creative's `_definition.md` (never moving a `published` or `archived` creative back); the output then carries `definition_status: "rendered"`.
6198
+
5458
6199
  | Flag | Default | Effect |
5459
6200
  |---|---|---|
5460
- | `--cache-dir <path>` | `<workspace root>/canvas/.cache` | Where content-addressed bytes + cache index live. |
5461
- | `--outputs-dir <path>` | `<workspace root>/canvas` | Root for per-run output directories. |
6201
+ | `--slug <slug>` | none | Run a creative by name (see above). |
6202
+ | `--force-remote` | off | With `--slug`: restore the recorded files even over local ones. |
5462
6203
  | `--run-id <id>` | auto ULID | Override the generated run id. |
5463
6204
  | `--cache-policy <policy>` | `read_write` | `read_write`, `bypass`, or `read_only`. |
5464
- | `--concurrency <n>` | `5` (or `BAKER_CANVAS_CONCURRENCY`) | Max nodes executing at once within a layer. |
5465
- | `--remote-cache <on\|off>` | `on` (or `BAKER_CANVAS_REMOTE_CACHE`) | Company-scoped remote cache + durable asset persistence. |
5466
- | `--max-credits <n>` | uncapped (or `BAKER_CANVAS_MAX_CREDITS`) | Credit ceiling: aborts before billing when the estimate exceeds it, and at layer boundaries once actual spend does. Completed nodes stay cached, so retrying with a higher cap loses nothing. |
6205
+ | `--concurrency <n>` / `--parallel <n>` | `8` (or `BAKER_CANVAS_CONCURRENCY`) | Max nodes executing at once within a layer. |
6206
+ | `--fresh` | off | Start a new run id instead of resuming an interrupted run of this canvas. |
6207
+ | `--regenerate <ids\|roles>` | none | Force these nodes, and everything downstream, fresh this run only. A role names every node carrying it: on a static ad `static_variants` (or its id `gen`) renders the variants again. An unknown name exits 2 (`unknown_regenerate_node`). |
6208
+ | `--fix "<what to change>"` | none | Static ad only: correct a finished version with one render that changes only what you say; every size is made again from it (see below). The same fix after it left its defect in place is refused (`no_progress`). |
6209
+ | `--use-variant <sha256>` | none | Static ad only: make every size again from another of a version's variants, the one the person chose; the only requests re-lay it out for each other size (see below). |
6210
+ | `--from <run_id>` | the last render | With `--fix` or `--use-variant`: the version to start from. |
6211
+ | `--autofix` / `--no-autofix` | on | Static ad only: when the review finds a blocking defect, make up to 3 more renders (`AUTO_FIX_ROUNDS`) that correct it before reporting (see below). `--no-autofix` reports after the first render. |
6212
+ | `--skip-viewer-check` | off | Static ad only: render without the viewer check (`baker canvas viewer-check`), when the person heard what the viewer would miss and wants the copy as it is. |
6213
+ | `--remote-cache <on\|off>` | `on` (or `BAKER_CANVAS_REMOTE_CACHE`) | Operator only. Company-scoped remote cache + durable asset persistence. |
5467
6214
  | `--no-record` | records | Skip posting the durable run-history record (and its live progress). |
6215
+ | `--until frames` | none | Stop before the clips with the frames settled; the run is recorded as `awaiting_approval` (not failed, not interrupted) and resumes with `--run-id`. |
6216
+ | `--cache-dir <path>` | `<workspace root>/canvas/.cache` | Operator only. Where content-addressed bytes + cache index live. |
6217
+ | `--outputs-dir <path>` | `<workspace root>/canvas` | Operator only. Root for per-run output directories. |
6218
+ | `--keep-runs <n>` | off | Operator only. Prune old `r_*` run dirs after the run, keeping the N newest. |
5468
6219
 
5469
6220
  **Run history streams live.** The run posts its plan (every node + its dependency edges) the moment validation passes, then re-posts a progress snapshot as each node starts and settles — the dashboard's creative workflow graph shows nodes flipping pending → running → done in real time, with each node's outputs attached as they land. A failed run keeps its per-node trail (what completed, what died). All best-effort: an unreachable backend never changes the run's outcome.
5470
6221
 
5471
6222
  **Failures don't abandon sibling work.** Nodes in a layer run under the concurrency cap and every one **settles** — a failed clip no longer kills its in-flight siblings, whose finished results still land in the content-addressed cache. One failure re-throws as-is; several are reported together (each failed node named). Re-running `baker canvas run` resumes from the cache and re-executes **only** the failed nodes and their descendants — never hand-orchestrate per-node renders. **One render per canvas at a time:** a `run` of a canvas that another live process in the same workspace is still rendering (a backgrounded render, or a subagent's) is refused with `CONFLICT` instead of sharing its run, `--run-id` included — wait for it and use its result. Long `video_generate` clips execute as **backend jobs** (the CLI polls; a CDN/proxy timeout can no longer kill a generation mid-flight).
5472
6223
 
6224
+ **Resuming an interrupted run.** A long `baker canvas run` (multi-clip video) that is killed mid-render — session end, sandbox pause — leaves a marker under the outputs dir. The next `baker canvas run` of the same canvas automatically **resumes** that run: it reuses the run id so still-running billed jobs re-attach instead of being abandoned and re-billed, and completed nodes come from the cache. A live render keeps its marker's heartbeat fresh, so a second `run` of the same canvas is refused while it is alive (checked with `kill(pid, 0)` plus the heartbeat, on Linux and macOS alike). Resume also works from a **different workspace or a fresh sandbox**: when no local marker survives, the run history is consulted for a resumable run of the same source (canvas, `prompt.json`, scenes, compositions and formats), and it is adopted when the backend says it may be: interrupted, stopped at a checkpoint, or a `running` row whose keepalive is older than the backend's staleness threshold. Ctrl-C / SIGTERM aborts gracefully — no new nodes dispatch, a resumable snapshot is flushed, and the marker survives. A clean completion (or a handled failure) clears the marker, so a normal re-run starts a fresh generation. Force a new run with `--fresh`, or pin a specific run with `--run-id <id>` (also the escape hatch to adopt a run that is reported as concurrently live). Independent same-layer nodes (e.g. video clips) fan out in parallel up to `--parallel`/`--concurrency` (default 8; env `BAKER_CANVAS_CONCURRENCY`).
6225
+
6226
+ **Static ads: the run corrects a blocking defect itself.** When the review of a static ad finds a blocking defect, `baker canvas run` makes up to 3 more renders (`AUTO_FIX_ROUNDS` in `static-autofix.ts`) before it reports, correcting each defect at the layer that causes it: words or a logo where an interface covers them (Reels, Stories) or the frame cuts them are **placed and extended** (`withExtendedSizes`): a `static_place` node reads where everything sits and shrinks the ad (to 60% at most) into the size's safe area, and a `static_extend_merge` node has the ad's own model paint the margins that leaves and keeps its painting as one picture across the whole frame, laying the ad's own pixels back only over the words, logo and buttons the placing read (padded and feathered, so no edge of the shrunk ad shows; the whole ad is laid back, as before, when nothing was read or the painting moved the words), so the model is never asked to move anything (a size already inside its area is passed on with nothing painted or billed, and one whose words sit too far out for a shrink to clear, or that the placing found inside its area while the review still blocks it, is laid out again instead, never placed the same way twice; a button is placed by its shape, its words grown by 0.7 of their height on every side, as the review judges it); a defect the spec asked for (a second subject, stars with no named source, a concept that does not read) and any defect of a native screen corrects the spec first (`static-spec-repair.ts`: a model rewrites `prompt.json` for those findings alone, every line of the copy kept word for word), then draws the ad again from it (a native screen redraws free); a picture wrong as a whole or in three places or more is made again from its brief, told what the last one got wrong, but only in a round with another after it unless the brief caused the defect; anything else on the picture is a `--fix` of the chosen picture for its own findings (`correctionOf`: the words to erase, the line to re-letter or add), naming what the last edit was already asked for, while each re-laid size the review flagged follows as an edit of its own last layout (`withSizesFollowingFix`), keeping what an earlier round fixed in it, and a size it found nothing blocking on is delivered as it was (`keep`, nothing in it made again or placed); anything else on a re-laid size alone edits just that size from its own last layout (`withRelayEdit`); sizes that do not show the same ad (`SIZES:`) are always the re-laid ones' fault, since every size follows the chosen picture, so they are laid out again to show exactly what it shows, never the picture edited to match them. A size a round did not touch (its file the same byte for byte) keeps the round before's verdict (`carryUnchanged`) and is not read again: the review reads only the sizes the round changed. Each correction is told what earlier rounds fixed so it keeps it right (`keptRules`), and starts from the best attempt so far: one that came out with more blocking defects than an earlier attempt is set aside and the earlier one corrected instead (`correctFrom`). Each round is one render (a fix, or one request per re-laid size) and prints `[autofix] round N of 3: …` on stdout; the final JSON's hints start with "Corrected automatically before this result, …" and its review is of the corrected version. Blocking findings left after the rounds keep the hints below. A round that would only repeat the correction that just left its defect in place (a chosen option, `--use-variant`, is only ever corrected) is not run: that version is reported as it is. `--no-autofix` reports after the first render.
6227
+
6228
+ **Static ads: correct a version, or use another of its variants.** Correct a finished version with `--fix "<what to change>"`: one render takes that version (the last one, or `--from <run_id>`) as its first reference and changes only what you said, keeping the picture, the layout and every other word, and every size is made again from the corrected picture. **A correction grows on top of the version it starts from**: the run executes that version's own graph (its variants, the prefilter and pick, every earlier correction) with each of those steps pinned to the outputs that version recorded (free, never re-rendered, whatever `prompt.json` says now), then appends `edit_1` (the picture as its first reference, framed for the master as a variant is) and makes the sizes from it; a second correction appends `edit_2` on `edit_1`. `--use-variant` wires the chosen variant from the version's own `gen` as a `chosen` step, and a `--regenerate adapt_<size>` keeps the version's picture the same way. So the run record, and the dashboard's workflow of each version, show the whole history: options, pick, edits, sizes. The canvas file is never rewritten. Every static run keeps the graph it executed in the asset store (`graphUrl` on its run record, up to 4 MB), so a version rendered in another workspace ("Edit creative …" from a new chat, `--from <run_id>`) grows the same way, its steps pinned to the outputs its run record holds; only a run recorded before graphs were kept is corrected from its picture by URL. **A `prompt.json` changed only in its words, their positions or its colours** since the version shown is made the same way, as an edit of that version (`[edit]` in the output, the instruction read off the change); three new options are asked for only with `--regenerate static_variants`. A `--fix` whose defect is still blocking afterwards is recorded, and asking for it again with the same words exits 2 with `no_progress`. When every blocking finding is on re-laid-out sizes, the first hint says to lay out those sizes again (`--regenerate adapt_<size>`) before any `--fix`. Every static run's hints say what it cost in credits and what for. There is no render limit per creative. Use another variant with `--use-variant <sha256>` (`--from <run_id>`, default the last render): when the person asks in the chat for another of a version's variants over the judge's pick, every size is made again from that picture, and the only requests re-lay it out for each other size; `--regenerate adapt_9x16` lays out one size again from the same chosen picture when only that size came out wrong (after a `--fix` or `--use-variant`, from that corrected or chosen picture, so the size never brings back the uncorrected one; `--from <run_id>` names another version).
6229
+
5473
6230
  #### `baker canvas scaffold-video <video> [flags]`
5474
6231
 
5475
6232
  Turn a reference video into a **runnable, self-validated reproduction canvas** in one command — the video counterpart of `scaffold-static-ad`. The `<video>` positional is a **local path OR an http(s) URL** (a `baker winning-ads` `media_url`, a library URL, any reel link) — a URL is downloaded for you (no manual `curl` first), so pass `--slug`/`--out` with it to give the canvas a home. It runs **billed passes** up front:
@@ -5483,7 +6240,7 @@ Before the deconstruct it runs a **local shot-cut pass** on the source file with
5483
6240
 
5484
6241
  A shot longer than the video model's per-clip ceiling (Seedance's 15s, passed as `video_deconstruct`'s `max_clip_s`) is split into equal **continuation sub-scenes** that share their splice boundary exactly — so a long shot is reproduced in **full** (no truncation) and joins seamlessly. Each sub-scene carries `continues_previous`.
5485
6242
 
5486
- It then scaffolds the full pipeline like an **editing timeline**: each clip gets a **static-ad-grade start AND end keyframe** (`image_generate`, each with its **own `params.prompt`** regenerated from that scene's `scenes/sNN.json` — edit the scene file to change that frame, never the node prompt by hand; the slim `prompt.style.json` wired as the **shared `target_blueprint`** style reference, plus a per-element reference legend). Each keyframe is **fully recast** to the dropped `el_*` reference images. The original extracted frame is kept LAST as a **pure composition anchor** (framing / camera angle / shot size / pose) only in an ad with **nobody in it** — no deconstructed cast and no recurring person. In any ad with people, no generated frame takes a source frame as a reference: an image model copies the face of whoever is in its references, whatever the prompt says, and which scenes a person is in is itself a model's guess — so framing is carried by the frame prompt instead. **People are cast from company avatars** (`--avatar <handle>`): each recurring person grounds on that avatar's identity sheet, used as-is (no sheet is built from it); a person with no avatar gets a `[TODO]` asking for one, never for a found photo. Both keyframes feed `video_generate` (`first_frame`+`last_frame`, so Seedance interpolates real in-shot motion; ultra-detailed motion brief; duration snapped to the nearest allowed clip length). Every keyframe grounds **only on its own extracted frame + `el_*` slots** — no reference to any other generated frame — so all images render **in parallel** (no cascade). Source-frame URLs are **deduped** (each ingested once). `--frames reuse` wires the real source frame straight in — only on a video with nobody in it: when the deconstruct found a cast or a recurring person, reuse is refused (`frames_reuse_refused`) and every frame is generated, so the source's actors never reach the ad.
6243
+ It then scaffolds the full pipeline like an **editing timeline**: each clip gets a **static-ad-grade start AND end keyframe** (`image_generate`, each with its **own `params.prompt`** regenerated from that scene's `scenes/sNN.json` — edit the scene file to change that frame, never the node prompt by hand; the slim `prompt.style.json` wired as the **shared `target_blueprint`** style reference, plus a per-element reference legend). Each keyframe is **fully recast** to the dropped `el_*` reference images. The original extracted frame is kept LAST as a **pure composition anchor** (framing / camera angle / shot size / pose) only in an ad with **nobody in it** — no deconstructed cast and no recurring person. In any ad with people, no generated frame takes a source frame as a reference: an image model copies the face of whoever is in its references, whatever the prompt says, and which scenes a person is in is itself a model's guess — so framing is carried by the frame prompt instead. **People are invented or cast**: a person with no avatar gets a generated candid portrait from their description (an `image_generate` node at the `el_*` slot, fused into a sheet), so every frame shows one realistic invented person; with `--avatar <handle>` each recurring person grounds on that avatar's identity sheet, used as-is. Never a found photo. Both keyframes feed `video_generate` (`first_frame`+`last_frame`, so Seedance interpolates real in-shot motion; ultra-detailed motion brief; duration snapped to the nearest allowed clip length). Every keyframe grounds **only on its own extracted frame + `el_*` slots** — no reference to any other generated frame — so all images render **in parallel** (no cascade). Source-frame URLs are **deduped** (each ingested once). `--frames reuse` wires the real source frame straight in — only on a video with nobody in it: when the deconstruct found a cast or a recurring person, reuse is refused (`frames_reuse_refused`) and every frame is generated, so the source's actors never reach the ad.
5487
6244
 
5488
6245
  **Composited scenes (split-screen / picture-in-picture / keyed presenter).** Real ads aren't always one full-frame shot — a frame can be **persistently divided** (b-roll on top, a presenter talking on the bottom) or **layer a presenter** over background footage (boxed in a corner, or green-screen keyed). The deconstruct now reports this per scene as `scene.composition` (`layout: split_screen | pip | keyed_overlay`, with one `region` per stream — each its own clean-plate frame + motion brief, the talking-head region flagged `is_presenter`). The scaffold reproduces a composited scene by building **one clip per region** (`s<i>_r0_*`, `s<i>_r1_*`, …) and compositing them with ffmpeg: a split-screen `vstack`/`hstack` (stack direction read from the region **panels**, so a top/bottom split always stacks vertically), or a picture-in-picture `overlay` of the presenter inset at its corner. A **keyed** presenter is first cut to transparency by `video_background_remove` (`s<i>_key`), then overlaid. The presenter region carries the native lip-synced voice; b-roll/render panels stay silent. To change a layout, edit `composition` in that scene's `scenes/sNN.json` and re-scaffold, or hand-edit the `s<i>_composite` ffmpeg args. Plain full-frame scenes (the default) are unaffected.
5489
6246
 
@@ -5511,7 +6268,7 @@ It then scaffolds the full pipeline like an **editing timeline**: each clip gets
5511
6268
 
5512
6269
  **Timing-faithful clip + extract (no overlap).** Each phrase clip is generated to its **coverage window** (the deconstruct's real scene/line timing, capped at the gateway-safe ~10s ceiling) and its converted voice is extracted to the **spoken window** (pause to pause) — *not* padded to a word-count estimate. Padding past the window was what ran the voice the clip's whole length and overlapped the next phrase; trusting the deconstruct's timing keeps consecutive phrases back-to-back and lets Seedance pace the quoted text to fit. `metadata.video.talking_scenes` records each phrase's `scene_s` vs `est_speech_s`; on top of that the scaffold flags any scene whose estimated speech overruns its window by more than ~1.3× as **`metadata.todo.overstuffed_scenes`** (also in the stdout checklist) — a loud advisory to shorten the copy or lengthen the scene before rendering, since an over-stuffed line pushes the picture off the audio timeline. It similarly flags **`oversize_scenes`** — a single scene whose own footage exceeds the gateway-safe ~10s clip ceiling (a b-roll shot or one-shot monologue). The phrase splitter only breaks at scene boundaries, so it can't shrink a single over-long scene; its clip would 524 at the gateway, so the advisory tells you to split that scene into two before rendering.
5513
6270
 
5514
- **Timeline-accurate picture — ONE clock.** Seedance can't render under 4s, so each clip is generated at the smallest allowed duration ≥ the scene length and **cut back to the exact scene duration inside the spine's own per-input filter chain** (no standalone trim nodes — one fewer re-encode per scene, and the graph stays a third smaller). Real screenshots/graphics loop fit-padded, generated stills loop fill-cropped, on the same chains. The spine is the ONE clock: every voice, SFX, music, and overlay placement is mapped onto where each scene's picture actually sits in the butted concat (not the reference video's own timestamps, which can carry dead air the picture doesn't reproduce), the audio mix's `total_ms` is pinned to the spine length, and every concat input is normalized to one raster clock (`yuv420p`, 30fps, square pixels, shared timebase) so a generated clip's unadvertised frame rate can never stretch the picture off the audio. The contract is stamped as `metadata.video.timeline` and `baker canvas validate` re-proves it from the live node params after any hand edit. Frames are also prompted as **clean text-free plates** (no baked captions/lower-thirds/tickers/logos-as-text) so the overlay layer is the single source of on-screen text.
6271
+ **Timeline-accurate picture — ONE clock.** Seedance can't render under 4s, so each clip is generated at the smallest allowed duration ≥ the scene length and **cut back to the exact scene duration inside the spine's own per-input filter chain** (no standalone trim nodes — one fewer re-encode per scene, and the graph stays a third smaller). Real screenshots/graphics loop fit-padded, generated stills loop fill-cropped, on the same chains. The spine is the ONE clock: every voice, SFX, music, and overlay placement is mapped onto where each scene's picture actually sits in the butted concat (not the reference video's own timestamps, which can carry dead air the picture doesn't reproduce), the audio mix's `total_ms` is pinned to the spine length, and every concat input is normalized to one raster clock (`yuv420p`, 30fps, square pixels, shared timebase) so a generated clip's unadvertised frame rate can never stretch the picture off the audio. The contract is stamped as `metadata.video.timeline` and `baker canvas validate` re-proves it from the live node params after any hand edit. The spine is a `video_concat` node (each clip's `duration_s`, trim or hold is a param, never argv) and the final mux an `av_mux`, and every scene node carries `tags` (its scene, region and role), which is what the validator reads. Frames are also prompted as **clean text-free plates** (no baked captions/lower-thirds/tickers/logos-as-text) so the overlay layer is the single source of on-screen text.
5515
6272
 
5516
6273
  **Scene transitions.** When the deconstruct flags a boundary as `fade`/`whip`/`zoom`/`dissolve`/`swipe` (`scene.transition_out`), the spine reproduces it as an ffmpeg **`xfade`** instead of a hard cut; plain `cut`/`match_cut` stay hard cuts. The overlap is consumed from **extra generated footage** (each transitioning clip is trimmed to `scene_s + transition` and the xfade `offset` lands on the cumulative scene start), so the total length still equals the sum of the scene lengths — the picture stays exactly on the audio timeline.
5517
6274
 
@@ -5543,12 +6300,14 @@ baker canvas run ./reference-ad.video.canvas.json
5543
6300
  | `--max-scenes <n>` | all source scenes | **Cost lever that reduces fidelity** — caps the deconstruct, MERGING away every scene beyond the cap (fewer cuts, lost beats). Prints a warning when set; omit it to reproduce every scene. |
5544
6301
  | `--language <code>` | auto | Transcript/dialogue language hint (e.g. `fr`, `en`). |
5545
6302
  | `--focus <text>` | — | Known provenance/emphasis to ground the deconstruct. |
5546
- | `--deconstruct-model <id>` | `~google/gemini-pro-latest` | Override the `video_deconstruct` model. |
6303
+ | `--deconstruct-model <id>` | `~google/gemini-flash-latest` | Override the `video_deconstruct` model. |
5547
6304
  | `--select-model <id>` | `~google/gemini-flash-latest` | Override the element-selection `text_generate` model. |
5548
- | `--image-model <id>` | `openai/gpt-image-2` | Override the per-frame `image_generate` model (defaults to the strongest, matching `scaffold-static-ad`). |
6305
+ | `--image-model <id>` | `openai/gpt-image-2.5-sunburst` | Override the per-frame `image_generate` model (defaults to the strongest, matching `scaffold-static-ad`). |
5549
6306
  | `--video-model <id>` | scored router | Override the `video_generate` model **and skip the router**. Curated roster: `google/gemini-omni-1.1-flash` (**the default video model** — native audio + reference images, `360p`\|`720p`\|`1080p`\|`4k` at ~$0.034\|$0.101\|$0.152\|$0.304 per second, `16:9`\|`9:16`, `duration` 3–10s and 10s when unset), `bytedance/seedance-2.0` (workhorse), `google/veo-3.1` (cine ceiling + real-face), `google/veo-3.1-fast` (cheap Veo), `kwaivgi/kling-v3.0-pro` (motion-transfer). |
5550
6307
  | `--real-face` | auto-detected | Force the Veo route for a photoreal human presenter (dodges the ByteDance real-person filter). **Usually unnecessary** — the scaffold auto-detects a photoreal on-camera person/animal cast from the deconstruct and routes it to Veo on its own; set this only to force it when the cast isn't detected. Covers AI-generated photoreal faces too, not just real ones. |
5551
6308
  | `--motion-transfer` | off | Motion driven from a reference / hyper-dynamic → the router picks Kling. |
6309
+
6310
+ A model flag naming a model the registry does not have for that node fails the command (`unknown_model`, listing the models it does have) before anything is billed; it no longer falls back silently to another model.
5552
6311
  | `--identity` | off | Same character/product across clips → the router picks the Seedance workhorse. |
5553
6312
  | `--budget <tier>` | `standard` | Router cost posture: `economy` (cheap Veo-fast tier) / `standard` / `premium`. |
5554
6313
  | `--avatar <handles>` | — | Cast company avatars as the reference's people, comma-separated in the order they appear (`marta` or `marta,joan`). Each recurring person grounds on that avatar's identity sheet and takes its subject description; a `same_as` look of them uses the same sheet. Resolved before anything is billed — a handle that is not `ready` fails the command. |
@@ -5571,23 +6330,40 @@ The two scaffold passes are billed (the full `video_deconstruct` is the heavy on
5571
6330
 
5572
6331
  #### `baker canvas scaffold-static-ad <image> [flags]`
5573
6332
 
5574
- `<image>` is a **local path or an `http(s)` URL** (e.g. a presigned S3 link from ad-dna). A URL is passed straight into the `original` ingest node as `source: "url"` — do not `curl` it to a file first. Note a presigned URL embedded in the emitted canvas will **expire** (the `original` node is composition-only, so a later re-run just skips it); pass a stable URL or a local file if you need the canvas to stay runnable. For the URL case, `--out` defaults to the current directory — pass `--out` to scaffold two URL sources into distinct files, or the second run overwrites the first.
6333
+ **New-creative limit:** a chat makes at most 10 new creatives. The eleventh `scaffold-static-ad`, `scaffold-ad` or `scaffold-video` with a new `--slug` exits 2 with `CREATIVE_LIMIT` before any billed pass. Re-scaffolding a creative already made in the chat, or one whose `_definition.md` existed before, does not count.
6334
+
6335
+ `<image>` is a **local path or an `http(s)` URL** (e.g. a presigned S3 link from ad-dna), and it is **optional when `--source-ad-id` is given**: the library's picture is used. With `--slug` the source is localized to durable, content-addressed storage before anything bills, so the emitted canvas stays runnable in any sandbox; without a slug a URL is passed straight into the `original` ingest node (a presigned one expires). For the URL case, `--out` defaults to the current directory — pass `--out` to scaffold two URL sources into distinct files, or the second run overwrites the first.
6336
+
6337
+ Turn a source/inspiration image into a **runnable, self-validated static-ad canvas** — the static counterpart of `scaffold-video`. It runs **billed Gemini passes** up front:
6338
+
6339
+ **A library ad is read once for every company.** With `--source-ad-id` and the library's own picture (no `<image>`, `--context`, `--describe-model` or `--select-model`), both passes below come from `POST /api/ad-library/reference-read`: the backend reads the ad from Ad DNA's picture, grounded in its full DNA, and keeps the read for every company. The first build on an ad pays for it (~40 s); every later one, in any company, gets it at once and free. When the shared read cannot be had, the passes run here as before.
5575
6340
 
5576
- Turn a source/inspiration image into a **runnable, self-validated static-ad canvas** — the static counterpart of `scaffold-video`. Like the video scaffold, this runs **billed Gemini passes** up front:
6341
+ 1. **`image_describe`** (`~google/gemini-flash-latest`) — reverse-engineers the image (layout by what fills each region, copy verbatim, typefaces, capture in words, the `mechanism` and the `audience` it is built for) and writes it next to the canvas as **`prompt.json`**, an **ad spec** (`"schema": "baker-static-ad/3"`; `/2` still reads) you rewrite into the ad you want: `viewer` (who sees it and what they already know), `message` (the one thing it tells them, never printed), `copy` (each line with `lettering`: `in_scene`, written on something in the picture (`on`, `look`), or `designed`, set in the brand's face), `scene` (`description`, `look`: photo | designed | illustration, `capture` in words, `mood`, `layout`), `brand`, `colors` (each tied to an object, from the brand palette), `references` (one picture each, labelled by role: `PERSON_1`, `ANIMAL_1`, `PRODUCT_1`, `BADGE_1`, `PROP_1`), `logo` (and `logo_position`), `third_party_brands` (other brands the offer is about, a car brand on a car loan: `name`, `what`, and `logo` naming the `BADGE_n` reference of its logo; their logos may appear), an optional `direction`, the `mechanism` to rebuild for our brand, and in `/3`: `viewer.market` (`country`, `language`, `currency`), `brand_presence` (`standard` | `native`: a native disguise carries the logo as its sender and no big button), `recognition` (the kind of product a stranger must take it for at a glance, never the format), `native` (the ad is a screen people know, drawn exactly from HTML at every size by the free `static_native` node instead of painted: `x_post` (a post or a thread, with `reply_to`), `whatsapp`, `imessage`, `trustpilot`, `google_reviews`, `notes`, `slack`; on a 9:16 the app is drawn full screen as a phone shows it: status bar and app header pinned at the top, the reply bar and tab bar (or a keyboard under a chat or note) pinned at the bottom, the posts or messages as large as the clear band holds, and the frame's headline, caption and logo a card in the brand's colours under the app's header; a wall of reviews (`trustpilot`, `google_reviews`) takes up to 16 real reviews, the first two (`FEATURED_REVIEWS`) the ad's copy set whole in the safe area and the rest filling the whole frame behind them as large, sharp cards, turned and cut by its edges, because a wall wins on how many there are; a review longer than 160 characters is shown cut at a word with "…", and that cut text is the copy the review reads back; a chat (`whatsapp`, `imessage`) on a 9:16 never shrinks below about 62% of the phone scale: its earliest messages scroll up under the header, as the app does, the node's optional `scrolled` output lists the copy lines scrolled off that size, and the run review expects those lines nowhere on it; a logo file whose drawing runs off its own edge is set aside for a whole one, and a square mark where a wordmark goes is drawn larger; the run review never flags the app's own interface words (status bar, keyboard) as under Reels; those words ("Today", "Verified", "Post your reply") are printed in the viewer's language: Spanish, Portuguese and English as checked against the apps, any other written once by a text model and cached per language (`scripts/interface-words-evals`), English when it cannot; the image model renders only the screen's `photos`, each a plain phone photo of ordinary people of the market; adding the block switches an existing ad on its next run, `--fix` does not apply, an invented brand handle or a rating the Knowledge does not hold (either decimal mark) is refused as `native_screen_invalid`, and a photo of ours goes in as its `image`: a native photo takes no `avatar`, since nothing would cast it), a role on each third-party brand (`partner` | `proof` | `category_signal`), a reference's `image` (wired as a picture of ours) and a person's `avatar` (the company avatar cast), and `concept` (`why_it_wins` in five layers, `load_bearing` elements with `have`/`adapt`/`missing` and `ours`, `verdict` `use`|`adapt`|`reject`, `transfer_note`; seeded from `baker winning-ads fit`). Every render keeps the load-bearing elements; `validate`/`run` refuse a `reject` or a `missing` element with nothing named (`concept_does_not_fit`) and a rating, or a promised price, share, multiple or time, the Knowledge does not hold with that unit, or a superlative it does not make (`proof_without_source`), in any language or several in one ad: a text model reads each claim of the copy and of the Knowledge as a kind, a number and a unit (cached by text, billed with the next run), code compares them exactly, and the decision model (Jev) judges a superlative; with no model, every number the copy writes in digits must be in the Knowledge (`claim-check.ts`, eval: `scripts/claim-check-evals`); a line citing a review platform gets its real badge wired (Trustpilot, G2 and the like by name; Google, Google Play or the App Store when the line shows stars or a score, or the decision model reads it as citing them for reviews in any language, `scripts/review-citation-evals`). **You never write a model prompt**: every render prompt is compiled from this file for the model it runs on (short labelled sections for gpt-image, sentences with reasons for Gemini; each line quoted once; pictures numbered with one role each; colours from the brand; realism as capture cues; the safe area from its one owner; constraints last), on every `validate` and `run`, and shown on its node. A `prompt.json` written before the ad spec (the describe blueprint) is read through an adapter and keeps working. The source's own read (`source`, and the library's `reference`) stays as bookkeeping and is never sent to a model.
6342
+ 2. **element selection** (`~google/gemini-flash-latest`) — picks the **main, identity-critical** elements (the brand logo, a showcased product, a trust badge) **plus any foreground/hero person or animal** — the emotional focal point — even a generic one, because a free-generated face/muzzle reads as AI and grows artifacts; the emotional hero always gets a real-reference slot. When the advertiser's logo appears in **more than one lockup** (a square/icon **mark** and a horizontal **wordmark**), each is emitted as its **own** element (e.g. `LOGO_MARK`, `LOGO_WORDMARK`). The describe pass also records the ad's **typography** (each typeface's classification, a best-guess family, and its weight/case), which seeds the spec's `brand.typeface`. Background extras are dropped. Each element becomes a `references` entry in the spec, named by its role and naming the canvas slot it fills, so the file says which slot grounds which subject.
5577
6343
 
5578
- 1. **`image_describe`** (`~google/gemini-pro-latest`) — reverse-engineers the image into a blueprint JSON, written next to the canvas as **`prompt.json`**. This is the editable "prompt": you rewrite it by hand into the ad you want (palette, copy, claims, subjects). It feeds the generator directly — there is **no automatic brand-transform step**. The blueprint also names the **`winning_mechanisms`** — the special sauce that makes the ad a candidate winner, each tagged `kind` (verbal: rhyme/pun/rhythm; visual: unexpected crop, visual gag, juxtaposition, pattern interrupt, before/after; structural: hook order/reveal) with a `device` and `why_it_works` — so your rewrite rebuilds the mechanism that makes the ad win instead of adapting only the surface and losing it.
5579
- 2. **element selection** (`~google/gemini-flash-latest`) — picks the **main, identity-critical** elements (the brand logo, a showcased product, a trust badge) **plus any foreground/hero person or animal** — the emotional focal point — even a generic one, because a free-generated face/muzzle reads as AI and grows artifacts; the emotional hero always gets a real-reference slot. When the advertiser's logo appears in **more than one lockup** (a square/icon **mark** and a horizontal **wordmark**), each is emitted as its **own** element (e.g. `LOGO_MARK`, `LOGO_WORDMARK`) so you drop the right file in each slot instead of stretching one logo to cover both. The describe pass also records the ad's **typography** under a `fonts` block (each typeface's classification, a best-guess family, and its weight/case) so you know exactly what to drop at the brand-font slot. Background extras are dropped. Each element is stamped back onto its blueprint entry as a `reference_image` label so the JSON self-documents which slot grounds which subject.
5580
- 3. **global layout** (`~google/gemini-flash-latest`) — produces a structured `layout` block in `prompt.json`: the column/row grid, each region's `x_pct`/`y_pct` bounds, panel splits, background/shape, and every text block's relative size/weight/case/alignment. This is what gives the generator a precise composition to rebuild.
6344
+ It then scaffolds a canvas on which **the image model designs the finished ad**, holding only what `prompt.json` holds; no element is mandatory:
5581
6345
 
5582
- It then scaffolds a canvas that ingests `prompt.json`, wires **one `[TODO]` ingest slot per detected element** (plus an optional brand-font → type-specimen) into `image_generate`, and wires the original image in for composition only — left out of the render when it shows a person, because the generator copies that face whatever the prompt says (the blueprint's layout carries the composition instead). Each **animal hero** is additionally fused into a generated **multi-view reference sheet** (`image_reference_sheet`, a turnaround built from the one dropped photo) that the render grounds on instead of the lone flat snapshot — the same identity lock the video scaffold uses, so the muzzle stays consistent and artifact-free from a single reference. Pass `--skip-actor-sheets` to ground straight on the dropped photo. A **person hero** is cast from a company Avatar with `--avatar <handle>`: the slot ingests its identity sheet and `prompt.json` describes that person in the Avatar's own words. The render keeps the face it is given, so it is never a found photo of a real person. The canvas is validated before it's written. stdout returns `{ ok, canvas_path, prompt_path, models, aspect_ratio, placements, output_formats, layout_regions, stats, checklist }` — the **checklist** lists every real asset to drop in (and which heroes get a sheet).
6346
+ - Up to **three variants** (`--takes`, default 3) render on the model the router picks for the concept (today `openai/gpt-image-2.5-sunburst` at quality `xhigh`, about 45 s a picture against 80 s at `max`; `--gen-model` overrides), each a finished ad: the picture and every word of the copy. Each variant is **its own request** (tagged `static_variant_call`), run in parallel on its own direction (the scene as written; another picture of the same message, by the scene's look; another line leading, named by number so no word changes), and a `collect` tagged `static_variants` gathers them, on every model: one request with `n` costs the same (`n` is billed per picture) but gave fewer distinct options, so the graph never uses it. The brief quotes every line of `copy` verbatim and asks for each once; it is compiled again from `prompt.json` on every `validate` and `run`, so an edit re-keys exactly the renders it touches. The brand's **typeface** is shown to the model as a `font_specimen` set on the `designed` lines: a file in `src/brand/fonts/` (via `src/brand/tokens.json` when present), else the heading family `tokens.json` or `BRAND.md` names, fetched as a web font file; an `in_scene` line keeps its own lettering only on the real packaging, signage or screen its `on` names (handwriting on post-its, whiteboards or chalkboards, and a line naming no surface, are set as designed type). Each line's `role` sets its size: a price or stat (else the headline) leads, the `cta` is a large solid button, fine print is smallest. **The logo is optional**: the real file in `src/brand/logos/` goes to the model as a reference to reproduce once, at least a fifth of the frame wide, where it contrasts or on a small brand-colour plate, only when `prompt.json` has `"logo": true` (an older blueprint: an `advertiser_brand` entry in `brands_logos`) and the file exists; no logo is never an error, never a slot to fill and never a stand-in. Setting it false drops the logo on the next run; adding one to a canvas scaffolded without the file needs the file in `src/brand/logos/` and a re-scaffold. `prompt.json` also carries a **`brand`** block (ground, ink, accent, CTA and palette, from `src/brand/tokens.json` roles or `BRAND.md`); a colour outside it never reaches the model. The brief describes a complete ad at the master's shape and asks only for its edge margin (see below). `baker canvas set-prompt` refuses these nodes and names `prompt.json`.
6347
+ - A **`static_prefilter`** node (tagged `variant_prefilter`) drops variants that fail the deterministic checks before the judge sees them, never all of them; its **judge** (`judge_model`, `judge_prompt`) compares the rest pairwise against the compiled brief when the checks tie (one variant passes through unjudged). A canvas scaffolded with a separate `image_select` judge (tagged `pick`) has it folded into the prefilter on its next run.
6348
+ - **Every placement format is made from the chosen picture, and no size is a crop or a paste of another.** The pick goes to exact pixels as the master (an `imagemagick` node tagged `static_master`) at the lead size: the 4:5 when a feed size is delivered, else the square, else the first portrait size, rendered at the model's nearest ratio (3:4 on GPT Image) and filled to Meta's pixels. It is delivered as it is. Every other size (9:16, 1:1, 16:9, …) is **re-laid out** from the master by one request (`image_generate` node `adapt_<size>`, tagged `static_adapt_format`, on the router's `relayout` model, GPT Image 2.5 at quality `xhigh`): the master, the variants' own product and people pictures and the logo file as references and a brief compiled from `prompt.json` for that frame on every run (`static-adapt.ts`): the exact lines and their hierarchy, the same people (same face, same clothes), product and label, logo and none the chosen ad does not show, the words and the button re-scaled for the frame, the frame stated as zones inside its safe area, never the master shrunk onto a background, set as a square or cut. A `size_<size>` node (`static_size`) brings it to Meta's exact pixels (1:1 at 1080×1080; a landscape size the same way). Every 9:16 Reels or Stories covers then passes a free `static_safe_fit` node (`safe_<size>`), which shrinks and shifts the picture just enough (to 85% at most) to bring a word or the logo a few points past the band back inside it; a layout further out passes unchanged and the run lays it out again. The 9:16 is never extended instead (the 4:5 set whole in the clear band, its scene continued to the frame) nor set over its own blurred copy: rated in October 2026, the first showed the ad small inside a continued scene ("text super tiny") and the second "looks bad". The master shape is one decision in the code (`DEFAULT_MASTER_SHAPE`: `relayout`; the `9x16_band` and `4x5_adapt` shapes, kept for the bench, make the sizes by cutting or reframing the master, and the person rating them rejected every such way; a canvas from the retired band master, which extended a square master into the 9:16, is upgraded to the re-layout graph on its next run). Placing the logo file on every size instead of having the model draw it (`logo_composite` nodes) is a scaffold option, off until it is proven on the bench.
6349
+ - Readers go by these roles, never by node ids: the run review, `--fix`, `--use-variant`, `--regenerate static_variants`, and the dashboard. A static ad scaffolded before this graph (`ad_a`/`ad_b`/`ad_c`, `pick`, re-lays `ad_4x5`/`ad_9x16`, `fit_4x5`) is **upgraded in place** on its next `validate` or `run`: prompt.json, every asset slot, the model and the formats are kept, the render part becomes this graph, and the creative is not reported as changed by the upgrade. Its old runs stay readable.
6350
+ - Detected elements are **never slots to fill**: the source's people, products, animals and badges are its world. The original image stays on the canvas as the record of the source; when `prompt.json` keeps its concept (`concept.verdict` `use` or `adapt`) the first renders are shown it **for its structure only**, and otherwise it never reaches a render. Only the people cast from a company Avatar and the brand's logo become pictures on the canvas; everything else is described in words (a picture of ours is added through a reference's `image`). A **person** with no avatar needs no slot and no casting: the render draws an invented, realistic person from the element's description. With `--avatar <handle>` (for an actor who recurs across ads) the slot ingests the Avatar's identity sheet and `prompt.json` describes that person in the Avatar's own words. A reference's `avatar` names who was cast and draws no face on its own: its face reaches the render only with a picture behind it (the `--avatar` slot, or the identity sheet URL from `baker avatars get <handle>` as the reference's `image`).
5583
6351
 
5584
- **Automatic placement fan-out.** By default the canvas doesn't stop at one ratio: the hero renders once at the platform's primary ratio, then an `image_aspect_adapt` node recomposes it into that platform's full placement set, and the canvas `output` points at the adapted images. The placement set is derived from `--platform` (`meta` → 4:5 + 9:16, `google` → 1:1 + 4:5 + 16:9, `linkedin` → 1:1, `tiktok` → 9:16, `youtube` → 16:9); override it with `--placements <preset>` or disable it with `--placements none` for a single base ad. The hero ratio passes through the adapt node **free**; each other ratio is a billed recomposition — and because adaptation re-generates, every ratio should be eyeballed for text and brand-color fidelity before publish (`ad-formats.md` in the scaffold skill covers this).
6352
+ The canvas is validated before it is written. stdout returns `{ ok, canvas_path, prompt_path, models, aspect_ratio, placements, output_formats, stats, checklist }`; the **checklist** carries `summary` (what a run does: candidates, judge, type, logo, formats), `font`, `logo`, `in_scene_text`, `formats` and every real asset to drop in.
5585
6353
 
5586
- The checklist `note` reminds you to fill **logo** slots from the company's committed brand logo in `src/brand/logos/` (per `BRAND.md`) — **not** from `baker images logo <domain>`, whose Brandfetch CDN often serves an outdated mark (a common cause of an old logo shipping on a live ad).
6354
+ **Placement formats.** The canvas delivers every size where the ad runs, each at its exact size, collected under `final` with the format as its label. `--platforms meta,tiktok,google_display,…` (platforms or placement ids from the registry in `@baker/canvas-contract` `AD_PLACEMENTS`: meta, google_display, google_pmax, google_demandgen, google_search, youtube, linkedin, tiktok, x, reddit, pinterest, snapchat, chatgpt) delivers every size their placements require, each kept clear of its own placement's interface (a TikTok 9:16 of TikTok's buttons and caption), and lists fixed banners (`728x90`, …) in the report as `banners`, never made from the picture (a destination of banners alone is refused with `invalid_platforms` before anything bills). A 9:16 for another platform keeps clear of its interface even when `--target-adset` narrows Meta's placements. Without it the set is derived from `--platform` (`meta` → 4:5 + 9:16, `google` → 1:1 + 4:5 + 16:9, `linkedin` → 1:1, `tiktok` → 9:16, `youtube` → 16:9); override it with `--placements <preset>` or `--placements none` for a single format. The report's `aspect_ratio` is the shape the variants really render at (the model's nearest to the master's format), and `format_plan` names the master and how each size is made from it (`master`, `crop` with its rectangle, or `adapt`).
5587
6355
 
5588
- The generation prompt is **model-aware and typography-hardened** (a static ad is the only path that bakes text *into* the image — video frames are clean plates). It resolves the image model's profile, so for **gpt-image** the exclusions (no invented badges/watermark) are hoisted to the very *end* of the prompt where the model reads them as pure exclusions rather than composition, and it quotes every blueprint string with an instruction to render each **once, verbatim**, lock brand names letter-by-letter, and keep type crisp and legible — the fix for the #1 static-ad defect (garbled/duplicated/dropped copy). Run `baker canvas critique` on the scaffolded canvas for an advisory read on text-legibility risk, identity grounding, and brand-type fidelity before you spend.
6356
+ **Who sees the ad.** `--target-adset <id>` names the Meta ad set the ad will run in. Before anything is billed, the viewer its targeting reaches is read exactly as `baker ads meta adsets --audience` reads it (`temperature`, what they already `knows`, `placements`) and written to `_definition.md` as `targetAdSet` and `viewer` (an existing definition keeps every other field). The sizes, the brief and the prefilter keep clear of that viewer's placements (`staticSafeArea`; `automatic`, a bare `facebook`/`instagram` or no placements is Advantage+, Reels included), and every `baker canvas run` checks the ad against that viewer. With no ad account, give the viewer from the Knowledge: `--viewer-temperature cold|warm|hot [--viewer-knows brand,offer,product] [--viewer-placements <placement,…>]`, recorded with `source: knowledge`; next to `--target-adset` they correct the read, and its `reasons` say so. A bad value exits 2 (`invalid_viewer`) before any billed pass; an ad set that cannot be read never stops the scaffold (a hint says why, and the sizes keep clear of every placement). With neither flag, a `viewer` (or `targetAdSet`) already in `_definition.md` is used as it is. stdout carries `target_adset`, `viewer` and a `checklist.viewer` line.
6357
+
6358
+ **After a run** the CLI reads the finished ad back on what makes a static win, not a checklist of elements: every delivered image's pixel shape against its format; **every line of `prompt.json`'s copy (in-scene and designed alike) read back with its box by a box reader and compared** (a line more than 2% off, missing, or joined by words nobody asked for is blocking; case and punctuation fold, as does a capital I read as an l (one stroke in a sans-serif), accents do not; words the picture adds are blocking only when set at the copy's usual size or making a claim (a figure, a price, an offer), and otherwise a note, as a screen's code, a poster or a mug in the scene, except a price (a currency sign beside a figure) the copy does not hold, blocking at any size and blurred by the correction, the document it is on kept; every render is asked for no readable prices on a bill, quote, receipt, screen or price tag beyond the copy; a copy line is "on the ad twice" only when most of its words repeat, never a word or two echoed on a prop; on a native screen, which code typesets from the spec, a line read differently is the reader's error and only a missing one counts); **the ad's own copy and its logo measured against the format's safe area** (`staticSafeArea`, the one owner of those areas; `must-show.ts` decides what is measured: never what the scene prints, nor a copy line printed again in the scene while the line itself sits clear). On a 9:16, which runs in Stories and Reels alike, what Stories' header or bottom bar covers (and any other app's interface named) is said as "where Stories and Reels cover it" ("where Reels covers it" for an ad set that runs only in Reels) and blocks for the logo, the button, the headline, the figure and the offer, while a supporting line (body, caption, label, small print) there is a note; what only Reels' taller footer covers is one note, and only for the lead line, since Stories shows it and the 9:16 is laid out in Stories' band; words merely close to where an app draws are no finding. The steps that place a size clear of an interface (`static_safe_fit`, `static_place`) are told the same copy (`prompt.json`'s lines, their roles, and the brand's name and the logo's words, a wordmark read close to them counting as the logo) and move the ad for those alone, a supporting line included only where the frame cuts it; on a feed image such as a 4:5, only a design margin, so words close to the edge are a warning and only words the frame cuts are blocking); a copy line written over two lines (`"15%\noff"`) is matched part by part wherever the reader finds each; designed type in a colour far from the brand palette is a warning; supporting lines too small to read on a phone are one note per size; the main line set smaller than a supporting one is a note, the brand's wordmark never counted as part of a line that starts with its name; the logo set twice is a note, a far smaller mark printed on something in the scene (a hoodie, a bag) is not. When the copy names a real person ("Lautaro Suarez, CTO at Vibiz", "田中 美咲、Acme 創業者"; read by a text model in any language, cached by copy, `scripts/named-people-evals`), the run casts them as themselves before it renders (`baker avatars research --create`, their avatar's sheet added to `prompt.json` `references` as a `PERSON_n`), and a face shown as a named person the ad has no photo of is blocking. When the reader cannot answer, the vision pass transcribes the copy instead and judges the safe zones by eye. Then one vision pass for clipping, overlaps, artifacts, one dominant idea, the brand with the logo covered and the concept's mechanisms (each finding typed by the model with the item it answers, what it is about, what is wrong and the edges that cut it, which the grading and the automatic correction read instead of its words), with every format shown a second time at phone-feed thumbnail size (240px wide, same call): a lead message unreadable at that size is blocking. A missing logo, CTA or copy is never a finding; a logo that is drawn and broken is. stdout carries `verdict` (`ready` | `ready_with_notes` | `needs_fixes` | `not_reviewed`), `judge` (the per-candidate scores) and hints that end with what the run made; the findings are stored on the run as its `review`. `needs_fixes` means the ad is not done even though every node completed, and the run is recorded with `presentable: false` so it is never shown as a version; every fix is an edit to `prompt.json` and a re-run (wrong words: the lines in `copy`; wrong picture: the `scene`; a bad roll: `--regenerate static_variants`; one size wrong while the chosen picture is right: `--regenerate adapt_<size>`, which re-lays it out, named in the hints); a blocking defect is first corrected by the run itself (`--autofix`, above). The checks rank the variants before the pick and the judge only breaks an exact tie, comparing pairwise against the compiled brief, knowing the brand and the viewer; every variant stays on offer to the person in Baker. Within one chat turn an ad gets two runs you start (each with its automatic corrections) and about 12 minutes in all: a correction round that would take it past that is not started (`[autofix] not started`), the version shown as it stands, and no render starts once it is spent; once the ad is `ready` or `ready_with_notes` only a correction of it starts (`--fix`, `--regenerate adapt_<size>`, or an edit of its words or colours), within the same two runs, and anything else is refused (`CONFLICT`, exit 2) with what to show instead; the person's next message starts a new turn (`turn-renders.ts`). When a defect still stands after the automatic rounds, the hints say to fix it with a `--fix`, then by changing the spec or the approach, and if it still cannot be fixed to show the best version and name what is left in one plain sentence, without a question.
6359
+
6360
+ The render prompt is **model-aware**: each model's profile (`src/engine/lib/prompt-profiles.ts`) sets its shape, word budget, picture order and request params. gpt-image gets short labelled sections with the constraints last; Gemini gets sentences that give the reason and a positive closing list of the only words allowed. A profile change, or a new model on the router (`src/engine/lib/image-model-router.ts`), is re-run on the static bench (`scripts/creative-evals`, `--job static`) before it ships. Run `baker canvas critique` on the scaffolded canvas for an advisory read before you spend.
6361
+
6362
+ **A reference from the winners library goes in by id.** `--source-ad-id <ad_id> --source-platform meta|linkedin|tiktok|google [--source-advertiser-id <id>]` (the ids beside the ad in `baker winning-ads search`) fetches the library's structured read of that ad, the same one `baker winning-ads content <ad_id> --full` returns, and writes it into `prompt.json` as a `reference` block: `{ ad_id, platform, advertiser, advertiser_id, format_pattern, layout: { composition, focal_subject, aspect_ratio, background, palette_mood, colors, production, aesthetic, cta_in_image, markers }, message: { mechanism, awareness, sophistication, angle, hook_archetype, pain_points, desires, value_props, objections_handled, proof, risk_reversal }, offer: { offer_type, pricing_model, entry_price, no_credit_card, guarantee, guarantee_days, urgency, bonus }, text_blocks: [{ text, role, position }], copy: { primary_text, headline, cta } }` (filled fields only). The describe pass still reads the pixels but is told that read as grounding, so the two cannot disagree on the container or the mechanism; when it names no `winning_mechanisms`, the library's container + mechanism seeds the list. The source ids (`sourceAdId`, `sourcePlatform`, `sourceAdvertiserId`, and `sourceAdvertiser` from the library when `--advertiser` is absent) are written to `_definition.md` and synced to the creative at scaffold time. A library read that fails warns, records the ids anyway and scaffolds from the picture alone (`hints`: `REFERENCE NOT READ`); with no picture either, the command exits 2 (`source_unavailable`). `baker canvas sync-definition` re-reads the three ids from `_definition.md` on every sync.
5589
6363
 
5590
6364
  ```bash
6365
+ baker canvas scaffold-static-ad --source-ad-id adg_123 --source-platform meta --slug spring-offer --platform meta --target-adset 120249211706990208
6366
+ # → the library's picture and read, made for the viewer that ad set reaches; writes src/creatives/spring-offer/{spring-offer.canvas.json, prompt.json, _definition.md}
5591
6367
  baker canvas scaffold-static-ad ./reference-ad.png --context "competitor ad for <brand>, <category>, <market>"
5592
6368
  # → writes ./static-ad.canvas.json + ./prompt.json
5593
6369
  # edit prompt.json into your ad + replace each [TODO] ingest path with a real file, then:
@@ -5597,45 +6373,44 @@ baker canvas run ./static-ad.canvas.json
5597
6373
 
5598
6374
  | Flag | Default | Effect |
5599
6375
  |---|---|---|
5600
- | `--context <text>` | — | Known provenance (advertiser, category, market) to ground the describe. |
6376
+ | `--source-ad-id <ad_id>` | — | The winners-library ad this creative adapts. Fetches its structured read into `prompt.json` `reference`, grounds the describe pass with it, and records the source on the creative. Makes `<image>` optional. |
6377
+ | `--source-platform <platform>` | `meta` | Platform the library ad is on: `meta`\|`linkedin`\|`tiktok`. Another value exits 2 (`invalid_source_platform`). |
6378
+ | `--source-advertiser-id <id>` | the library's | The reference's library advertiser id, recorded as `sourceAdvertiserId`. |
6379
+ | `--context <text>` | — | Known provenance (advertiser, category, market) to ground the describe. Appended to the library's read when `--source-ad-id` is given. |
5601
6380
  | `--out <path>` | `<image-dir>/static-ad.canvas.json` (cwd when `<image>` is a URL) | Where to write the canvas (`prompt.json` is written alongside). |
5602
- | `--slug <slug>` | — | Creative slug (lowercase kebab): writes the canvas to `src/creatives/<slug>/<slug>.canvas.json` — the repo convention that attaches every run to the creative's dashboard generation history. `--out` wins over `--slug`. With a slug, the reference image is **downloaded into `src/creatives/<slug>/references/` and normalized to a model-safe format** (SVG/AVIF/HEIC/… → PNG), named from the actual bytes (not the URL string) so a presigned/extensionless URL never lands as PNG-bytes-in-`.jpg` — the canvas ingests that committed, portable path instead of the expiring URL. |
6381
+ | `--slug <slug>` | — | Creative slug (lowercase kebab): writes the canvas to `src/creatives/<slug>/<slug>.canvas.json` — the repo convention that attaches every run to the creative's dashboard generation history. `--out` wins over `--slug`. With a slug, the reference image is localized to durable storage and normalized to a model-safe format (SVG/AVIF/HEIC/… → PNG), so the canvas ingests a stable URL instead of an expiring one. |
5603
6382
  | `--describe-model <id>` | registry default (`~google/gemini-pro-latest`) | Override the `image_describe` model. |
5604
6383
  | `--select-model <id>` | registry default (`~google/gemini-flash-latest`) | Override the element-selection `text_generate` model. |
5605
- | `--layout-model <id>` | registry default (`~google/gemini-flash-latest`) | Override the global-layout `text_generate` model. |
5606
- | `--gen-model <id>` | registry default (`openai/gpt-image-2`) | Override the `image_generate` model. |
5607
- | `--platform <channel>` | `meta` | `_definition.md` channel **and** the default placement set the hero fans out to (`meta`\|`google`\|`linkedin`\|`tiktok`\|`youtube`\|`x`\|`other`; `x`/`other` fan out to nothing). |
5608
- | `--placements <preset\|none>` | derived from `--platform` | Override the fan-out set: `meta_feed`\|`meta_feed_stories`\|`meta_all`\|`linkedin`\|`google_demandgen`\|`tiktok`\|`youtube`, or `none` for a single base ad. |
5609
- | `--aspect <ratio>` | inferred from the image, else `9:16` | Force the hero's aspect ratio (clamped into the placement set so it stays a free pass-through). |
5610
- | `--skip-font` | off | Skip the brand-font → type-specimen slot. |
5611
- | `--skip-actor-sheets` | off | Ground each animal hero on its lone dropped photo instead of a generated multi-view reference sheet. |
6384
+ | `--gen-model <id>` | the router's pick for the concept | Override the render model. An unknown model id in any `--*-model` flag fails the command (`unknown_model`) before anything is billed. |
6385
+ | `--takes <n>` | `3` | Variants of the finished ad rendered (one request each) and judged (1-3); `1` skips the judge. |
6386
+ | `--platform <channel>` | `meta` | `_definition.md` channel **and** the default placement set (`meta`\|`google`\|`linkedin`\|`tiktok`\|`youtube`\|`x`\|`other`; `x`/`other` deliver one format). |
6387
+ | `--platforms <list>` | none | Where the ad runs, comma-separated platforms or placement ids: every size they require, each kept clear of its own interface. Wins over `--placements`. Banners alone are refused. |
6388
+ | `--placements <preset\|none>` | derived from `--platform` | An older fixed set: `meta_feed`\|`meta_feed_stories`\|`meta_all`\|`linkedin`\|`google_demandgen`\|`tiktok`\|`youtube`, or `none` for a single base ad. |
6389
+ | `--aspect <ratio>` | inferred from the image, else `9:16` | Force the lead format (it leads the placement set when the set has it). |
5612
6390
  | `--avatar <handles>` | — | Cast company avatars as the image's people, comma-separated in the order they were detected (`marta` or `marta,joan`). Each person slot grounds on that avatar's identity sheet and takes its subject description. Checked before any billed pass; an unknown, not-ready or sheetless avatar fails with code `avatar`. A handle with no person to play is reported in `hints` as `NOT CAST`. |
5613
6391
 
5614
- Scaffolding runs (and bills) the two vision passes; **running** the result generates a billed image. `baker canvas validate` does not check that the `[TODO]` paths exist — supply the real files before `run`.
6392
+ Scaffolding runs (and bills) the two vision passes; **running** the result generates billed images. `baker canvas validate` does not check that the `[TODO]` paths exist — supply the real files before `run`.
5615
6393
 
5616
- **Resuming an interrupted run.** A long `baker canvas run` (multi-clip video) that is killed mid-render — session end, sandbox pause — leaves a marker under the outputs dir. The next `baker canvas run` of the same canvas automatically **resumes** that run: it reuses the run id so still-running billed jobs re-attach instead of being abandoned and re-billed, and completed nodes come from the cache. Resume also works from a **different workspace or a fresh sandbox**: when no local marker survives, the run history is consulted and an interrupted (or stale) run of the exact same canvas is adopted automatically. Ctrl-C / SIGTERM aborts gracefully — no new nodes dispatch, a resumable snapshot is flushed, and the marker survives. A clean completion (or a handled failure) clears the marker, so a normal re-run starts a fresh generation. Force a new run with `--fresh`, or pin a specific run with `--run-id <id>` (also the escape hatch to adopt a run that is reported as concurrently live). Independent same-layer nodes (e.g. video clips) fan out in parallel up to `--parallel`/`--concurrency` (default 8; env `BAKER_CANVAS_CONCURRENCY`).
6394
+ Size adaptations move and proportionally scale the existing composition first, keeping its background treatment, including intentional flat colour. Generation, adaptation, correction and series briefs preserve plausible anatomy, device proportions, scale, perspective and support, and reflow text while preserving letter shapes. Recognizable app controls remain native; marketing overlays stay visibly separate. Shared guidance records applicable intent and constraints briefly in `_definition.md`, then carries them into `prompt.json` (Markdown is not a render input). Direct briefs need no invented winner or fixed template; reference adaptations reason through the mechanism, transfer hypothesis and what must survive. `creatives make` returns this preparation hint before rendering. Choose the represented role and inspect existing Avatars before creating a new actor; a real founder or customer keeps their verified identity.
5617
6395
 
5618
- #### `baker canvas rerun <slug> [flags]`
6396
+ During continuations, the accepted image's screen content takes priority over geometry references unless explicitly changed. Laptop scenes receive guidance for a real laptop and consistent display, keyboard and hinge perspective.
5619
6397
 
5620
- Re-run a creative's latest recorded canvas **from run history** — no local files needed. Every `canvas run` of a creative uploads a portable snapshot (the canvas JSON plus the local files it ingests: prompt blueprints, composition dirs, reference images) to durable storage and records it on the run. `rerun` restores those files into `src/creatives/<slug>/` (sha-verified; files already matching are left untouched) and then executes the normal run flow — so an interrupted run **resumes** (in-flight jobs re-attach) and a completed one re-renders from the cache at zero credits.
6398
+ #### `baker canvas rerun <slug> [flags]` (deprecated)
5621
6399
 
5622
- | Flag | Default | Meaning |
5623
- | --- | --- | --- |
5624
- | `--force-remote` | off | Overwrite local files whose content differs from the snapshot (otherwise a conflict aborts with the differing paths). |
5625
- | `--fresh` | off | Start a new run id instead of resuming an interrupted one. |
5626
- | `--regenerate <ids>` | — | Same as `canvas run --regenerate`. |
5627
- | `--concurrency <n>` | — | Same as `canvas run --concurrency`. |
6400
+ Now `baker canvas run --slug <slug>`; `rerun` stays as a hidden alias for one release and prints a deprecation note. Every `canvas run` of a creative records a portable snapshot (the canvas JSON plus the local files it ingests: prompt blueprints, composition dirs, reference images) in durable storage, so `run --slug` can rebuild `src/creatives/<slug>/` in a workspace that never had it. A snapshot whose files are exactly the last one uploaded for the creative is reused without a request, and a file the store already holds is not uploaded again. Files the snapshot could not include (missing at run time, or oversized) are listed as warnings.
5628
6401
 
5629
6402
  ```bash
5630
- baker canvas rerun spring-offer
5631
- baker canvas rerun spring-offer --force-remote --regenerate gen
6403
+ baker canvas run --slug spring-offer
6404
+ baker canvas run --slug spring-offer --force-remote --regenerate gen
5632
6405
  ```
5633
6406
 
5634
- Use it when a creative was built in another conversation (or its sandbox is gone) and you need to continue or re-render it here. Files the snapshot could not include (missing at run time, or oversized) are listed as warnings — supply those locally only if the run actually needs to regenerate the nodes that read them.
6407
+ #### `baker canvas stale`
6408
+
6409
+ Lists the creatives edited since their last render in this workspace, each with the command to re-render it: `{ ok, data: { stale: [{ slug, canvasPath, renderedRunId, sourceSha }] }, hints? }`. "Edited" is one content fingerprint of what the models would be sent: the canvas as it renders (after the copy, logo and scene projections), `prompt.json` without its bookkeeping (`reference`, `source_context`, `notes`), every `scenes/sNN.json`, every local composition directory the canvas renders, and the `formats` in `_definition.md`, so a change counts however it was made and reformatting a JSON file is not a change. The fingerprint is prefixed `v2:`; a render fingerprinted by the older rule (a bare sha) is never reported stale, so upgrading the CLI re-renders nothing. A completed `run` records the fingerprint it drew in `canvas/.rendered/<slug>.json`, and a render attempt of the current source is not asked for again until the source moves. Creatives never rendered in this workspace, and ones with an unfilled `[TODO]` asset slot, are not listed. Read-only, offline, free. The scaffold's turn-end reminder and its `validate-creative-renders` check read this list (at Publish it is a warning: Publish ships the last version the person was shown and never renders); the same fingerprint is what `sync-definition` and `run` post as `sourceSha`.
5635
6410
 
5636
6411
  #### `baker canvas inspect <run_id> [--thumbnails]`
5637
6412
 
5638
- One-page summary of a completed run: per-node duration + cache status, list of files in the run dir, optional video thumbnails (start/middle/end frames extracted via ffmpeg).
6413
+ One-page summary of a run: per-node duration + cache status, list of files in the run dir, optional video thumbnails (start/middle/end frames extracted via ffmpeg). When the run has no folder in this workspace (an older version, or a run from another chat), it is read from the company's run history instead (`source: "run_history"`): status, stats, per-node results and the durable URL of every output.
5639
6414
 
5640
6415
  ```bash
5641
6416
  baker canvas inspect r_01JXYZ...
@@ -5697,14 +6472,20 @@ baker canvas catalog --category audio --full
5697
6472
  // Path source — an absolute path is used as-is, and so resolves only on the machine that wrote it.
5698
6473
  // Keep canvases portable: creative canvases are validated for this (`absolute-reference`).
5699
6474
  { "id": "src", "type": "ingest", "params": { "source": "path", "path": "/abs/work/clip.mp4", "expect": "video" } }
6475
+
6476
+ // URL source — an earlier output, by its durable canvas-assets URL: the same bytes and sha back
6477
+ { "id": "src", "type": "ingest", "params": { "source": "url", "url": "https://media.withbaker.com/canvas-assets/5b/5b50…c672.mp3", "expect": "audio" } }
5700
6478
  ```
5701
6479
 
6480
+ **Never point `path` into `canvas/`.** `canvas/.cache/` and `canvas/r_<id>/` are the engine's own working folder and are never committed, so a canvas that reads from them renders only where it was written. `canvas validate` and `canvas run` rewrite such a path to the durable URL of the same bytes (reported under `normalized_paths`), keeping the file first if the store does not hold it yet.
6481
+
5702
6482
  **Strategy resolution** (deterministic from `source` + `expect` + URL extension for text):
5703
6483
 
5704
6484
  | `source` | `expect` | URL / path | Strategy |
5705
6485
  |----------|---------------|----------------------------------|-----------------|
5706
6486
  | path | any | any | `local_file` — read from disk, sniff mime, upload to R2 via presign |
5707
- | url | video / audio | any | `yt_dlp` (works on platform URLs *and* direct file URLs via the generic extractor) |
6487
+ | url | any | `…/canvas-assets/<xx>/<sha>.<ext>` | `canvas_asset` — the stored asset, verbatim, its sha checked |
6488
+ | url | video / audio | any other | `yt_dlp` (works on platform URLs *and* direct file URLs via the generic extractor; verifies TLS against the system bundle) |
5708
6489
  | url | image / json / font | any | `direct_fetch` (HTTP GET) |
5709
6490
  | url | text | ends in `.txt` or `.md` | `direct_fetch` |
5710
6491
  | url | text | anything else (HTML, PDF, …) | `firecrawl` `/v2/scrape` (extracted markdown — HTML/PDF made readable) |
@@ -6105,23 +6886,95 @@ import {
6105
6886
 
6106
6887
  ## Creatives
6107
6888
 
6108
- Publish an approved canvas render as a first-class Baker creative. The image uploads to the Baker image library (tagged `creative`), a creative record is created/updated, and the command prints the creative reference JSON the dashboard renders in chat.
6889
+ Save a finished canvas render as a Baker creative: a static image (PNG, JPG, WebP) or a finished video (MP4, MOV, WebM). Choose and save the best finished version without waiting for the user to choose, including in the turn that rendered it. Saving pins the version as the agent's choice without recording user approval or putting an ad live. An image uploads to the Baker image library (tagged `creative`); a video uploads to the Baker video library and is saved once it plays (bounded at 3 minutes). A creative record is created/updated, and the command prints the creative reference JSON the dashboard renders in chat.
6109
6890
 
6110
6891
  ```bash
6111
- baker creatives publish ./canvas/<run_id>/<final>.png --title "Spring Offer 4x5" \
6112
- --slug spring-offer-4x5 --run-id r_01JXYZ... \
6892
+ baker creatives publish --title "Spring Offer 4x5" \
6893
+ --slug spring-offer-4x5 --run-id r_01JXYZ... --output 4:5 \
6113
6894
  --source-reference-url "https://www.facebook.com/ads/library/?id=..."
6895
+ # or from a local file
6896
+ baker creatives publish ./canvas/<run_id>/<final>.png --title "Spring Offer 4x5" --slug spring-offer-4x5 --run-id r_01JXYZ...
6897
+ # a finished video render (needs the file and --slug)
6898
+ baker creatives publish ./canvas/<run_id>/final.mp4 --title "Spring Offer Reel" \
6899
+ --slug spring-offer-reel --run-id r_01JXYZ...
6114
6900
  ```
6115
6901
 
6116
6902
  | Flag | Effect |
6117
6903
  |---|---|
6904
+ | `<file>` | The image or video. For an image it is optional with `--run-id`: the run's own saved output (from its run record, sha-verified) is used instead, so a workspace that never had the run's folder can publish it. |
6118
6905
  | `--title <text>` | Required. Human title for the creative. |
6119
- | `--slug <slug>` | Creative slug (`src/creatives/<slug>/`) — attaches the image to that creative's row, marks it `published`. |
6120
- | `--run-id <r_…>` | Pins the approved generation from the creative's run history as the published one. |
6906
+ | `--slug <slug>` | Creative slug (`src/creatives/<slug>/`): attaches the image or video to that creative's row, marks it `published`. Required for a video. With `--run-id` an image publish also sets `status: published` in the creative's `_definition.md` (`definitionStatus` in the output). |
6907
+ | `--run-id <r_…>` | Pins the finished generation from the creative's run history as the saved one. |
6908
+ | `--output <label\|slot\|n>` | With `--run-id` and no file: which of the run's images to save when it made several (a label such as `4:5`, a slot, or a 1-based number). |
6121
6909
  | `--source-reference-url <url>` | Original reference ad URL, recorded on the creative. |
6122
- | `--context <text>` | Optional describe context for the uploaded image asset. |
6910
+ | `--context <text>` | Optional describe context for the uploaded image or video. |
6911
+
6912
+ An image prints `{ ok, data: { creativeId, imageId, reference } }`. A video prints `{ ok, data: { creativeId, videoId, reference }, hints }`, where `reference.asset.kind` is `video` (with `videoId`, `muxPlaybackId`, the poster as `thumbnailUrl`, size and duration) and `hints` names the next step to put it in a Meta ad: `baker ads meta media upload --kind video --video-id <videoId>`.
6913
+
6914
+ Without `--slug` an image behaves as before (one creative record per published image). With `--slug` it upserts the repo-convention row — the same one the dashboard's Creatives tab and the `src/creatives/{slug}/` folder describe — so publish, repo sync, and run history all land on a single record regardless of order.
6123
6915
 
6124
- Without `--slug` the command behaves as before (one creative record per published image). With `--slug` it upserts the repo-convention row — the same one the dashboard's Creatives tab and the `src/creatives/{slug}/` folder describe — so publish, repo sync, and run history all land on a single record regardless of order.
6916
+ ### `baker creatives performance`
6917
+
6918
+ How each published creative is doing in the live Meta ads that show it. A creative is linked to an ad when a chat's publish uploads its image or video to Meta and creates an ad with it (or swaps an existing ad onto it); the results are read live from Meta.
6919
+
6920
+ ```bash
6921
+ baker creatives performance # every creative in an ad, last 30 days
6922
+ baker creatives performance --slug spring-offer # one creative
6923
+ baker creatives performance --days 14 --full # + ids, last-7-day figures, one row per ad
6924
+ ```
6925
+
6926
+ | Flag | Effect |
6927
+ |---|---|
6928
+ | `--slug <slug>` | Only this creative. |
6929
+ | `--days <n>` | Window ending today, 1-90 (default 30). |
6930
+ | `--full` | Adds creative, image and video ids, result type, days live, last-7-day CTR and cost per result, a video's watch funnel (`video`: 3-second views, ThruPlays, 25/50/75/100% watched), and one row per ad. |
6931
+
6932
+ Each creative reports `spend`, `impressions`, `ctr` (percent), `cpc`, `results`, `costPerResult`, `frequency`, a `fatigue` verdict (`fresh`, `fading`, `fatigued`, `dead`, `too_early`, `no_delivery`) with its reason, and `definition`: the angle, hook, mechanism, dominantSlot, persona, awareness, hypothesis and reference fields read from the local `src/creatives/<slug>/_definition.md`. A video creative also reports `hookRate` (percent of impressions that watched 3 seconds) and `holdRate` (percent of those that reached a ThruPlay); a static one carries neither. `hints` name worn-out creatives, videos whose hook rate is under 25% (the first 3 seconds need new hooks) or whose hold rate is under 30% behind a working hook (the body loses people), the angle, mechanism and dominant slot with the lowest cost per result (each ranked once two or more values have results), and unreadable ad accounts.
6933
+
6934
+ ### `baker creatives feedback [--slug <slug>] [--full]`
6935
+
6936
+ What people said about creative versions in their chats (an approval, or a rejection with reasons and a note; keeping a version counts as an approval). Compact by default: approved and rejected counts, the top rejection reasons, what approved versions share (format, platform, option) and the 8 latest verdicts, plus `hints[]` saying what to do differently. `--slug` narrows to one creative; `--full` returns every verdict (up to 200) with whole notes.
6937
+
6938
+ ```bash
6939
+ baker creatives feedback
6940
+ baker creatives feedback --slug spring-offer --full
6941
+ ```
6942
+
6943
+ Each entry carries its `source`: `review` (the review buttons Baker had before, kept on older verdicts), `selection` (choosing or saving the version) or `chat` (what the person wrote in a chat, recorded with `feedback record`).
6944
+
6945
+ When the person asks in the chat for another variant of a version, make every size from it with `baker canvas run --slug <slug> --use-variant <sha256> --from <runId>`; Baker has no control to choose one.
6946
+
6947
+ ### `baker creatives feedback record`
6948
+
6949
+ Record what the person said about one version in a chat: an approval, or a rejection with reasons and their own words. It is stored as the verdict of the person who wrote the chat's latest message (`recordedFor: "sender"`), or of the chat itself when nobody has written in it (`"chat"`). One verdict per person per version or option: a later one replaces theirs. Must run from a chat.
6950
+
6951
+ ```bash
6952
+ baker creatives feedback record --slug spring-offer --verdict rejected \
6953
+ --reasons text_mistakes,weak_hook --note "the price is 49 not 59"
6954
+ baker creatives feedback record --slug spring-offer --run r_01K… --option 2 --verdict approved
6955
+ ```
6956
+
6957
+ | Flag | Effect |
6958
+ |---|---|
6959
+ | `--slug <slug>` | Required. The creative (`src/creatives/<slug>/`). |
6960
+ | `--run <r_…>` | The version. Left out, the one the creative shows now (its last render in this workspace). |
6961
+ | `--verdict approved\|rejected` | Required. |
6962
+ | `--option <o>` | One option of a version that made several: its label, its number (`2`) or its slot. |
6963
+ | `--reasons <a,b>` | Comma-separated: `text_mistakes`, `logo_or_brand_wrong`, `weak_hook`, `wrong_message`, `looks_fake`, `not_our_style`, `voice_or_sound`, `pacing`, `other`. The two video reasons: `voice_or_sound` (the voiceover, music or mix) and `pacing` (too slow, too fast, the hook lands late). A rejection needs reasons or a note. |
6964
+ | `--note <text>` | The person's own words (stored up to 1000 characters). |
6965
+
6966
+ After a rejection, `hints[]` restates what to change, starting from that version.
6967
+
6968
+ ### `baker creatives select`
6969
+
6970
+ Record the user's choice of a creative version, or only some of its options, the way choosing it in Baker does: the creative shows that version as the chosen one, and the choice records the chat sender's approval (`source: "selection"`). Naming options replaces the earlier pick. It does not add the image to the library; that is `baker creatives publish`, which pins the agent's saved version without recording user approval. Use `select` only for a choice the user actually made; autonomous saving uses `publish`.
6971
+
6972
+ ```bash
6973
+ baker creatives select --slug spring-offer # the version it shows now
6974
+ baker creatives select --slug spring-offer --run r_01K… --options "beach,2"
6975
+ ```
6976
+
6977
+ Prints `{ ok, data: { creative, runId, kept?, options, approvalRecorded }, hints }`, where `options` lists every option the version offers.
6125
6978
 
6126
6979
  ## Landing Quality (`baker landing`)
6127
6980
 
@@ -6138,7 +6991,9 @@ baker landing inspiration code <section id> # the standalone bundle
6138
6991
  baker landing inspiration page <source id> # a whole page as a section sequence
6139
6992
  baker landing inspiration sequences "b2b saas pricing page" --scope all # what follows what, across many pages
6140
6993
  baker landing inspiration add https://linear.app --note "client likes this density"
6994
+ baker landing inspiration add https://linear.app --refresh # read a saved page again — the site changed
6141
6995
  baker landing inspiration favorites # what this company has saved
6996
+ baker landing inspiration retry-failed # study again every saved page that failed
6142
6997
  baker landing inspiration scrape <url> --out .baker/<dir> # capture any page now, synchronously
6143
6998
  ```
6144
6999
 
@@ -6152,6 +7007,7 @@ baker landing inspiration scrape <url> --out .baker/<dir> # capture any page n
6152
7007
  - **Compact by default, `--full` when you have chosen something.** Every list command returns what you need to *pick* a row; the 600-character "why it works" paragraph, the classification facets and the markup ride behind `--full` on `search`, `view`, `page`, `favorites` and `code`. A default `favorites` used to cost more than a default search for a question — "what does this client keep saving?" — that the facets alone answer.
6153
7008
  - **`sequences` answers the ordering question search structurally can't.** Search ranks individual sections and caps each site at three, so a page's top-to-bottom order never survives its results, and `page` recovers exactly one page's order. `sequences` counts adjacency across many pages at once and returns `openers`, `transitions` (`{ from, to, count }`, most frequent first) and `closers`, filtered by `--scope --domain --section-type --limit`. It returns page **ids only** — `page` stays the one way to read a single page — and always reports `pages_considered` alongside `pages_returned`, because a frequency over an unstated denominator is not evidence. `--full` counts over every matching page and prints the whole table.
6154
7009
  - **`page_id` on every search row** is what `page <id>` takes, so "how does this page sequence its sections?" is reachable from a result rather than only right after `add`. `favorites --limit` is capped at 100.
7010
+ - **Pages are re-read, on request and on a schedule.** A page is a copy taken when it was read, so `add --refresh` reads it again when the live site has changed (refused while a read is under way or within 10 minutes of the last one), and the library re-reads pages older than 30 days on its own, a small batch a day. A re-read that fails before it starts replacing the copy keeps the previous one and is reported as `refresh_failed_at`. `page` reports `read_at` and hints at `--refresh` (with the address the page is filed under) when the copy is over 30 days old.
6155
7011
  - **`add` studies in the background; `scrape` returns with the page on disk.** `add` grows the shared library and takes minutes, so it can never answer "build our page like this one" within the same turn. `scrape` runs the identical capture locally and blocks until it finishes, writing section screenshots, standalone markup, a whole-page reproduction and `report.html`. `--no-motion` is the single biggest lever on runtime; `--no-mobile`, `--no-code` and `--no-report` skip further passes.
6156
7012
  - **Filming is the expensive pass, and it runs three sections at a time.** Every moving section is filmed in its own fresh page load — the only way to catch an entrance animation before it fires — so a page with ten moving sections pays for ten full loads. Measured on one heavy page, filming was 74% of the capture. The takes are independent, so they overlap; a page that cannot be captured inside the library's budget now says so instead of timing out silently.
6157
7013
  - **A page we can't open says why.** A dead certificate, an address that doesn't resolve, a site that won't answer or one that is simply too slow each produce their own plain-language reason rather than a browser error.
@@ -6178,9 +7034,24 @@ baker landing variant oferta --fork Hero.astro,Proof.astro --because "…" --cha
6178
7034
  - **Errors carry the retry.** `NOT_FOUND` returns `availableSlugs` (wrong page) or `availableComponents` (wrong `--fork`); `INVALID_SLUG` fires when the slug is not a page, or is already a variant of one. Omitting `--fork` succeeds with a hint saying the page currently renders exactly the control, so a test between them cannot conclude.
6179
7035
  - **Run `baker experiment plan` first.** Most pages never get the traffic to settle most questions.
6180
7036
 
7037
+ ### `baker landing export`
7038
+
7039
+ A download of the company's **published** landings as a ZIP of plain HTML — the same pack Landings → Export makes. It reads what is live, so it starts at once; landings changed in the chat and not yet published are not in it.
7040
+
7041
+ ```bash
7042
+ baker landing export start # → { exportId }, returns at once
7043
+ baker landing export start --profile '{"htmlOutput":"fragment"}' # download format for this export only (JSON or @file)
7044
+ baker landing export status <id> # progress; downloadUrl once ready
7045
+ baker landing export list # the last 10 downloads
7046
+ ```
7047
+
7048
+ - **No password flag.** A protected ZIP is made from the dashboard — secrets never pass through the agent.
7049
+ - **Listed on the chat as already applied**, with a download button once ready.
7050
+ - `--profile` fields sit on top of the company's saved download format; a field that cannot be built is refused by name (`BAD_REQUEST`).
7051
+
6181
7052
  ### `baker landing critique`
6182
7053
 
6183
- Deterministic design-quality critic for landing pages — an offline, no-LLM detector for the well-known "AI slop" tells (gradient text, overused fonts like Inter, side-tab colored borders, cream/parchment grounds, purple-on-heading palettes, buzzword copy, broken images, and more). It scores a landing's `.astro` source and returns findings tiered **block / warn / advisory**.
7054
+ Design-quality critic for landing pages. The design rules are code over the landing's `.astro` source (gradient text, overused fonts like Inter, side-tab colored borders, cream/parchment grounds, purple-on-heading palettes, broken images, and more); the copy rules read what the page says, in whatever language it is written, through the decision model (Jev): buzzwords, AI writing tells, agent washing and positioning integrity. Each sentence is asked once, in one fanned-out set of requests (about half a second and a fraction of a cent for a whole page, billed with the workspace's next run), and the answers are kept in `.cache/landing-critique/`, so unchanged copy scores the same on every run. It returns findings tiered **block / warn / advisory**.
6184
7055
 
6185
7056
  ```bash
6186
7057
  baker landing critique spring-offer # score src/pages/spring-offer/
@@ -6190,14 +7061,15 @@ baker landing critique spring-offer summer-offer claude # score three landings
6190
7061
 
6191
7062
  - **Takes several slugs at once.** `data.landings` is always an array with one entry per slug (`{ slug, overall, counts, dimensions, findings }`). For a single slug those fields are also spread at the top of `data`, so existing one-landing readers are unaffected. A wrong slug anywhere in the batch fails the whole call before any landing is scored.
6192
7063
  - **Advisory by design — findings never fail it.** A finding is guidance, not a gate; the command errors (exit 2) only on a wrong slug (`NOT_FOUND` / `INVALID_SLUG`, with `error.fix.availableSlugs`).
6193
- - **The brand is the allowlist.** A font, hue, or ground the client's brand commits is a decision, not a tell — matched values downgrade to advisory `brandDrift` so the critic never fights a real brand. The allowlist reads `src/brand/tokens.json` (the canonical machine-readable brand token file) when present; workspaces that predate it fall back to scanning the `@theme` block in `src/styles/global.css` (hex or oklch) plus `src/brand/BRAND.md` prose.
7064
+ - **The copy is never judged on a guess.** When the decision model cannot answer (no key, a timeout), no copy rule reports anything — a concession included — and a hint says the copy went unchecked and to re-run.
7065
+ - **The brand is the allowlist.** A font, hue, or ground the client's brand commits is a decision, not a tell — matched values downgrade to advisory `brandDrift` so the critic never fights a real brand. The allowlist reads `src/brand/tokens.json` (the canonical machine-readable brand token file) when present; workspaces that predate it fall back to scanning the `@theme` block in `src/styles/global.css` (hex or oklch) plus the faces and colours `src/brand/BRAND.md` says the brand uses, read in any language by the brand reader — a face or colour BRAND.md bans ("nunca uses Inter") is never allowlisted.
6194
7066
  - **Records a freshness snapshot** to `.cache/landing-critique/<slug>.json` (the source hash it scored). The publish quality gate reads it to enforce that every changed landing was critiqued and ships no block-tier tell. Re-run after edits so the snapshot stays fresh.
6195
7067
 
6196
- - **AI writing tells are scored in the `copy` family, in English and Spanish.** Beyond buzzwords and em-dash density, the critic detects the constructions models over-produce: the negation pivot ("it's not just X, it's Y" / "no se trata solo de X, sino de Y"), fake-candid openers ("Honestly?", "La verdad es que"), participles hung off a comma (", ensuring…", ", permitiendo…"), copula avoidance ("serves as", "cuenta con"), vague attributions, signposting, filler, hollow closers, false ranges, staccato runs, Title Case headings, and chatbot framing that leaked into the page. Most need a **cluster** (≥2 on the page) before they fire — the source guidance is explicit that isolated tells prove nothing — with two deliberate exceptions: chatbot framing fires at one, and curly quotes are not detected at all, since every editor inserts them automatically. Nothing in this family is block-tier, so it never fails the publish gate. Pattern set adapted from Wikipedia's "Signs of AI writing" (WikiProject AI Cleanup).
7068
+ - **AI writing tells are scored in the `copy` family, in any language.** Beyond buzzwords and em-dash density, the critic detects the constructions models over-produce: the negation pivot ("it's not just X, it's Y", "Not a tool. A partner."), fake-candid openers ("Honestly?", "La verdad es que"), participles hung off a comma (", ensuring…", ", permitiendo…"), copula avoidance ("serves as", "cuenta con"), vague attributions, signposting, filler, hollow closers, aphorism formulas, false ranges, and chatbot framing that leaked into the page — each asked of every sentence; staccato runs and Title Case headings are measured in code. Most need a **cluster** (≥2 on the page) before they fire — the source guidance is explicit that isolated tells prove nothing — with two deliberate exceptions: chatbot framing fires at one, and curly quotes are not detected at all, since every editor inserts them automatically. Nothing in this family is block-tier, so it never fails the publish gate. Pattern set adapted from Wikipedia's "Signs of AI writing" (WikiProject AI Cleanup).
6197
7069
 
6198
- - **Agent washing is a copy tell.** A page that claims autonomy ("fully autonomous", "while you sleep", "no human intervention") and never says anywhere what the buyer approves, overrides, or audits scores an `agent-washing` warn. One signal about oversight of the agent's actions — approval, review, override, undo/rollback, audit log, escalation — anywhere on the page clears it; the rule is page-scope precisely because the hero can defer the trust story to a section below it. Generic privacy boilerplate ("GDPR", "encrypted", "your data") deliberately does **not** clear it, since it appears in most footers and says nothing about who is in charge of the agent.
7070
+ - **Agent washing is a copy tell.** A page with a sentence claiming autonomy ("fully autonomous", "while you sleep", "no human intervention", in any language) that never says anywhere what the buyer approves, overrides, or audits scores an `agent-washing` warn. Oversight of the agent's actions — approval, review, override, undo/rollback, audit log, escalation — said anywhere on the page clears it; the rule is page-scope precisely because the hero can defer the trust story to a section below it. Generic privacy boilerplate ("GDPR", "encrypted", "your data") deliberately does **not** clear it, since it appears in most footers and says nothing about who is in charge of the agent.
6199
7071
 
6200
- - **Positioning integrity is scored too, and it is the one copy rule that blocks on a single sentence.** `competitor-concession` fires when the page ranks somebody else above the client, or says the client does not lead at what the page sells ("su constructor es mejor que el nuestro", "we're not the best"). Every other prose rule waits for a cluster because one AI tell proves nothing; this one is the whole defect on its own — the page is paid traffic, and it is arguing the visitor should leave. Two shapes that read identically are exempt by design: a concession about **price**, in either direction ("we are not the cheapest / the most expensive, and here is why" is premium positioning), and a superlative flipped by a negation ("ningún equipo trabaja más rápido que el nuestro"), which is the strongest claim *for* the client. Its warn-tier sibling `competitor-named-in-comparison` fires when a name from `src/content/competitors/` appears in a comparative sentence outside a `<table>` — a comparison table is a section the landing skill teaches, but naming a rival anywhere else on paid traffic is the client's call, not the build's. Spanish, Portuguese and English.
7072
+ - **Positioning integrity is scored too, and it is the one copy rule that blocks on a single sentence.** `competitor-concession` fires when the page ranks somebody else above the client, or says the client does not lead at what the page sells ("su constructor es mejor que el nuestro", "we're not the best"). Every other prose rule waits for a cluster because one AI tell proves nothing; this one is the whole defect on its own — the page is paid traffic, and it is arguing the visitor should leave. Two shapes that read identically are exempt by design: a concession about **price**, in either direction ("we are not the cheapest / the most expensive, and here is why" is premium positioning), and a superlative flipped by a negation ("ningún equipo trabaja más rápido que el nuestro"), which is the strongest claim *for* the client. Its warn-tier sibling `competitor-named-in-comparison` fires when a name from `src/content/competitors/` appears in a sentence outside a `<table>` that compares the two (naming it as an integration is not a comparison) — a comparison table is a section the landing skill teaches, but naming a rival anywhere else on paid traffic is the client's call, not the build's. Any language.
6201
7073
 
6202
7074
  - **Comments are not markup, and no rule reads one.** Astro expression comments (`{/* … */}`) and HTML comments (`<!-- … -->`) are blanked before detection, offsets preserved so `file:line` stays exact. A shared scaffold component explaining its `preconnect` in prose that spelled `<img>` twice was read as two broken images — block-tier — so every landing rendering a video failed the publish gate on a file the client cannot edit. Comment prose is also where a component's reasoning lives, em-dashes and buzzwords included, so reading it inflated the count-threshold tells too.
6203
7075
 
@@ -6269,6 +7141,7 @@ This CLI is designed for AI agent consumption. Key patterns:
6269
7141
 
6270
7142
  ## Internal Notes
6271
7143
 
7144
+ - **Each command loads only itself.** Every subcommand is declared lazily and `baker` loads the one a call names (`src/command-loading.ts`); `schema`, `--help` and an unknown name load the whole tree. Loading everything made each `baker` process hold ~124 MB while it waited on one request, and sixty of them, run by parallel agents, filled an 8 GB Runtime; it is now ~48 MB. A new subcommand is mounted as `name: () => import("./x.ts").then((m) => m.xCommand)`, and a test that walks the tree synchronously loads it first with `loadAllCommands`. `__tests__/command-loading-budget.test.ts` builds the CLI as published and fails when startup loads more than 5% of it, or any command path more than half (a canvas render, the heaviest, loads under a third).
6272
7145
  - **0.316.1**: `baker flows show <slug>` reports **destination drift** — what the HubSpot form a Form submits to has changed under it — as `data.drift[]` plus a `DESTINATION DRIFT` hint carrying the repair as a ready-to-run command. It is the first time a Runtime can see this at all: the findings live in Baker and the HubSpot form id the repair needs is inside an encrypted config, so a Company Workspace could read neither. The classification is the one the dashboard badge reads (`classifyDrift` in `@baker/api`), so the two surfaces cannot disagree about whether a Form is healthy; a failed check answers `drift: null`, which is "unknown", never "clean". `baker flows map <slug> --side-effect <id> --hubspot-form <guid> --prune-missing [--write]` is the repair: it drops only the mappings whose destination field is absent from the form's live field list, names each in `dropped`, needs no `--map`, and refuses to run without a readable destination rather than guessing. Reach for it instead of `--replace`, which drops every mapping the call does not name.
6273
7146
 
6274
7147
  - **0.103.0**: `baker actions ...` commands now type their `/api/actions/...` request/response payloads from the shared `@baker/api` contract package (also consumed by the backend) instead of hand-written local interfaces. No command, flag, or output-shape changes.
@@ -6403,3 +7276,9 @@ Pushing to `main` with changes in `packages/cli/` triggers the GitHub Actions wo
6403
7276
  npx convex env set BAKER_CLI_VERSION <version> # Override sandbox version
6404
7277
  npx convex env remove BAKER_CLI_VERSION # Clean up after testing
6405
7278
  ```
7279
+
7280
+ ### Agreed tracking setup and external questionnaire diagnostics
7281
+
7282
+ `baker analytics plan [--full]` reads the effective session plan. `--json @preparation.json` stages journeys, event purposes and readiness, optionally wiring conversion definitions, funnel steps and a managed Tag Manager installation. The generated base queues early events and runs before event tags; repeating preparation replaces its own draft operations. These changes apply only at Publish. Waiting events record the missing change and owner; prepared tags do not imply accepted events. The full request contract and example are in the scaffold's [analytics tool doc](../../apps/scaffold-template/__tooling__/docs/tools/baker/analytics.md#shared-journey-and-tracking-plan).
7283
+
7284
+ `baker analytics questionnaire [--name questionnaire_step] [--days 7] [--inactivity 30]` reports distinct attempts reaching/completing/skipping questions and distinguishes pending, abandoned and incomplete steps. The external app must emit the documented attempt/version/sequence contract. The report refuses truncated histories above 600 events and excludes attempts without an observed start; no observations do not establish zero abandonment.