@koda-sl/baker-cli 0.204.0 → 0.209.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/README.md +56 -3
  2. package/dist/{chunk-UZYVSPCA.js → chunk-3E33AGNR.js} +2 -2
  3. package/dist/{chunk-SVHFT7AN.js → chunk-3WODSPNZ.js} +4 -4
  4. package/dist/{chunk-2R2DEPG7.js → chunk-CRXSLYEX.js} +3 -3
  5. package/dist/{chunk-5MPIOGRO.js → chunk-E2WYWHJJ.js} +370 -221
  6. package/dist/chunk-E2WYWHJJ.js.map +1 -0
  7. package/dist/{chunk-KHHD26LZ.js → chunk-FUSTYLP5.js} +3 -3
  8. package/dist/{chunk-6NZG2TCM.js → chunk-IRNM3T6B.js} +6 -1
  9. package/dist/{chunk-6NZG2TCM.js.map → chunk-IRNM3T6B.js.map} +1 -1
  10. package/dist/cli.js +732 -121
  11. package/dist/cli.js.map +1 -1
  12. package/dist/client-CF6ZORCO.js +15 -0
  13. package/dist/engine/index.js +3 -3
  14. package/dist/{env-FF7ZB5W5.js → env-7SCX7HAH.js} +4 -2
  15. package/dist/{output-YGPWST77.js → output-4YIOSWOG.js} +5 -5
  16. package/dist/{shared-CRWB5B7P.js → shared-EBF5DEDJ.js} +6 -6
  17. package/package.json +1 -1
  18. package/dist/chunk-5MPIOGRO.js.map +0 -1
  19. package/dist/client-MP7BKFKM.js +0 -15
  20. /package/dist/{chunk-UZYVSPCA.js.map → chunk-3E33AGNR.js.map} +0 -0
  21. /package/dist/{chunk-SVHFT7AN.js.map → chunk-3WODSPNZ.js.map} +0 -0
  22. /package/dist/{chunk-2R2DEPG7.js.map → chunk-CRXSLYEX.js.map} +0 -0
  23. /package/dist/{chunk-KHHD26LZ.js.map → chunk-FUSTYLP5.js.map} +0 -0
  24. /package/dist/{client-MP7BKFKM.js.map → client-CF6ZORCO.js.map} +0 -0
  25. /package/dist/{env-FF7ZB5W5.js.map → env-7SCX7HAH.js.map} +0 -0
  26. /package/dist/{output-YGPWST77.js.map → output-4YIOSWOG.js.map} +0 -0
  27. /package/dist/{shared-CRWB5B7P.js.map → shared-EBF5DEDJ.js.map} +0 -0
package/dist/cli.js CHANGED
@@ -18,18 +18,23 @@ import {
18
18
  SEEDANCE_PROFILE,
19
19
  VIDEO_GENERATE_MODELS,
20
20
  ValidationError,
21
+ YtDlpError,
22
+ canonicalUrl,
21
23
  clipParamRecipe,
22
24
  clipProfileFor,
23
25
  collectAssetRefLikes,
24
26
  createEngineFromEnv,
25
27
  defaultRegistry,
26
28
  describeFailureReason,
29
+ durationSeconds,
27
30
  elementMentionKeywords,
28
31
  estimateVideoCredits,
29
32
  fetchExternalBytes,
30
33
  generateCatalog,
31
34
  imageProfileFor,
35
+ isAudioOnly,
32
36
  isPersistedAssetRef,
37
+ isVimeoAuthFailure,
33
38
  looksLikeHttpUrl,
34
39
  maxInputReferences,
35
40
  maxInputSlot,
@@ -38,9 +43,11 @@ import {
38
43
  nearestSupportedImageSize,
39
44
  parseRefExpr,
40
45
  platformFormats,
46
+ probeYtDlp,
41
47
  promptMaxLength,
42
48
  requireCredentialsFromEnv,
43
49
  resolveConcurrency,
50
+ runYtDlp,
44
51
  sha256Hex,
45
52
  spineInputFlags,
46
53
  spineInputOps,
@@ -49,8 +56,9 @@ import {
49
56
  supportsReferenceToVideo,
50
57
  toModelSafeImage,
51
58
  ulid,
52
- validateCanvasDeep
53
- } from "./chunk-5MPIOGRO.js";
59
+ validateCanvasDeep,
60
+ ytDlpBlockSignal
61
+ } from "./chunk-E2WYWHJJ.js";
54
62
  import {
55
63
  csvOrJson,
56
64
  daysAgoIso,
@@ -59,7 +67,7 @@ import {
59
67
  resolveAccountIdArg,
60
68
  resolveEffectiveStatus,
61
69
  todayIso
62
- } from "./chunk-SVHFT7AN.js";
70
+ } from "./chunk-3WODSPNZ.js";
63
71
  import {
64
72
  buildQueryCacheKey,
65
73
  cacheGet,
@@ -75,20 +83,21 @@ import {
75
83
  writeAdsJson,
76
84
  writeAdsOutput,
77
85
  writeJsonEnvelope
78
- } from "./chunk-KHHD26LZ.js";
86
+ } from "./chunk-FUSTYLP5.js";
79
87
  import {
80
88
  ApiError,
81
89
  apiGet,
82
90
  apiPost,
83
91
  validateConvexId
84
- } from "./chunk-2R2DEPG7.js";
92
+ } from "./chunk-CRXSLYEX.js";
85
93
  import {
86
94
  installStreamTaps,
87
95
  logInvocation
88
- } from "./chunk-UZYVSPCA.js";
96
+ } from "./chunk-3E33AGNR.js";
89
97
  import {
90
98
  captureBudgetMs,
91
99
  captureProxyCredentials,
100
+ captureVimeoCookie,
92
101
  getEnv,
93
102
  isProxyFailure,
94
103
  plannedRoutes,
@@ -96,10 +105,10 @@ import {
96
105
  requireChatId,
97
106
  resolveChatId,
98
107
  shouldEscalate
99
- } from "./chunk-6NZG2TCM.js";
108
+ } from "./chunk-IRNM3T6B.js";
100
109
 
101
110
  // src/cli.ts
102
- import { defineCommand as defineCommand216, runMain } from "citty";
111
+ import { defineCommand as defineCommand217, runMain } from "citty";
103
112
 
104
113
  // src/commands/actions/index.ts
105
114
  import { defineCommand as defineCommand18 } from "citty";
@@ -4785,6 +4794,60 @@ var hubspotWorkflowsListResponseSchema = z12.object({
4785
4794
  ok: z12.literal(true),
4786
4795
  data: z12.object({ workflows: z12.array(hubspotWorkflowSummarySchema) })
4787
4796
  });
4797
+ var hubspotWorkflowsViewRequestSchema = z12.object({
4798
+ /** The id from `workflows list`. Resolved to HubSpot's internal flow id server-side. */
4799
+ workflowId: z12.string().min(1),
4800
+ /**
4801
+ * Include the untouched HubSpot payload — the only place the branch filter
4802
+ * criteria (which property is compared to which value) can be read.
4803
+ */
4804
+ full: z12.boolean().optional()
4805
+ });
4806
+ var hubspotWorkflowBranchSchema = z12.object({
4807
+ /**
4808
+ * 1-based position in the order HubSpot lists the branches. On a `LIST_BRANCH`
4809
+ * that is precedence — branch 1 wins over branch 2. On a `STATIC_BRANCH` it is
4810
+ * only display order; those match on the value in `name`, not top down.
4811
+ */
4812
+ order: z12.number().int(),
4813
+ name: z12.string(),
4814
+ nextActionId: z12.string().nullable()
4815
+ });
4816
+ var hubspotWorkflowStepSchema = z12.object({
4817
+ actionId: z12.string(),
4818
+ /** HubSpot's action kind: LIST_BRANCH, SINGLE_CONNECTION, CUSTOM_CODE, WEBHOOK... */
4819
+ type: z12.string(),
4820
+ actionTypeId: z12.string().nullable(),
4821
+ /** The step's configured inputs — for a property-setting step, what it writes and where. */
4822
+ fields: z12.record(z12.string(), z12.unknown()).nullable(),
4823
+ nextActionId: z12.string().nullable(),
4824
+ /** Present only on branch steps, in evaluation order. */
4825
+ branches: z12.array(hubspotWorkflowBranchSchema).nullable(),
4826
+ defaultBranch: z12.object({ name: z12.string().nullable(), nextActionId: z12.string().nullable() }).nullable()
4827
+ });
4828
+ var hubspotWorkflowDetailSchema = z12.object({
4829
+ /** HubSpot's internal flow id, which differs from the workflow id. */
4830
+ id: z12.string(),
4831
+ /** The id this workflow is known by in `workflows list` and in HubSpot's own URLs. */
4832
+ workflowId: z12.string(),
4833
+ name: z12.string(),
4834
+ enabled: z12.boolean(),
4835
+ flowType: z12.string().nullable(),
4836
+ /** The record type it runs on: "0-1" contacts, "0-2" companies, "0-3" deals. */
4837
+ objectTypeId: z12.string().nullable(),
4838
+ enrollment: z12.object({
4839
+ type: z12.string().nullable(),
4840
+ /** Null means HubSpot's criteria shape does not report it — unknown, not off. */
4841
+ reEnrollment: z12.boolean().nullable()
4842
+ }),
4843
+ /** Every step. Execution order follows `nextActionId`, not the array index. */
4844
+ steps: z12.array(hubspotWorkflowStepSchema),
4845
+ raw: z12.record(z12.string(), z12.unknown()).optional()
4846
+ });
4847
+ var hubspotWorkflowsViewResponseSchema = z12.object({
4848
+ ok: z12.literal(true),
4849
+ data: z12.object({ workflow: hubspotWorkflowDetailSchema })
4850
+ });
4788
4851
  var hubspotContactsSummaryRequestSchema = z12.object({
4789
4852
  /** Window in days, counted back from now, over contact creation date. */
4790
4853
  days: z12.number().int().min(1).max(365).default(30)
@@ -5761,6 +5824,30 @@ var testimonialsOutscraperWebhookResponseSchema = z18.object({
5761
5824
  // ../api/src/videos.ts
5762
5825
  import { z as z19 } from "zod";
5763
5826
  var videoStatusSchema = z19.enum(["uploading", "uploaded", "processing", "ready", "error"]);
5827
+ var videoSourceSchema = z19.enum([
5828
+ "uploaded",
5829
+ "url",
5830
+ "bilibili",
5831
+ "bluesky",
5832
+ "dailymotion",
5833
+ "facebook",
5834
+ "instagram",
5835
+ "loom",
5836
+ "newgrounds",
5837
+ "ok",
5838
+ "pinterest",
5839
+ "reddit",
5840
+ "rutube",
5841
+ "snapchat",
5842
+ "streamable",
5843
+ "tiktok",
5844
+ "tumblr",
5845
+ "twitch",
5846
+ "twitter",
5847
+ "vimeo",
5848
+ "vk",
5849
+ "youtube"
5850
+ ]);
5764
5851
  var videoTranscriptSegmentSchema = z19.object({
5765
5852
  text: z19.string(),
5766
5853
  startSecond: z19.number(),
@@ -5861,6 +5948,25 @@ var videosUploadRequestSchema = z19.object({
5861
5948
  descriptionContext: z19.string().optional()
5862
5949
  });
5863
5950
  var videosUploadResponseSchema = z19.object({ uploadUrl: z19.string(), videoId: z19.string() });
5951
+ var videosIngestRequestSchema = z19.object({
5952
+ // Must be a direct media file Mux can pull (mp4/mov/webm/…). A page URL —
5953
+ // a YouTube watch link, a TikTok post — is not one: the CLI resolves those to
5954
+ // a media file before calling this route.
5955
+ url: z19.string().url(),
5956
+ source: videoSourceSchema,
5957
+ externalId: z19.string().optional(),
5958
+ externalUrl: z19.string().optional(),
5959
+ // What the person adding the clip knew that the frames cannot show — which
5960
+ // campaign it belongs to, whether the person on camera is a real customer.
5961
+ // The upload route has carried this since it existed; ingest silently dropped
5962
+ // it, so a link-added clip was analysed with strictly less to go on than the
5963
+ // same file uploaded by hand.
5964
+ descriptionContext: z19.string().optional()
5965
+ });
5966
+ var videosIngestResponseSchema = z19.object({
5967
+ videoId: z19.string(),
5968
+ deduped: z19.boolean()
5969
+ });
5864
5970
  var videosDeleteRequestSchema = z19.object({ id: z19.string().min(1, "Missing video ID") });
5865
5971
  var videosDeleteResponseSchema = z19.object({ ok: z19.literal(true) });
5866
5972
 
@@ -8821,11 +8927,11 @@ function rawTextEntries(value) {
8821
8927
  const values = Array.isArray(value) ? value : typeof value === "string" ? [value] : [];
8822
8928
  return values.filter((v) => typeof v === "string").flatMap((v) => v.split(",")).map((v) => v.trim()).filter(Boolean);
8823
8929
  }
8824
- function rawFileEntries(path37) {
8825
- if (typeof path37 !== "string" || path37.length === 0) {
8930
+ function rawFileEntries(path38) {
8931
+ if (typeof path38 !== "string" || path38.length === 0) {
8826
8932
  return [];
8827
8933
  }
8828
- return readFileSync2(path37, "utf8").split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
8934
+ return readFileSync2(path38, "utf8").split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
8829
8935
  }
8830
8936
  function keywordEntries(args) {
8831
8937
  const defaultMatch = typeof args["match-type"] === "string" ? args["match-type"].toUpperCase() : void 0;
@@ -8848,19 +8954,19 @@ function keywordEntries(args) {
8848
8954
  }
8849
8955
  return entries;
8850
8956
  }
8851
- function loadJsonFileArg(path37) {
8852
- if (typeof path37 !== "string" || path37.length === 0) {
8957
+ function loadJsonFileArg(path38) {
8958
+ if (typeof path38 !== "string" || path38.length === 0) {
8853
8959
  return {};
8854
8960
  }
8855
8961
  try {
8856
- const parsed = JSON.parse(readFileSync2(path37, "utf8"));
8962
+ const parsed = JSON.parse(readFileSync2(path38, "utf8"));
8857
8963
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
8858
- failWriteValidation(`${path37} must contain a JSON object`);
8964
+ failWriteValidation(`${path38} must contain a JSON object`);
8859
8965
  }
8860
8966
  return parsed;
8861
8967
  } catch (err) {
8862
8968
  if (err instanceof SyntaxError) {
8863
- failWriteValidation(`${path37} is not valid JSON: ${err.message}`);
8969
+ failWriteValidation(`${path38} is not valid JSON: ${err.message}`);
8864
8970
  }
8865
8971
  throw err;
8866
8972
  }
@@ -8990,10 +9096,10 @@ async function stageUpdate(kind, customerId, target, payload, hints) {
8990
9096
  async function stageTarget(kind, customerId, target, hints) {
8991
9097
  await stageGoogleOp({ kind, customerId, target }, hints);
8992
9098
  }
8993
- async function draftAction(path37, body, chat) {
9099
+ async function draftAction(path38, body, chat) {
8994
9100
  try {
8995
9101
  const chatId = resolveChatId(chat);
8996
- const response = await apiPost(path37, { chatId, ...body });
9102
+ const response = await apiPost(path38, { chatId, ...body });
8997
9103
  writeJsonEnvelope(response);
8998
9104
  } catch (err) {
8999
9105
  handleGoogleError(err);
@@ -11851,19 +11957,19 @@ function failWriteValidation2(message) {
11851
11957
  writeJsonEnvelope({ ok: false, error: { code: "VALIDATION_ERROR", message } });
11852
11958
  process.exit(1);
11853
11959
  }
11854
- function loadJsonFileArg2(path37) {
11855
- if (typeof path37 !== "string" || path37.length === 0) {
11960
+ function loadJsonFileArg2(path38) {
11961
+ if (typeof path38 !== "string" || path38.length === 0) {
11856
11962
  return {};
11857
11963
  }
11858
11964
  try {
11859
- const parsed = JSON.parse(readFileSync4(path37, "utf8"));
11965
+ const parsed = JSON.parse(readFileSync4(path38, "utf8"));
11860
11966
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
11861
- failWriteValidation2(`${path37} must contain a JSON object`);
11967
+ failWriteValidation2(`${path38} must contain a JSON object`);
11862
11968
  }
11863
11969
  return parsed;
11864
11970
  } catch (err) {
11865
11971
  if (err instanceof SyntaxError) {
11866
- failWriteValidation2(`${path37} is not valid JSON: ${err.message}`);
11972
+ failWriteValidation2(`${path38} is not valid JSON: ${err.message}`);
11867
11973
  }
11868
11974
  throw err;
11869
11975
  }
@@ -11948,15 +12054,15 @@ function parseLocaleFlag(value) {
11948
12054
  }
11949
12055
  return { language: match[1], country: match[2].toUpperCase() };
11950
12056
  }
11951
- function loadTargetingFileArg(path37) {
11952
- if (typeof path37 !== "string" || path37.length === 0) {
12057
+ function loadTargetingFileArg(path38) {
12058
+ if (typeof path38 !== "string" || path38.length === 0) {
11953
12059
  return void 0;
11954
12060
  }
11955
- const parsed = loadJsonFileArg2(path37);
12061
+ const parsed = loadJsonFileArg2(path38);
11956
12062
  const criteria = parsed.targetingCriteria ?? parsed;
11957
12063
  if (!criteria.include) {
11958
12064
  failWriteValidation2(
11959
- `${path37} must contain targeting criteria with an "include" block (see baker schema ads.linkedin.campaigns.create)`
12065
+ `${path38} must contain targeting criteria with an "include" block (see baker schema ads.linkedin.campaigns.create)`
11960
12066
  );
11961
12067
  }
11962
12068
  return criteria;
@@ -11991,14 +12097,14 @@ function parseCsvLine(line) {
11991
12097
  cells.push(current);
11992
12098
  return cells.map((cell) => cell.trim());
11993
12099
  }
11994
- function parseListFileArg(path37, maxRows) {
11995
- if (typeof path37 !== "string" || path37.length === 0) {
12100
+ function parseListFileArg(path38, maxRows) {
12101
+ if (typeof path38 !== "string" || path38.length === 0) {
11996
12102
  return void 0;
11997
12103
  }
11998
- const raw = readFileSync4(path37, "utf8");
12104
+ const raw = readFileSync4(path38, "utf8");
11999
12105
  const lines = raw.split(/\r?\n/).filter((line) => line.trim().length > 0);
12000
12106
  if (lines.length < 2) {
12001
- failWriteValidation2(`${path37} needs a header row and at least one data row`);
12107
+ failWriteValidation2(`${path38} needs a header row and at least one data row`);
12002
12108
  }
12003
12109
  const columns = parseCsvLine(lines[0]).map((column) => column.trim());
12004
12110
  const rows = [];
@@ -12017,7 +12123,7 @@ function parseListFileArg(path37, maxRows) {
12017
12123
  }
12018
12124
  }
12019
12125
  if (rows.length > maxRows) {
12020
- failWriteValidation2(`${path37} has ${rows.length} rows \u2014 the inline limit is ${maxRows}. Split the list.`);
12126
+ failWriteValidation2(`${path38} has ${rows.length} rows \u2014 the inline limit is ${maxRows}. Split the list.`);
12021
12127
  }
12022
12128
  return { columns, rows };
12023
12129
  }
@@ -12113,11 +12219,11 @@ function readPositionals(args) {
12113
12219
  function splitIdList(raw) {
12114
12220
  return raw.split(",").map((id) => id.trim()).filter(Boolean);
12115
12221
  }
12116
- function idsFileEntries(path37) {
12117
- if (typeof path37 !== "string" || path37.length === 0) {
12222
+ function idsFileEntries(path38) {
12223
+ if (typeof path38 !== "string" || path38.length === 0) {
12118
12224
  return [];
12119
12225
  }
12120
- return readFileSync4(path37, "utf8").split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#")).flatMap(splitIdList);
12226
+ return readFileSync4(path38, "utf8").split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#")).flatMap(splitIdList);
12121
12227
  }
12122
12228
  function requireTargets(args, entity) {
12123
12229
  const positionals = readPositionals(args);
@@ -14740,9 +14846,9 @@ function compactRow(row) {
14740
14846
  ...destination.postUrn ? { postUrn: destination.postUrn } : {}
14741
14847
  };
14742
14848
  }
14743
- function readPath(row, path37) {
14849
+ function readPath(row, path38) {
14744
14850
  let current = row;
14745
- for (const segment of path37.split(".")) {
14851
+ for (const segment of path38.split(".")) {
14746
14852
  const record = asRecord2(current);
14747
14853
  if (!record) return void 0;
14748
14854
  current = record[segment];
@@ -14752,10 +14858,10 @@ function readPath(row, path37) {
14752
14858
  function projectFields(rows, paths) {
14753
14859
  return rows.map((row) => {
14754
14860
  const projected = {};
14755
- for (const path37 of paths) {
14756
- const value = readPath(row, path37);
14861
+ for (const path38 of paths) {
14862
+ const value = readPath(row, path38);
14757
14863
  if (value !== void 0) {
14758
- projected[path37] = value;
14864
+ projected[path38] = value;
14759
14865
  }
14760
14866
  }
14761
14867
  return projected;
@@ -16049,11 +16155,11 @@ var updateStatusSchema = z21.enum(UPDATE_STATUSES);
16049
16155
  function currencyMinimums2(currencyCode) {
16050
16156
  return CURRENCY_MINIMUMS2[currencyCode] ?? DEFAULT_CURRENCY_MINIMUM2;
16051
16157
  }
16052
- function validateDailyBudgetFloor(money, ctx, path37) {
16158
+ function validateDailyBudgetFloor(money, ctx, path38) {
16053
16159
  if (money?.currencyCode) {
16054
16160
  const min = currencyMinimums2(money.currencyCode).dailyBudgetMin;
16055
16161
  if (Number(money.amount) < min) {
16056
- ctx.addIssue({ code: "custom", path: path37, message: `below the ${min} ${money.currencyCode} daily minimum` });
16162
+ ctx.addIssue({ code: "custom", path: path38, message: `below the ${min} ${money.currencyCode} daily minimum` });
16057
16163
  }
16058
16164
  }
16059
16165
  }
@@ -16715,19 +16821,19 @@ function failWriteValidation3(message) {
16715
16821
  writeJsonEnvelope({ ok: false, error: { code: "VALIDATION_ERROR", message } });
16716
16822
  process.exit(1);
16717
16823
  }
16718
- function loadJsonFileArg3(path37) {
16719
- if (typeof path37 !== "string" || path37.length === 0) {
16824
+ function loadJsonFileArg3(path38) {
16825
+ if (typeof path38 !== "string" || path38.length === 0) {
16720
16826
  return {};
16721
16827
  }
16722
16828
  try {
16723
- const parsed = JSON.parse(readFileSync8(path37, "utf8"));
16829
+ const parsed = JSON.parse(readFileSync8(path38, "utf8"));
16724
16830
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
16725
- failWriteValidation3(`${path37} must contain a JSON object`);
16831
+ failWriteValidation3(`${path38} must contain a JSON object`);
16726
16832
  }
16727
16833
  return parsed;
16728
16834
  } catch (err) {
16729
16835
  if (err instanceof SyntaxError) {
16730
- failWriteValidation3(`${path37} is not valid JSON: ${err.message}`);
16836
+ failWriteValidation3(`${path38} is not valid JSON: ${err.message}`);
16731
16837
  }
16732
16838
  throw err;
16733
16839
  }
@@ -17270,10 +17376,10 @@ function duplicateCommand2(entity, label) {
17270
17376
  replace: { type: "boolean", description: "Pause the original once the copy publishes" }
17271
17377
  },
17272
17378
  run: async ({ args }) => {
17273
- const { apiPost: apiPost2 } = await import("./client-MP7BKFKM.js");
17274
- const { requireChatId: requireChatId2 } = await import("./env-FF7ZB5W5.js");
17275
- const { writeJsonEnvelope: writeJsonEnvelope2 } = await import("./output-YGPWST77.js");
17276
- const { handleMetaError: handleMetaError2 } = await import("./shared-CRWB5B7P.js");
17379
+ const { apiPost: apiPost2 } = await import("./client-CF6ZORCO.js");
17380
+ const { requireChatId: requireChatId2 } = await import("./env-7SCX7HAH.js");
17381
+ const { writeJsonEnvelope: writeJsonEnvelope2 } = await import("./output-4YIOSWOG.js");
17382
+ const { handleMetaError: handleMetaError2 } = await import("./shared-EBF5DEDJ.js");
17277
17383
  try {
17278
17384
  const accountId = bareAccountId2(args);
17279
17385
  const chatId = requireChatId2();
@@ -27898,12 +28004,12 @@ function listFlowSlugs() {
27898
28004
  return readdirSync2(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith("_") && entry.name !== ".gitkeep").map((entry) => entry.name).sort();
27899
28005
  }
27900
28006
  function readFlowTree(slug) {
27901
- const path37 = join3(flowsDir(), slug, "_data.json");
27902
- if (!existsSync5(path37)) {
28007
+ const path38 = join3(flowsDir(), slug, "_data.json");
28008
+ if (!existsSync5(path38)) {
27903
28009
  failLocal(`No form "${slug}". Run "baker flows list" to see the forms in this workspace.`);
27904
28010
  }
27905
28011
  try {
27906
- return JSON.parse(readFileSync9(path37, "utf-8"));
28012
+ return JSON.parse(readFileSync9(path38, "utf-8"));
27907
28013
  } catch (error) {
27908
28014
  failLocal(`Could not read form "${slug}": ${error instanceof Error ? error.message : String(error)}`);
27909
28015
  }
@@ -28316,10 +28422,10 @@ async function stageOps(ops) {
28316
28422
  handleError(err);
28317
28423
  }
28318
28424
  }
28319
- async function draftAction2(path37, body, chat) {
28425
+ async function draftAction2(path38, body, chat) {
28320
28426
  const chatId = resolveChatId(chat);
28321
28427
  try {
28322
- const data = await apiPost(path37, { chatId, ...body });
28428
+ const data = await apiPost(path38, { chatId, ...body });
28323
28429
  writeJsonEnvelope({ ok: true, data });
28324
28430
  return data;
28325
28431
  } catch (err) {
@@ -29820,6 +29926,23 @@ registerSchema({
29820
29926
  "enabled-only": { type: "boolean", description: "Only workflows that are on", required: false, default: false }
29821
29927
  }
29822
29928
  });
29929
+ registerSchema({
29930
+ command: "hubspot.workflows.view",
29931
+ description: "One workflow's actual configuration: what enrols a record, whether it re-enrols, and every step with the property it writes. Read it before saying a workflow is correct \u2014 `workflows list` only tells you it exists and whether it is on. Branches come back in HubSpot's order, and on a LIST_BRANCH step that is evaluation order (`order: 1` wins over `order: 2`) \u2014 which is where this kind of setup silently breaks: a broad branch placed above a narrow one swallows the records the narrow one was written for, and nothing reports it. A STATIC_BRANCH matches on a value instead, so its order carries no precedence. Steps are NOT in execution order \u2014 follow `nextActionId` from step to step. Add --full for the branch filter criteria (which property is compared to which value).",
29932
+ args: {
29933
+ workflowId: {
29934
+ type: "positional",
29935
+ description: "Workflow id (from `baker hubspot workflows list`)",
29936
+ required: true
29937
+ },
29938
+ full: {
29939
+ type: "boolean",
29940
+ description: "Include the raw HubSpot payload \u2014 the only place the branch filter criteria can be read",
29941
+ required: false,
29942
+ default: false
29943
+ }
29944
+ }
29945
+ });
29823
29946
  registerSchema({
29824
29947
  command: "hubspot.pipelines.list",
29825
29948
  description: "The account's deal pipelines and their stages, in the order they run. Use it to answer where a lead lands once captured and whether it moved \u2014 the 'have these leads been worked?' half of a form audit.",
@@ -30119,6 +30242,62 @@ var workflowsListCommand = defineCommand120({
30119
30242
  }
30120
30243
  }
30121
30244
  });
30245
+ function workflowViewHints(workflow) {
30246
+ const hints = [];
30247
+ if (!workflow.enabled) {
30248
+ hints.push(
30249
+ "This workflow is switched off, so nothing below it is running now \u2014 it may still have run while it was on. Say so before reporting on what it does."
30250
+ );
30251
+ }
30252
+ const ordered = workflow.steps.filter((step) => step.type === "LIST_BRANCH" && (step.branches?.length ?? 0) > 1);
30253
+ if (ordered.length > 0) {
30254
+ hints.push(
30255
+ "Branches are listed in evaluation order and the first match wins. Check that no broad branch sits above a narrower one \u2014 if it does, the narrow branch never runs and nothing in HubSpot reports it."
30256
+ );
30257
+ }
30258
+ if (workflow.raw === void 0 && ordered.length > 0) {
30259
+ hints.push(
30260
+ "This view names the branches but not what they compare. Re-run with --full to read the filter criteria before judging whether a branch matches what it was meant to match."
30261
+ );
30262
+ }
30263
+ if (workflow.enrollment.reEnrollment === false) {
30264
+ hints.push(
30265
+ "Re-enrolment is off: a record that meets the criteria a second time will not run through this again. That is a real choice, not necessarily a fault \u2014 but confirm it is the intended one."
30266
+ );
30267
+ } else if (workflow.enrollment.reEnrollment === null) {
30268
+ hints.push(
30269
+ "Whether this re-enrols is not reported for this kind of enrolment, so do not state it either way \u2014 read it in HubSpot if it matters."
30270
+ );
30271
+ }
30272
+ return hints;
30273
+ }
30274
+ var workflowsViewCommand = defineCommand120({
30275
+ meta: {
30276
+ name: "view",
30277
+ description: "Read one workflow's real configuration \u2014 enrolment, branches in order, and what each step writes. Example: baker hubspot workflows view 121183594 --full"
30278
+ },
30279
+ args: {
30280
+ workflowId: {
30281
+ type: "positional",
30282
+ description: "Workflow id (from `baker hubspot workflows list`)",
30283
+ required: true
30284
+ },
30285
+ full: { type: "boolean", description: "Include the raw HubSpot payload with the filter criteria", required: false }
30286
+ },
30287
+ run: async ({ args }) => {
30288
+ try {
30289
+ const response = await apiPost("/api/hubspot/workflows/view", {
30290
+ workflowId: args.workflowId,
30291
+ ...args.full ? { full: true } : {}
30292
+ });
30293
+ const { workflow } = response.data;
30294
+ const hints = workflowViewHints(workflow);
30295
+ writeJson({ ...response, meta: { count: workflow.steps.length }, ...hints.length > 0 ? { hints } : {} });
30296
+ } catch (err) {
30297
+ failNotConnected(err);
30298
+ }
30299
+ }
30300
+ });
30122
30301
  var pipelinesListCommand = defineCommand120({
30123
30302
  meta: {
30124
30303
  name: "list",
@@ -30240,7 +30419,7 @@ var formsCommand = defineCommand120({
30240
30419
  });
30241
30420
  var workflowsCommand = defineCommand120({
30242
30421
  meta: { name: "workflows", description: "HubSpot workflows on the connected account." },
30243
- subCommands: { list: workflowsListCommand }
30422
+ subCommands: { list: workflowsListCommand, view: workflowsViewCommand }
30244
30423
  });
30245
30424
  var pipelinesCommand = defineCommand120({
30246
30425
  meta: { name: "pipelines", description: "HubSpot deal pipelines on the connected account." },
@@ -30263,6 +30442,7 @@ Examples:
30263
30442
  baker hubspot forms view <formId> --as-node # the form.external blob -> write into the step
30264
30443
  baker hubspot forms submissions <formId> # how many leads it got, and when
30265
30444
  baker hubspot workflows list --enabled-only # what runs after a lead is captured
30445
+ baker hubspot workflows view <workflowId> # its real branches, in order, and what each step writes
30266
30446
  baker hubspot pipelines list # where a lead lands, and the stages after it
30267
30447
  baker hubspot contacts summary --days 30 # are recent leads being worked? (counts only)
30268
30448
  baker hubspot contacts lookup a@b.com # was this one lead picked up?
@@ -30340,9 +30520,9 @@ async function readImageBuffer(pathOrUrl) {
30340
30520
  }
30341
30521
  return readFile19(pathOrUrl);
30342
30522
  }
30343
- async function isDirectory(path37) {
30523
+ async function isDirectory(path38) {
30344
30524
  try {
30345
- const s = await stat4(path37);
30525
+ const s = await stat4(path38);
30346
30526
  return s.isDirectory();
30347
30527
  } catch {
30348
30528
  return false;
@@ -30626,13 +30806,13 @@ function resolveDownloadPath({ baseName, extension, out, outIsDirectory: outIsDi
30626
30806
  }
30627
30807
  function disambiguate(paths) {
30628
30808
  const taken = /* @__PURE__ */ new Set();
30629
- return paths.map((path37) => {
30630
- if (!taken.has(path37)) {
30631
- taken.add(path37);
30632
- return path37;
30809
+ return paths.map((path38) => {
30810
+ if (!taken.has(path38)) {
30811
+ taken.add(path38);
30812
+ return path38;
30633
30813
  }
30634
- const ext = extname3(path37);
30635
- const stem = path37.slice(0, path37.length - ext.length);
30814
+ const ext = extname3(path38);
30815
+ const stem = path38.slice(0, path38.length - ext.length);
30636
30816
  let n = 2;
30637
30817
  while (taken.has(`${stem}-${n}${ext}`)) n += 1;
30638
30818
  const unique = `${stem}-${n}${ext}`;
@@ -30759,10 +30939,10 @@ async function runDownloads(plan) {
30759
30939
  const paths = disambiguate(fetched.map((item) => item.path));
30760
30940
  const downloaded = [];
30761
30941
  for (const [index, item] of fetched.entries()) {
30762
- const path37 = paths[index] ?? item.path;
30942
+ const path38 = paths[index] ?? item.path;
30763
30943
  try {
30764
- await atomicWrite(path37, item.buffer);
30765
- downloaded.push({ input: item.input, output: path37, bytes: item.buffer.length, contentType: item.contentType });
30944
+ await atomicWrite(path38, item.buffer);
30945
+ downloaded.push({ input: item.input, output: path38, bytes: item.buffer.length, contentType: item.contentType });
30766
30946
  } catch (err) {
30767
30947
  failed.push({ input: item.input, error: failureMessage(err, "Write failed") });
30768
30948
  }
@@ -32513,14 +32693,14 @@ async function processInternal(inputBuffer, isSVG, options) {
32513
32693
  processedData = result.data;
32514
32694
  info = result.info;
32515
32695
  }
32516
- let pipeline = sharp2(processedData, {
32696
+ let pipeline2 = sharp2(processedData, {
32517
32697
  raw: { channels: 4, width: info.width, height: info.height }
32518
32698
  }).png();
32519
32699
  if (options.shrinkToContent) {
32520
32700
  stages.push("trim");
32521
- pipeline = pipeline.trim();
32701
+ pipeline2 = pipeline2.trim();
32522
32702
  }
32523
- const outputBuffer = await pipeline.toBuffer();
32703
+ const outputBuffer = await pipeline2.toBuffer();
32524
32704
  return { buffer: outputBuffer, stages };
32525
32705
  }
32526
32706
  function applyResize(buffer, resize) {
@@ -40493,7 +40673,7 @@ function parseImageRefs(spec) {
40493
40673
  }
40494
40674
  var defaultDeps = {
40495
40675
  ingest: (url) => apiPost("/api/images/ingest", { url, source: "uploaded" }),
40496
- upload: (path37) => uploadLocalImage({ file: path37, contentType: detectImageContentType(path37), source: "uploaded" })
40676
+ upload: (path38) => uploadLocalImage({ file: path38, contentType: detectImageContentType(path38), source: "uploaded" })
40497
40677
  };
40498
40678
  async function resolveLibraryImageIds(spec, limit, deps = defaultDeps) {
40499
40679
  const refs = parseImageRefs(spec);
@@ -40513,10 +40693,10 @@ async function resolveLibraryImageIds(spec, limit, deps = defaultDeps) {
40513
40693
  }
40514
40694
  return { imageIds, added };
40515
40695
  }
40516
- function uploadFailure(path37) {
40696
+ function uploadFailure(path38) {
40517
40697
  return (error) => {
40518
40698
  if (error instanceof ApiError) throw error;
40519
- throw new ApiError("VALIDATION_ERROR", `Could not read "${path37}" as an image.`);
40699
+ throw new ApiError("VALIDATION_ERROR", `Could not read "${path38}" as an image.`);
40520
40700
  };
40521
40701
  }
40522
40702
 
@@ -41566,10 +41746,10 @@ async function stageOp4(op) {
41566
41746
  handleError4(err);
41567
41747
  }
41568
41748
  }
41569
- async function draftAction3(path37, body, chat) {
41749
+ async function draftAction3(path38, body, chat) {
41570
41750
  const chatId = resolveChatId(chat);
41571
41751
  try {
41572
- const data = await apiPost(path37, { chatId, ...body });
41752
+ const data = await apiPost(path38, { chatId, ...body });
41573
41753
  writeJsonEnvelope({ ok: true, data });
41574
41754
  return data;
41575
41755
  } catch (err) {
@@ -42542,7 +42722,7 @@ Full guide: __tooling__/docs/tools/baker/testimonials.md`
42542
42722
  });
42543
42723
 
42544
42724
  // src/commands/videos/index.ts
42545
- import { defineCommand as defineCommand202 } from "citty";
42725
+ import { defineCommand as defineCommand203 } from "citty";
42546
42726
 
42547
42727
  // src/commands/videos/delete.ts
42548
42728
  import { defineCommand as defineCommand197 } from "citty";
@@ -42673,8 +42853,420 @@ var groupCommand2 = defineCommand199({
42673
42853
  }
42674
42854
  });
42675
42855
 
42676
- // src/commands/videos/search.ts
42856
+ // src/commands/videos/ingest.ts
42857
+ import { mkdtemp as mkdtemp2, rm as rm7, stat as stat7 } from "fs/promises";
42858
+ import { tmpdir as tmpdir3 } from "os";
42859
+ import path37 from "path";
42677
42860
  import { defineCommand as defineCommand200 } from "citty";
42861
+
42862
+ // src/lib/streamUpload.ts
42863
+ import { createHash as createHash2 } from "crypto";
42864
+ import { createReadStream } from "fs";
42865
+ import { Readable } from "stream";
42866
+ import { pipeline } from "stream/promises";
42867
+ async function hashFile(filePath) {
42868
+ const hash = createHash2("sha256");
42869
+ await pipeline(createReadStream(filePath), hash);
42870
+ return hash.digest("hex");
42871
+ }
42872
+ async function putFileStreamed(args) {
42873
+ const body = Readable.toWeb(createReadStream(args.filePath));
42874
+ return await fetch(args.putUrl, {
42875
+ method: "PUT",
42876
+ body,
42877
+ headers: {
42878
+ "Content-Type": args.contentType,
42879
+ "Content-Length": String(args.sizeBytes)
42880
+ },
42881
+ duplex: "half"
42882
+ });
42883
+ }
42884
+
42885
+ // src/commands/videos/videoPlatforms.ts
42886
+ var PLATFORM_DOMAINS = {
42887
+ youtube: ["youtube.com", "youtu.be"],
42888
+ instagram: ["instagram.com", "ddinstagram.com"],
42889
+ tiktok: ["tiktok.com"],
42890
+ twitter: ["twitter.com", "x.com", "vxtwitter.com", "fixvx.com"],
42891
+ facebook: ["facebook.com", "fb.watch", "fb.com"],
42892
+ reddit: ["reddit.com", "redd.it"],
42893
+ vimeo: ["vimeo.com"],
42894
+ dailymotion: ["dailymotion.com", "dai.ly"],
42895
+ twitch: ["twitch.tv"],
42896
+ bluesky: ["bsky.app"],
42897
+ bilibili: ["bilibili.com", "bilibili.tv", "b23.tv"],
42898
+ vk: ["vk.com", "vk.ru", "vkvideo.ru"],
42899
+ ok: ["ok.ru"],
42900
+ pinterest: ["pinterest.com", "pin.it"],
42901
+ tumblr: ["tumblr.com"],
42902
+ snapchat: ["snapchat.com"],
42903
+ loom: ["loom.com"],
42904
+ streamable: ["streamable.com"],
42905
+ newgrounds: ["newgrounds.com"],
42906
+ rutube: ["rutube.ru"]
42907
+ };
42908
+ var VIDEO_PLATFORMS = Object.keys(PLATFORM_DOMAINS);
42909
+ function platformFromUrl(rawUrl) {
42910
+ let host;
42911
+ try {
42912
+ host = new URL(rawUrl).hostname.toLowerCase();
42913
+ } catch {
42914
+ return "url";
42915
+ }
42916
+ for (const [platform, domains] of Object.entries(PLATFORM_DOMAINS)) {
42917
+ if (domains.some((domain) => host === domain || host.endsWith(`.${domain}`))) {
42918
+ return platform;
42919
+ }
42920
+ }
42921
+ return "url";
42922
+ }
42923
+
42924
+ // src/commands/videos/ingestUrl.ts
42925
+ var MAX_VIDEO_INGEST_BYTES = 16 * 1024 * 1024 * 1024;
42926
+ var MAX_VIDEO_INGEST_SECONDS = 3 * 60 * 60;
42927
+ function refuseByDuration(seconds, maxSeconds) {
42928
+ if (seconds === void 0 || seconds <= maxSeconds) return null;
42929
+ return `That video is ${(seconds / 3600).toFixed(1)} hours long \u2014 the limit is ${Math.round(maxSeconds / 3600)} hours.`;
42930
+ }
42931
+ var DOWNLOAD_TIMEOUT_FLOOR_MS = 10 * 60 * 1e3;
42932
+ var DOWNLOAD_TIMEOUT_CEILING_MS = 15 * 60 * 1e3;
42933
+ function downloadTimeoutMs(seconds) {
42934
+ if (seconds === void 0) return DOWNLOAD_TIMEOUT_FLOOR_MS;
42935
+ const scaled = seconds * 2 * 1e3;
42936
+ return Math.min(Math.max(scaled, DOWNLOAD_TIMEOUT_FLOOR_MS), DOWNLOAD_TIMEOUT_CEILING_MS);
42937
+ }
42938
+ var INGEST_MAX_HEIGHT = 1080;
42939
+ var DIRECT_MEDIA_EXTENSIONS = [".mp4", ".mov", ".webm", ".m4v", ".mkv", ".avi"];
42940
+ function isDirectMediaUrl(rawUrl) {
42941
+ const pathname = safePathname(rawUrl);
42942
+ return DIRECT_MEDIA_EXTENSIONS.some((ext) => pathname.endsWith(ext));
42943
+ }
42944
+ function resolveIngestSource(explicit, rawUrl) {
42945
+ if (explicit) return explicit;
42946
+ return platformFromUrl(rawUrl);
42947
+ }
42948
+ var TOOLING_OR_BLOCK_SIGNATURES = [
42949
+ "requested format is not available",
42950
+ "only images are available",
42951
+ "nsig",
42952
+ "sign in to confirm",
42953
+ // bot check — distinct from "Private video. Sign in if…"
42954
+ "unable to download api page",
42955
+ "http error 403",
42956
+ // yt-dlp's own "the site changed under me" signature. It prints a GitHub issue
42957
+ // link whenever a page parsed nothing like the shape the extractor expects —
42958
+ // which is precisely a downloader that has aged out, not a bad link. Observed
42959
+ // live on TikTok, which probed fine and then broke ninety minutes later from
42960
+ // the same machine, identically through direct, datacenter and residential
42961
+ // exits. Without this it fell through to "check the link is public", sending
42962
+ // the reader to inspect a video that was never the problem.
42963
+ "please report this issue"
42964
+ ];
42965
+ function isToolingOrBlockFailure(detail) {
42966
+ const haystack = detail.toLowerCase();
42967
+ return TOOLING_OR_BLOCK_SIGNATURES.some((signature) => haystack.includes(signature));
42968
+ }
42969
+ var REGION_BLOCK_SIGNATURES = [
42970
+ "in your country",
42971
+ "geo-restricted",
42972
+ "geo restricted",
42973
+ "not available from your location",
42974
+ "blocked in your country"
42975
+ ];
42976
+ function isDrmProtected(detail) {
42977
+ return detail.toLowerCase().includes("drm protected");
42978
+ }
42979
+ function isRegionBlocked(detail) {
42980
+ const haystack = detail.toLowerCase();
42981
+ return REGION_BLOCK_SIGNATURES.some((signature) => haystack.includes(signature));
42982
+ }
42983
+ function worthDownloadingInstead(errorCode) {
42984
+ return errorCode === "VALIDATION_ERROR";
42985
+ }
42986
+ function directDedupeKey(source, rawUrl) {
42987
+ return `${source}:${rawUrl}`;
42988
+ }
42989
+ function safePathname(rawUrl) {
42990
+ try {
42991
+ return new URL(rawUrl).pathname.toLowerCase();
42992
+ } catch {
42993
+ return "";
42994
+ }
42995
+ }
42996
+
42997
+ // src/commands/videos/ingest.ts
42998
+ registerSchema({
42999
+ command: "videos.ingest",
43000
+ description: "Add a video to the library from a URL \u2014 a direct file, or a YouTube/TikTok/Vimeo page.",
43001
+ args: {
43002
+ url: { type: "string", description: "Video URL (direct file or a watch/post page)", required: true },
43003
+ source: { type: "string", description: "Provenance: url | youtube", required: false },
43004
+ "external-id": { type: "string", description: "Provider asset id (deduped on)", required: false },
43005
+ "external-url": { type: "string", description: "Canonical page URL", required: false },
43006
+ download: {
43007
+ type: "boolean",
43008
+ description: "Force the download path instead of letting Baker fetch the URL directly",
43009
+ required: false,
43010
+ default: false
43011
+ },
43012
+ country: {
43013
+ type: "string",
43014
+ description: "Two-letter country to fetch from (e.g. US, DE, JP) \u2014 use when a video is blocked in your region",
43015
+ required: false
43016
+ },
43017
+ context: {
43018
+ type: "string",
43019
+ description: "What you know that the video cannot show \u2014 the campaign, whether the person on camera is a real customer, what it is for. Highest-leverage flag here: it is what makes the clip findable later.",
43020
+ required: false
43021
+ },
43022
+ "dry-run": { type: "boolean", description: "Preview the operation without executing", required: false }
43023
+ }
43024
+ });
43025
+ var ingestCommand2 = defineCommand200({
43026
+ meta: {
43027
+ name: "ingest",
43028
+ description: "Add a video to the library from a URL. A direct file URL is handed straight to Baker, which fetches it. A page URL (YouTube, TikTok, Vimeo, Instagram) is downloaded here first, then uploaded \u2014 and a direct URL that Baker cannot fetch falls back to that same path automatically.\n\nExample: baker videos ingest https://www.youtube.com/watch?v=abc123"
43029
+ },
43030
+ args: {
43031
+ url: { type: "positional", description: "Video URL", required: false },
43032
+ source: { type: "string", description: "Provenance: url | youtube", required: false },
43033
+ "external-id": { type: "string", description: "Provider asset id", required: false },
43034
+ "external-url": { type: "string", description: "Canonical page URL", required: false },
43035
+ download: { type: "boolean", description: "Force the download path", required: false, default: false },
43036
+ country: { type: "string", description: "Two-letter country to fetch from, e.g. US", required: false },
43037
+ context: {
43038
+ type: "string",
43039
+ description: "Context for the analysis \u2014 campaign, who is on camera, intended use",
43040
+ required: false
43041
+ },
43042
+ "dry-run": { type: "boolean", description: "Preview without executing", required: false, default: false }
43043
+ },
43044
+ run: ({ args }) => runVideoIngest({
43045
+ url: args.url,
43046
+ source: args.source,
43047
+ externalId: args["external-id"],
43048
+ externalUrl: args["external-url"],
43049
+ forceDownload: args.download === true,
43050
+ country: args.country,
43051
+ context: args.context,
43052
+ dryRun: args["dry-run"] === true
43053
+ })
43054
+ });
43055
+ async function runVideoIngest(opts) {
43056
+ const url = opts.url;
43057
+ if (!url) {
43058
+ writeJson({ ok: false, error: { code: "VALIDATION_ERROR", message: "URL is required" } });
43059
+ process.exit(1);
43060
+ }
43061
+ const direct = !opts.forceDownload && isDirectMediaUrl(url);
43062
+ const country = opts.country?.trim() || void 0;
43063
+ const args = {
43064
+ url,
43065
+ source: resolveIngestSource(opts.source, url),
43066
+ externalId: opts.externalId,
43067
+ // Deliberately NOT pre-filled with the pasted URL. Doing so shadowed the
43068
+ // canonical URL the probe resolves (`?? canonicalUrl(...)` below could never
43069
+ // run), so the same video filed under a different `externalUrl` depending on
43070
+ // whether the link was copied from a share sheet, a shorts page or the
43071
+ // address bar. `downloadThenIngest` falls back to `args.url` when the probe
43072
+ // resolves nothing, which is the value this used to hard-code.
43073
+ externalUrl: opts.externalUrl,
43074
+ descriptionContext: opts.context?.trim() || void 0
43075
+ };
43076
+ const validation = videosIngestRequestSchema.safeParse({ ...args, url });
43077
+ if (!validation.success) {
43078
+ const problem = validation.error.issues[0];
43079
+ writeJson({
43080
+ ok: false,
43081
+ error: {
43082
+ code: "VALIDATION_ERROR",
43083
+ message: problem ? `${problem.path.join(".") || "request"}: ${problem.message}` : "Invalid request"
43084
+ }
43085
+ });
43086
+ process.exit(1);
43087
+ }
43088
+ if (opts.dryRun) {
43089
+ reportDryRun(args, direct, country);
43090
+ return;
43091
+ }
43092
+ try {
43093
+ writeJson({ ok: true, data: await ingestByRoute(args, direct, country) });
43094
+ } catch (err) {
43095
+ reportIngestFailure(err);
43096
+ }
43097
+ }
43098
+ function reportDryRun(args, direct, country) {
43099
+ writeJson({
43100
+ ok: true,
43101
+ dryRun: true,
43102
+ operation: "videos.ingest",
43103
+ params: {
43104
+ url: args.url,
43105
+ source: args.source,
43106
+ // Known up front on the direct route, because it is derived from the URL
43107
+ // alone. The download route's key comes from the probe, which a dry run
43108
+ // does not perform, so there it stays null rather than becoming a guess.
43109
+ externalId: args.externalId ?? (direct ? directDedupeKey(args.source, args.url) : null),
43110
+ // A page URL's canonical form is only known after the probe, which a dry
43111
+ // run does not perform — so this is null rather than a guess.
43112
+ externalUrl: args.externalUrl ?? null,
43113
+ direct,
43114
+ country: country ?? null,
43115
+ descriptionContext: args.descriptionContext ?? null
43116
+ }
43117
+ });
43118
+ }
43119
+ async function ingestByRoute(args, direct, country) {
43120
+ if (!direct) {
43121
+ return { ...await downloadThenIngest(args, country), via: "download" };
43122
+ }
43123
+ try {
43124
+ const externalId = args.externalId ?? directDedupeKey(args.source, args.url);
43125
+ return { ...await ingestUrl({ ...args, externalId }), via: "direct" };
43126
+ } catch (err) {
43127
+ if (!(err instanceof ApiError) || !worthDownloadingInstead(err.code)) throw err;
43128
+ return { ...await downloadThenIngest(args, country), via: "download", fallbackFrom: err.message };
43129
+ }
43130
+ }
43131
+ function reportYtDlpFailure(err) {
43132
+ const detail = `${err.stderrTail} ${err.message}`;
43133
+ if (isProxyFailure(ytDlpBlockSignal(detail))) {
43134
+ fail7(
43135
+ "Couldn't download that video: our connection to the internet was refused.",
43136
+ "This is Baker's egress proxy, not the link \u2014 its credentials look wrong or expired. The link is probably fine. Report it; retrying won't help until the proxy is fixed."
43137
+ );
43138
+ }
43139
+ if (isDrmProtected(detail)) {
43140
+ fail7(
43141
+ "That video is copy-protected, so it can't be added to the library.",
43142
+ "The publisher encrypted this one against downloading. Nothing will change that \u2014 not retrying, not a different region. Ask whoever owns the video for the original file and add it with `baker videos upload <file>`."
43143
+ );
43144
+ }
43145
+ if (isVimeoAuthFailure(detail)) {
43146
+ fail7(
43147
+ "Vimeo wouldn't hand over that video \u2014 Baker isn't signed in to Vimeo.",
43148
+ "Vimeo now refuses anonymous downloads entirely, so this needs a signed-in session and the one Baker holds has either expired or was never set. Nothing is wrong with the link. Report it so the Vimeo cookie can be refreshed; meanwhile download the file and use `baker videos upload <file>`."
43149
+ );
43150
+ }
43151
+ if (isRegionBlocked(detail)) {
43152
+ fail7(
43153
+ `That video isn't available in the region we're fetching from. ${err.stderrTail || err.message}`,
43154
+ "Re-run with `--country <two-letter code>` for a country where the video plays \u2014 e.g. `--country US`. Pick the one the video belongs to (the uploader's country is the usual answer); each attempt costs a paid connection, so choose rather than work through the list."
43155
+ );
43156
+ }
43157
+ fail7(
43158
+ `Couldn't download that video. ${err.stderrTail || err.message}`,
43159
+ isToolingOrBlockFailure(detail) ? "The site wouldn't hand over the video \u2014 usually the downloader being out of date against a site that changed, or the request being blocked. Retrying won't help, and the link is probably fine. Report it so the tool can be updated; meanwhile download the file and use `baker videos upload <file>`." : "Check the link is public and playable. Private or age-restricted videos can't be downloaded \u2014 download the file yourself and use `baker videos upload <file>`."
43160
+ );
43161
+ }
43162
+ function fail7(message, fix, code = "INGEST_FAILED") {
43163
+ writeJson({ ok: false, error: { code, message, fix } });
43164
+ process.exit(1);
43165
+ }
43166
+ function reportIngestFailure(err) {
43167
+ if (err instanceof ApiError) {
43168
+ writeJson({ ok: false, error: { code: err.code, message: err.message } });
43169
+ process.exit(1);
43170
+ }
43171
+ if (err instanceof YtDlpError) reportYtDlpFailure(err);
43172
+ const detail = err instanceof Error ? err.message : String(err);
43173
+ if (detail.includes("timed out after")) {
43174
+ fail7(
43175
+ "That video took too long to download and was stopped part-way.",
43176
+ "Long or slow-serving videos can outrun the download window. Try a shorter clip, or download this one yourself and use `baker videos upload <file>`.",
43177
+ "INTERNAL_ERROR"
43178
+ );
43179
+ }
43180
+ fail7(
43181
+ `Couldn't add that video. ${detail}`,
43182
+ "This is a fault on Baker's side, not a problem with the link. Report it.",
43183
+ "INTERNAL_ERROR"
43184
+ );
43185
+ }
43186
+ function dedupeKey(args, info) {
43187
+ const id = typeof info.id === "string" ? info.id : void 0;
43188
+ if (!id) return void 0;
43189
+ if (info.extractor === "generic") return `${args.source}:${canonicalUrl(info) ?? args.url}`;
43190
+ return `${args.source}:${id}`;
43191
+ }
43192
+ function ingestUrl(args) {
43193
+ const body = {
43194
+ url: args.url,
43195
+ source: args.source
43196
+ };
43197
+ if (args.externalId) body.externalId = args.externalId;
43198
+ if (args.externalUrl) body.externalUrl = args.externalUrl;
43199
+ if (args.descriptionContext) body.descriptionContext = args.descriptionContext;
43200
+ return apiPost("/api/videos/ingest", body);
43201
+ }
43202
+ async function downloadThenIngest(args, country) {
43203
+ const vimeoCookie = captureVimeoCookie();
43204
+ const workDir = await mkdtemp2(path37.join(tmpdir3(), "videos-ingest-"));
43205
+ try {
43206
+ const probe = await probeYtDlp({ url: args.url, country, vimeoCookie, cookieDir: workDir });
43207
+ if (isAudioOnly(probe.info)) {
43208
+ throw new ApiError(
43209
+ "VALIDATION_ERROR",
43210
+ "That link is audio, not video, so it can't go in the video library. Music and podcast links have no picture to store."
43211
+ );
43212
+ }
43213
+ const tooLong = refuseByDuration(durationSeconds(probe.info), MAX_VIDEO_INGEST_SECONDS);
43214
+ if (tooLong) {
43215
+ throw new ApiError("VALIDATION_ERROR", tooLong);
43216
+ }
43217
+ const externalId = args.externalId ?? dedupeKey(args, probe.info);
43218
+ const { filePath, tier } = await runYtDlp({
43219
+ url: args.url,
43220
+ audioOnly: false,
43221
+ workDir,
43222
+ maxHeight: INGEST_MAX_HEIGHT,
43223
+ country,
43224
+ vimeoCookie,
43225
+ // Budgeted from the probed length, so the duration we accept and the time
43226
+ // we allow to fetch it cannot drift apart.
43227
+ timeoutMs: downloadTimeoutMs(durationSeconds(probe.info))
43228
+ });
43229
+ const stats = await stat7(filePath);
43230
+ if (stats.size > MAX_VIDEO_INGEST_BYTES) {
43231
+ throw new ApiError(
43232
+ "VALIDATION_ERROR",
43233
+ `That video is ${(stats.size / 1024 ** 3).toFixed(1)} GB, over the ${Math.round(MAX_VIDEO_INGEST_BYTES / 1024 ** 3)} GB limit for this route.`
43234
+ );
43235
+ }
43236
+ const publicUrl = await uploadToAssetStore(filePath, stats.size);
43237
+ return {
43238
+ ...await ingestUrl({
43239
+ ...args,
43240
+ url: publicUrl,
43241
+ externalId,
43242
+ externalUrl: args.externalUrl ?? canonicalUrl(probe.info) ?? args.url
43243
+ }),
43244
+ egress: tier,
43245
+ // Named in the result for the same reason `egress` is: a geo-targeted exit
43246
+ // is a deliberate, billable choice and must not be invisible afterwards.
43247
+ ...country ? { country } : {}
43248
+ };
43249
+ } finally {
43250
+ await rm7(workDir, { recursive: true, force: true }).catch(() => {
43251
+ });
43252
+ }
43253
+ }
43254
+ async function uploadToAssetStore(filePath, sizeBytes) {
43255
+ const sha256 = await hashFile(filePath);
43256
+ const { putUrl, publicUrl } = await apiPost("/api/canvas/assets/presign", {
43257
+ sha256,
43258
+ mime: "video/mp4",
43259
+ purpose: "source"
43260
+ });
43261
+ const putRes = await putFileStreamed({ putUrl, filePath, contentType: "video/mp4", sizeBytes });
43262
+ if (!putRes.ok) {
43263
+ throw new ApiError("INTERNAL_ERROR", `Storing the downloaded video failed: HTTP ${putRes.status}`);
43264
+ }
43265
+ return publicUrl;
43266
+ }
43267
+
43268
+ // src/commands/videos/search.ts
43269
+ import { defineCommand as defineCommand201 } from "citty";
42678
43270
  registerSchema({
42679
43271
  command: "videos.search",
42680
43272
  description: "Search videos by text query. Only returns ready videos.",
@@ -42684,7 +43276,7 @@ registerSchema({
42684
43276
  tags: { type: "string", description: "Comma-separated tags to filter by", required: false }
42685
43277
  }
42686
43278
  });
42687
- var searchCommand4 = defineCommand200({
43279
+ var searchCommand4 = defineCommand201({
42688
43280
  meta: {
42689
43281
  name: "search",
42690
43282
  description: "Semantic search videos by text query. Uses hybrid BM25 + vector + reranking. Example: baker videos search 'product demo' --tags tutorial"
@@ -42734,9 +43326,9 @@ var searchCommand4 = defineCommand200({
42734
43326
  var tagsCommand5 = makeTagsCommand("videos", "video", "/api/videos/tags");
42735
43327
 
42736
43328
  // src/commands/videos/upload.ts
42737
- import { readFile as readFile24, stat as stat7 } from "fs/promises";
43329
+ import { readFile as readFile24, stat as stat8 } from "fs/promises";
42738
43330
  import { basename as basename3, extname as extname4 } from "path";
42739
- import { defineCommand as defineCommand201 } from "citty";
43331
+ import { defineCommand as defineCommand202 } from "citty";
42740
43332
  var MIME_MAP = {
42741
43333
  ".mp4": "video/mp4",
42742
43334
  ".mov": "video/quicktime",
@@ -42746,9 +43338,9 @@ var MIME_MAP = {
42746
43338
  };
42747
43339
  registerSchema({
42748
43340
  command: "videos.upload",
42749
- description: "Upload a video file to Baker (via Mux direct upload)",
43341
+ description: "Upload a video to Baker \u2014 a local file (via Mux direct upload) or a remote http(s) URL.",
42750
43342
  args: {
42751
- file: { type: "string", description: "Path to the video file", required: true },
43343
+ file: { type: "string", description: "Local video file path, or a remote http(s) URL", required: true },
42752
43344
  "content-type": {
42753
43345
  type: "string",
42754
43346
  description: "MIME type (auto-detected from extension if omitted)",
@@ -42775,33 +43367,50 @@ function detectContentType(filePath) {
42775
43367
  }
42776
43368
  return mime;
42777
43369
  }
42778
- var uploadCommand2 = defineCommand201({
43370
+ function isRemoteUrl3(value) {
43371
+ return /^https?:\/\//i.test(value);
43372
+ }
43373
+ var uploadCommand2 = defineCommand202({
42779
43374
  meta: {
42780
43375
  name: "upload",
42781
- description: "Upload a video file to Baker via Mux direct upload. Auto-detects content type. Example: baker videos upload ./demo.mp4"
43376
+ description: "Upload a video to Baker \u2014 accepts a local file path OR a remote http(s) URL.\n\nLocal: auto-detects content type and uploads via Mux direct upload.\nRemote: hands off to `videos ingest` (direct fetch, or download-then-upload for a YouTube/TikTok/Vimeo page).\n\nExamples:\n baker videos upload ./demo.mp4\n baker videos upload https://www.youtube.com/watch?v=abc123"
42782
43377
  },
42783
43378
  args: {
42784
- file: { type: "positional", description: "Path to the video file", required: false },
42785
- "content-type": { type: "string", description: "MIME type (auto-detected if omitted)", required: false },
43379
+ file: { type: "positional", description: "Local video file path or remote http(s) URL", required: false },
43380
+ "content-type": { type: "string", description: "MIME type (local only \u2014 auto-detected)", required: false },
42786
43381
  context: {
42787
43382
  type: "string",
42788
43383
  description: "What this clip is and what it is for \u2014 who is on camera, which campaign, how it was shot. Start here: the analysis reads it, so it is what makes the clip findable later.",
42789
43384
  required: false
42790
43385
  },
43386
+ source: { type: "string", description: "Provenance (URL mode only): url | youtube", required: false },
43387
+ "external-id": { type: "string", description: "Provider asset id (URL mode only)", required: false },
43388
+ "external-url": { type: "string", description: "Canonical page URL (URL mode only)", required: false },
42791
43389
  "dry-run": { type: "boolean", description: "Preview without executing", required: false, default: false }
42792
43390
  },
42793
43391
  run: async ({ args }) => {
42794
43392
  try {
42795
43393
  const filePath = args.file;
42796
43394
  if (!filePath) {
42797
- writeJson({ ok: false, error: { code: "VALIDATION_ERROR", message: "File path is required" } });
43395
+ writeJson({ ok: false, error: { code: "VALIDATION_ERROR", message: "File path or URL is required" } });
42798
43396
  process.exit(1);
42799
43397
  }
43398
+ if (isRemoteUrl3(filePath)) {
43399
+ await runVideoIngest({
43400
+ url: filePath,
43401
+ source: args.source,
43402
+ externalId: args["external-id"],
43403
+ externalUrl: args["external-url"],
43404
+ context: args.context,
43405
+ dryRun: args["dry-run"] === true
43406
+ });
43407
+ return;
43408
+ }
42800
43409
  const contentType = args["content-type"] || detectContentType(filePath);
42801
43410
  const originalFilename = basename3(filePath);
42802
43411
  const descriptionContext = args.context;
42803
43412
  if (args["dry-run"]) {
42804
- const fileStats = await stat7(filePath);
43413
+ const fileStats = await stat8(filePath);
42805
43414
  writeJson({
42806
43415
  ok: true,
42807
43416
  dryRun: true,
@@ -42845,16 +43454,17 @@ var uploadCommand2 = defineCommand201({
42845
43454
  });
42846
43455
 
42847
43456
  // src/commands/videos/index.ts
42848
- var videosCommand = defineCommand202({
43457
+ var videosCommand = defineCommand203({
42849
43458
  meta: {
42850
43459
  name: "videos",
42851
- description: `Find and manage videos in Baker. Subcommands: search, get, upload, delete, tags.
43460
+ description: `Find and manage videos in Baker. Subcommands: search, get, upload, ingest, delete, tags.
42852
43461
 
42853
43462
  Examples:
42854
43463
  baker videos search "product demo" --limit 5
42855
43464
  baker videos search "tutorial" --tags explainer
42856
43465
  baker videos get <video-id>
42857
43466
  baker videos upload ./demo.mp4
43467
+ baker videos ingest https://www.youtube.com/watch?v=abc123
42858
43468
  baker videos delete <video-id> --dry-run
42859
43469
  baker videos tags
42860
43470
  Full guide: __tooling__/docs/tools/baker/videos.md`
@@ -42864,16 +43474,17 @@ Full guide: __tooling__/docs/tools/baker/videos.md`
42864
43474
  group: groupCommand2,
42865
43475
  search: searchCommand4,
42866
43476
  upload: uploadCommand2,
43477
+ ingest: ingestCommand2,
42867
43478
  delete: deleteCommand3,
42868
43479
  tags: tagsCommand5
42869
43480
  }
42870
43481
  });
42871
43482
 
42872
43483
  // src/commands/winning-ads/index.ts
42873
- import { defineCommand as defineCommand215 } from "citty";
43484
+ import { defineCommand as defineCommand216 } from "citty";
42874
43485
 
42875
43486
  // src/commands/winning-ads/advertisers.ts
42876
- import { defineCommand as defineCommand203 } from "citty";
43487
+ import { defineCommand as defineCommand204 } from "citty";
42877
43488
 
42878
43489
  // src/commands/winning-ads/shared.ts
42879
43490
  function splitList2(value) {
@@ -42926,7 +43537,7 @@ function advertiserNormalizer(record, full) {
42926
43537
  last_synced_at: record.last_synced_at ?? null
42927
43538
  };
42928
43539
  }
42929
- var advertisersCommand2 = defineCommand203({
43540
+ var advertisersCommand2 = defineCommand204({
42930
43541
  meta: {
42931
43542
  name: "advertisers",
42932
43543
  description: 'List corpus advertisers by name or domain. Find your own advertiser for --exclude-advertiser, or a competitor for --advertiser-id / winners. Example: baker winning-ads advertisers "Deel" --output md'
@@ -42984,7 +43595,7 @@ var advertisersCommand2 = defineCommand203({
42984
43595
  });
42985
43596
 
42986
43597
  // src/commands/winning-ads/brief.ts
42987
- import { defineCommand as defineCommand204 } from "citty";
43598
+ import { defineCommand as defineCommand205 } from "citty";
42988
43599
  registerSchema({
42989
43600
  command: "winning-ads.brief",
42990
43601
  description: "Generate a creative brief grounded in strategically-similar winning ads. Optionally describe the target creative with --dna (JSON) and steer with --notes.",
@@ -43030,7 +43641,7 @@ function parseDna(raw) {
43030
43641
  }
43031
43642
  return parsed;
43032
43643
  }
43033
- var briefCommand = defineCommand204({
43644
+ var briefCommand = defineCommand205({
43034
43645
  meta: {
43035
43646
  name: "brief",
43036
43647
  description: `Generate a creative brief from winning references. Example: baker winning-ads brief --dna '{"angle":"cost savings"}' --notes "B2B, LinkedIn video" --k 8`
@@ -43066,7 +43677,7 @@ var briefCommand = defineCommand204({
43066
43677
  });
43067
43678
 
43068
43679
  // src/commands/winning-ads/content.ts
43069
- import { defineCommand as defineCommand205 } from "citty";
43680
+ import { defineCommand as defineCommand206 } from "citty";
43070
43681
  registerSchema({
43071
43682
  command: "winning-ads.content",
43072
43683
  description: "Read what's INSIDE one winning ad: the spoken transcript, the on-screen text, and the ad copy. Use this after `search`/`winners`/`feed` return a shortlist \u2014 pass an ad_id to understand a reference before reproducing it. Add --full for speech, pacing, and soundtrack detail. Video ads carry the transcript/on-screen text; static ads carry only the copy.",
@@ -43079,7 +43690,7 @@ registerSchema({
43079
43690
  }
43080
43691
  }
43081
43692
  });
43082
- var contentCommand = defineCommand205({
43693
+ var contentCommand = defineCommand206({
43083
43694
  meta: {
43084
43695
  name: "content",
43085
43696
  description: "Read the transcript + on-screen text + copy of one winning ad. Example: baker winning-ads content adg_123 --platform meta --full --output md"
@@ -43128,7 +43739,7 @@ var contentCommand = defineCommand205({
43128
43739
  });
43129
43740
 
43130
43741
  // src/commands/winning-ads/feed.ts
43131
- import { defineCommand as defineCommand206 } from "citty";
43742
+ import { defineCommand as defineCommand207 } from "citty";
43132
43743
  function buildFeedParams(input) {
43133
43744
  const params = {};
43134
43745
  const advertiser = splitList2(input.advertiser);
@@ -43180,7 +43791,7 @@ registerSchema({
43180
43791
  format: { type: "string", description: "Comma-separated formats to include (e.g. static,video)", required: false }
43181
43792
  }
43182
43793
  });
43183
- var feedCommand = defineCommand206({
43794
+ var feedCommand = defineCommand207({
43184
43795
  meta: {
43185
43796
  name: "feed",
43186
43797
  description: "Winners across every brand you follow (browse, then trim per advertiser). Example: baker winning-ads feed --per-advertiser 5 --output md"
@@ -43265,7 +43876,7 @@ var feedCommand = defineCommand206({
43265
43876
  });
43266
43877
 
43267
43878
  // src/commands/winning-ads/follow.ts
43268
- import { defineCommand as defineCommand207 } from "citty";
43879
+ import { defineCommand as defineCommand208 } from "citty";
43269
43880
  var PLATFORMS = ["meta", "linkedin"];
43270
43881
  registerSchema({
43271
43882
  command: "winning-ads.follow",
@@ -43280,7 +43891,7 @@ registerSchema({
43280
43891
  label: { type: "string", description: "Optional display label (defaults to the resolved name)", required: false }
43281
43892
  }
43282
43893
  });
43283
- var followCommand = defineCommand207({
43894
+ var followCommand = defineCommand208({
43284
43895
  meta: {
43285
43896
  name: "follow",
43286
43897
  description: 'Follow a brand to track ALL its ads \u2014 every platform and country. --platform is how we read your input, not a limit. A domain tracks both Meta + LinkedIn. Example: baker winning-ads follow "deel.com" --platform meta'
@@ -43327,7 +43938,7 @@ var followCommand = defineCommand207({
43327
43938
  });
43328
43939
 
43329
43940
  // src/commands/winning-ads/follow-competitors.ts
43330
- import { defineCommand as defineCommand208 } from "citty";
43941
+ import { defineCommand as defineCommand209 } from "citty";
43331
43942
  var PLATFORMS2 = ["meta", "linkedin"];
43332
43943
  var BATCH_TIMEOUT_MS = 3e5;
43333
43944
  function buildFollowBatchBody(input) {
@@ -43360,7 +43971,7 @@ registerSchema({
43360
43971
  }
43361
43972
  }
43362
43973
  });
43363
- var followCompetitorsCommand = defineCommand208({
43974
+ var followCompetitorsCommand = defineCommand209({
43364
43975
  meta: {
43365
43976
  name: "follow-competitors",
43366
43977
  description: 'Follow many brands at once by domain \u2014 add every competitor in one call. Example: baker winning-ads follow-competitors "deel.com,notion.so,hubspot.com"'
@@ -43435,7 +44046,7 @@ var followCompetitorsCommand = defineCommand208({
43435
44046
  });
43436
44047
 
43437
44048
  // src/commands/winning-ads/following.ts
43438
- import { defineCommand as defineCommand209 } from "citty";
44049
+ import { defineCommand as defineCommand210 } from "citty";
43439
44050
  registerSchema({
43440
44051
  command: "winning-ads.following",
43441
44052
  description: "List the brands you follow in your ad-dna library, with each one's status (ready vs still adding) and cached ad counts.",
@@ -43468,7 +44079,7 @@ function followingNormalizer(record, full) {
43468
44079
  platforms: Array.isArray(record.platforms) ? record.platforms : []
43469
44080
  };
43470
44081
  }
43471
- var followingCommand = defineCommand209({
44082
+ var followingCommand = defineCommand210({
43472
44083
  meta: {
43473
44084
  name: "following",
43474
44085
  description: "List brands you follow, with status (ready / adding\u2026) and cached counts. Example: baker winning-ads following --output md"
@@ -43503,7 +44114,7 @@ var followingCommand = defineCommand209({
43503
44114
  });
43504
44115
 
43505
44116
  // src/commands/winning-ads/patterns.ts
43506
- import { defineCommand as defineCommand210 } from "citty";
44117
+ import { defineCommand as defineCommand211 } from "citty";
43507
44118
  registerSchema({
43508
44119
  command: "winning-ads.patterns",
43509
44120
  description: "Mine what separates two cohorts of ads: pass a comma-list of winning ad ids (--winners) and a comma-list of weaker ad ids (--duds). Returns the discriminating DNA fields.",
@@ -43542,7 +44153,7 @@ function discriminatorRow(record) {
43542
44153
  top_values_duds: Array.isArray(record.top_values_b) ? record.top_values_b.join(", ") : ""
43543
44154
  };
43544
44155
  }
43545
- var patternsCommand = defineCommand210({
44156
+ var patternsCommand = defineCommand211({
43546
44157
  meta: {
43547
44158
  name: "patterns",
43548
44159
  description: "Discover what separates winning ads from weak ones. Example: baker winning-ads patterns --winners a_1,a_2,a_3 --duds a_9,a_8 --output md"
@@ -43598,7 +44209,7 @@ var patternsCommand = defineCommand210({
43598
44209
  });
43599
44210
 
43600
44211
  // src/commands/winning-ads/search.ts
43601
- import { defineCommand as defineCommand211 } from "citty";
44212
+ import { defineCommand as defineCommand212 } from "citty";
43602
44213
  registerSchema({
43603
44214
  command: "winning-ads.search",
43604
44215
  description: "Search the ad-dna corpus of scored winning ads. Returns a lean shortlist (advertiser, summary, scores, media_url) to pick a reference to reproduce.",
@@ -43706,7 +44317,7 @@ function buildSearchBody2(args) {
43706
44317
  }
43707
44318
  return body;
43708
44319
  }
43709
- var searchCommand5 = defineCommand211({
44320
+ var searchCommand5 = defineCommand212({
43710
44321
  meta: {
43711
44322
  name: "search",
43712
44323
  description: "Search winning reference ads. Example: baker winning-ads search 'B2B SaaS before/after AI automation' --platform meta --format static --winner-category winner --exclude-advertiser adv_123 --output md"
@@ -43821,7 +44432,7 @@ var searchCommand5 = defineCommand211({
43821
44432
  });
43822
44433
 
43823
44434
  // src/commands/winning-ads/seeds.ts
43824
- import { defineCommand as defineCommand212 } from "citty";
44435
+ import { defineCommand as defineCommand213 } from "citty";
43825
44436
  function leanRow(r) {
43826
44437
  return {
43827
44438
  key: r.key,
@@ -43849,7 +44460,7 @@ function makeSeedCommand(opts) {
43849
44460
  limit: { type: "number", description: "Max keys 1-100 (default 20)", required: false, default: 20 }
43850
44461
  }
43851
44462
  });
43852
- return defineCommand212({
44463
+ return defineCommand213({
43853
44464
  meta: { name: opts.name, description: opts.description },
43854
44465
  args: {
43855
44466
  platform: { type: "string", description: "Single platform to segment on", required: false },
@@ -43898,7 +44509,7 @@ var formatsCommand = makeSeedCommand({
43898
44509
  });
43899
44510
 
43900
44511
  // src/commands/winning-ads/unfollow.ts
43901
- import { defineCommand as defineCommand213 } from "citty";
44512
+ import { defineCommand as defineCommand214 } from "citty";
43902
44513
  registerSchema({
43903
44514
  command: "winning-ads.unfollow",
43904
44515
  description: "Stop following a brand \u2014 removes it from your ad-dna library by advertiser id.",
@@ -43906,7 +44517,7 @@ registerSchema({
43906
44517
  advertiser: { type: "string", description: "Advertiser id to unfollow", required: true }
43907
44518
  }
43908
44519
  });
43909
- var unfollowCommand = defineCommand213({
44520
+ var unfollowCommand = defineCommand214({
43910
44521
  meta: {
43911
44522
  name: "unfollow",
43912
44523
  description: "Stop following a brand by advertiser id. Example: baker winning-ads unfollow adv_123"
@@ -43927,7 +44538,7 @@ var unfollowCommand = defineCommand213({
43927
44538
  });
43928
44539
 
43929
44540
  // src/commands/winning-ads/winners.ts
43930
- import { defineCommand as defineCommand214 } from "citty";
44541
+ import { defineCommand as defineCommand215 } from "citty";
43931
44542
  registerSchema({
43932
44543
  command: "winning-ads.winners",
43933
44544
  description: "Top winning ads for one advertiser id (from `advertisers` or `following`). Returns lean winner cards; add --full for DNA + longevity.",
@@ -43937,7 +44548,7 @@ registerSchema({
43937
44548
  platform: { type: "string", description: "Filter to a single platform: meta|linkedin", required: false }
43938
44549
  }
43939
44550
  });
43940
- var winnersCommand = defineCommand214({
44551
+ var winnersCommand = defineCommand215({
43941
44552
  meta: {
43942
44553
  name: "winners",
43943
44554
  description: "Top winning ads for a specific advertiser id. Example: baker winning-ads winners adv_123 --top 15 --output md"
@@ -43987,7 +44598,7 @@ var winnersCommand = defineCommand214({
43987
44598
  });
43988
44599
 
43989
44600
  // src/commands/winning-ads/index.ts
43990
- var winningAdsCommand = defineCommand215({
44601
+ var winningAdsCommand = defineCommand216({
43991
44602
  meta: {
43992
44603
  name: "winning-ads",
43993
44604
  description: `Search the ad-dna corpus of scored "winning" ads for reference creatives to reproduce, and manage the brands your library tracks. Proxied through the Baker backend (BAKER_API_KEY) \u2014 no separate token needed.
@@ -44177,7 +44788,7 @@ function unknownFlagEnvelope(unknown, commandPath, suggestion) {
44177
44788
  };
44178
44789
  }
44179
44790
  function commandPathOf(root, argv) {
44180
- const path37 = [];
44791
+ const path38 = [];
44181
44792
  let command = root;
44182
44793
  for (const token of argv) {
44183
44794
  if (token === "--" || token.startsWith("-")) {
@@ -44188,10 +44799,10 @@ function commandPathOf(root, argv) {
44188
44799
  if (next === void 0 || typeof next !== "object") {
44189
44800
  break;
44190
44801
  }
44191
- path37.push(token);
44802
+ path38.push(token);
44192
44803
  command = next;
44193
44804
  }
44194
- return path37.join(" ");
44805
+ return path38.join(" ");
44195
44806
  }
44196
44807
  function refuseUnknownFlags(root, argv) {
44197
44808
  const unknown = findUnknownFlags(root, argv);
@@ -44225,7 +44836,7 @@ function getCliVersion() {
44225
44836
  }
44226
44837
 
44227
44838
  // src/cli.ts
44228
- var main = defineCommand216({
44839
+ var main = defineCommand217({
44229
44840
  meta: {
44230
44841
  name: "baker",
44231
44842
  version: getCliVersion(),