@koda-sl/baker-cli 0.71.2 → 0.74.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2219,6 +2219,60 @@ baker testimonials list --source google --sentiment positive --limit 20
2219
2219
 
2220
2220
  ---
2221
2221
 
2222
+ ### Winning Ads (`baker winning-ads`)
2223
+
2224
+ Search the **ad-dna** corpus of scored "winning" competitor ads for reference creatives to reproduce (e.g. with `baker canvas`). 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.
2225
+
2226
+ > 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`.
2227
+
2228
+ ### `baker winning-ads search <query>`
2229
+
2230
+ Semantic search (dense recall + BM25 + rerank). 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` and `match_confidence`. `--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.
2231
+
2232
+ > `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.
2233
+
2234
+ > 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.)
2235
+
2236
+ ```bash
2237
+ # Only LinkedIn video ads:
2238
+ baker winning-ads search "B2B lead gen demo" --platform linkedin --format video --output md
2239
+ # Cross-platform, exclude our own brand + an advertiser we already mined:
2240
+ baker winning-ads search "fintech onboarding" --exclude-advertiser adv_ourbrand,adv_usedbefore --output md
2241
+ # Similar-to a reference ad, recent only:
2242
+ baker winning-ads search --ref-ad-id a_12345 --first-seen-after 2026-01-01T00:00:00Z --limit 5 --output md
2243
+ ```
2244
+
2245
+ | Flag | Purpose |
2246
+ |---|---|
2247
+ | `query` (positional) | Free-text natural-language query |
2248
+ | `--ref-ad-id <id>` | Find ads similar to this ad (instead of free text) |
2249
+ | `--limit <n>` | Max results 1–100 (**default 10** — shortlist size) |
2250
+ | `--max-per-advertiser <n>` | Cap results per advertiser 1–50 (default 3) |
2251
+ | `--min-relevance <0-1>` | Relevance floor; trims weak matches |
2252
+ | `--platform <list>` | One or many of `meta,tiktok,linkedin,google_search,google_display,youtube,reddit,x,pinterest,snapchat` — pass a single value to search **only** that platform |
2253
+ | `--format <list>` | `video,static,carousel` |
2254
+ | `--winner-category <list>` | `winner,scaled_winner,evergreen,rising,untested,dud,…` (default: all) |
2255
+ | `--awareness <list>` | `unaware,problem_aware,solution_aware,product_aware,most_aware` |
2256
+ | `--advertiser-id <list>` | **Restrict to** these advertiser ids (browse one brand's winners) |
2257
+ | `--exclude-advertiser <list>` | **Drop** these advertiser ids — your own brand + already-used references |
2258
+ | `--country <list>` / `--language <list>` | Filter by country / language codes |
2259
+ | `--first-seen-after` / `--first-seen-before` | ISO datetime bounds (recency) |
2260
+ | `--output json\|md\|files` | Output format (default json) |
2261
+ | `--full` | Include DNA detail + longevity |
2262
+
2263
+ 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.
2264
+
2265
+ ### `baker winning-ads advertisers <brand>`
2266
+
2267
+ Resolve a brand name → `advertiser_id`(s) in the corpus. Use it to find **your own** advertiser (to `--exclude-advertiser`) or a **competitor** (to `--advertiser-id`). Returns `advertiser_id`, `label`, `active_ads`, `total_ads`.
2268
+
2269
+ ```bash
2270
+ baker winning-ads advertisers "Acme" --output md # find our own advertiser id
2271
+ baker winning-ads advertisers "Deel" --platform meta --output md
2272
+ ```
2273
+
2274
+ ---
2275
+
2222
2276
  ### Scheduled Actions (`baker scheduled-actions`)
2223
2277
 
2224
2278
  Manage company-scoped scheduled recipes that spawn Work Actions later or on a cadence. `create`, `update`, and `delete` stage draft ops on `BAKER_CHAT_ID` and apply when the chat is published. `list` and `get` include draft state when `BAKER_CHAT_ID` is set. `trigger` runs immediately on a published scheduled action and does not require a chat id. Use a regular Work Action instead when there is no date or cadence.
package/dist/cli.js CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  } from "./chunk-JIDZ37KG.js";
13
13
 
14
14
  // src/cli.ts
15
- import { defineCommand as defineCommand138, runMain } from "citty";
15
+ import { defineCommand as defineCommand141, runMain } from "citty";
16
16
 
17
17
  // src/commands/actions/index.ts
18
18
  import { defineCommand as defineCommand12 } from "citty";
@@ -314,6 +314,52 @@ function testimonialNormalizer(record, full) {
314
314
  }
315
315
  return compactTestimonial(record);
316
316
  }
317
+ function round2(value) {
318
+ if (typeof value !== "number" || Number.isNaN(value)) {
319
+ return null;
320
+ }
321
+ return Math.round(value * 100) / 100;
322
+ }
323
+ function asRecord(value) {
324
+ return value && typeof value === "object" ? value : {};
325
+ }
326
+ function compactWinningAd(record) {
327
+ const dna = asRecord(record.dna);
328
+ return {
329
+ advertiser: String(record.advertiser ?? ""),
330
+ platform: String(record.platform ?? ""),
331
+ format: String(record.format ?? ""),
332
+ relevance: round2(record.relevance),
333
+ winner_score: round2(record.winner_score),
334
+ summary: String(dna.creative_concept ?? ""),
335
+ advertiser_id: String(record.advertiser_id ?? ""),
336
+ ad_id: String(record.ad_id ?? ""),
337
+ media_url: String(record.media_url ?? "")
338
+ };
339
+ }
340
+ function fullWinningAd(record) {
341
+ const compact = compactWinningAd(record);
342
+ const dna = asRecord(record.dna);
343
+ return {
344
+ ...compact,
345
+ winner_category: String(record.winner_category ?? ""),
346
+ media_kind: record.media_kind ?? null,
347
+ days_active: record.days_active ?? null,
348
+ reach: record.reach ?? null,
349
+ active: typeof record.active === "boolean" ? record.active : null,
350
+ angle: dna.angle ?? null,
351
+ awareness_stage: dna.awareness_stage ?? null,
352
+ target_persona: dna.target_persona ?? null,
353
+ hook_archetype: dna.hook_archetype ?? null,
354
+ industry: dna.industry ?? null
355
+ };
356
+ }
357
+ function winningAdNormalizer(record, full) {
358
+ if (full) {
359
+ return fullWinningAd(record);
360
+ }
361
+ return compactWinningAd(record);
362
+ }
317
363
  function applyFieldMask(data, fields) {
318
364
  const result = {};
319
365
  for (const field of fields) {
@@ -5633,10 +5679,10 @@ var IDENTITY_FIELDS_BY_LEVEL = {
5633
5679
  };
5634
5680
  function composeFields2(intent, level) {
5635
5681
  const intentFields = INSIGHTS_INTENTS[intent].fields;
5636
- const identity = IDENTITY_FIELDS_BY_LEVEL[level];
5682
+ const identity2 = IDENTITY_FIELDS_BY_LEVEL[level];
5637
5683
  const seen = /* @__PURE__ */ new Set();
5638
5684
  const out = [];
5639
- for (const f of [...identity, ...intentFields]) {
5685
+ for (const f of [...identity2, ...intentFields]) {
5640
5686
  if (!seen.has(f)) {
5641
5687
  seen.add(f);
5642
5688
  out.push(f);
@@ -14600,6 +14646,317 @@ Examples:
14600
14646
  }
14601
14647
  });
14602
14648
 
14649
+ // src/commands/winning-ads/index.ts
14650
+ import { defineCommand as defineCommand140 } from "citty";
14651
+
14652
+ // src/commands/winning-ads/advertisers.ts
14653
+ import { defineCommand as defineCommand138 } from "citty";
14654
+ registerSchema({
14655
+ command: "winning-ads.advertisers",
14656
+ description: "Resolve a brand name to advertiser_id(s) in the ad-dna corpus \u2014 to find your OWN advertiser (to --exclude-advertiser) or a competitor (to --advertiser-id).",
14657
+ args: {
14658
+ query: { type: "string", description: "Brand / advertiser name to match (case-insensitive)", required: false },
14659
+ limit: { type: "number", description: "Max results (default 20)", required: false, default: 20 },
14660
+ platform: { type: "string", description: "Filter to a single platform", required: false }
14661
+ }
14662
+ });
14663
+ function identity(record) {
14664
+ return record;
14665
+ }
14666
+ var advertisersCommand2 = defineCommand138({
14667
+ meta: {
14668
+ name: "advertisers",
14669
+ description: 'Resolve a brand name to advertiser_id(s). Use it to find your own advertiser for --exclude-advertiser, or a competitor for --advertiser-id. Example: baker winning-ads advertisers "Deel" --output md'
14670
+ },
14671
+ args: {
14672
+ query: { type: "positional", description: "Brand / advertiser name", required: false },
14673
+ limit: { type: "string", description: "Max results (default 20)", required: false },
14674
+ platform: { type: "string", description: "Filter to a single platform", required: false },
14675
+ output: { type: "string", description: "Output format: json|files|md", required: false, default: "json" },
14676
+ fields: { type: "string", description: "Comma-separated field names to include", required: false }
14677
+ },
14678
+ run: async ({ args }) => {
14679
+ try {
14680
+ const params = {};
14681
+ const query = args.query;
14682
+ if (query) {
14683
+ params.q = query;
14684
+ }
14685
+ if (args.limit) {
14686
+ params.limit = String(args.limit);
14687
+ }
14688
+ if (args.platform) {
14689
+ params.platform = String(args.platform);
14690
+ }
14691
+ const data = await apiGet("/api/winning-ads/advertisers", params);
14692
+ const output = args.output || "json";
14693
+ if (output === "json") {
14694
+ writeJson({ ok: true, data });
14695
+ return;
14696
+ }
14697
+ const list = Array.isArray(data?.advertisers) ? data.advertisers : [];
14698
+ writeOutput(
14699
+ { ok: true, data: list },
14700
+ output,
14701
+ args.fields ? args.fields.split(",") : void 0,
14702
+ false,
14703
+ identity
14704
+ );
14705
+ } catch (err) {
14706
+ if (err instanceof ApiError) {
14707
+ writeJson({ ok: false, error: { code: err.code, message: err.message } });
14708
+ process.exit(1);
14709
+ }
14710
+ writeJson({ ok: false, error: { code: "INTERNAL_ERROR", message: "Unexpected error" } });
14711
+ process.exit(1);
14712
+ }
14713
+ }
14714
+ });
14715
+
14716
+ // src/commands/winning-ads/search.ts
14717
+ import { defineCommand as defineCommand139 } from "citty";
14718
+ registerSchema({
14719
+ command: "winning-ads.search",
14720
+ 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.",
14721
+ args: {
14722
+ query: { type: "string", description: "Free-text natural-language query", required: false },
14723
+ "ref-ad-id": {
14724
+ type: "string",
14725
+ description: "Find ads similar to this ad id (instead of free text)",
14726
+ required: false
14727
+ },
14728
+ limit: { type: "number", description: "Max results 1-100 (default 10)", required: false, default: 10 },
14729
+ "max-per-advertiser": {
14730
+ type: "number",
14731
+ description: "Cap results per advertiser 1-50 (default 3)",
14732
+ required: false,
14733
+ default: 3
14734
+ },
14735
+ "min-relevance": { type: "number", description: "Relevance floor 0-1; trims weak matches", required: false },
14736
+ platform: {
14737
+ type: "string",
14738
+ description: "Comma list: meta,tiktok,linkedin,google_search,google_display,youtube,reddit,x,pinterest,snapchat",
14739
+ required: false
14740
+ },
14741
+ format: { type: "string", description: "Comma list: video,static,carousel", required: false },
14742
+ "winner-category": {
14743
+ type: "string",
14744
+ description: "Comma list: winner,scaled_winner,evergreen,rising,untested,dud,\u2026",
14745
+ required: false
14746
+ },
14747
+ awareness: {
14748
+ type: "string",
14749
+ description: "Comma list: unaware,problem_aware,solution_aware,product_aware,most_aware",
14750
+ required: false
14751
+ },
14752
+ country: { type: "string", description: "Comma list of country codes", required: false },
14753
+ language: { type: "string", description: "Comma list of language codes", required: false },
14754
+ "advertiser-id": {
14755
+ type: "string",
14756
+ description: "Restrict to these advertiser ids (browse one brand's winners)",
14757
+ required: false
14758
+ },
14759
+ "exclude-advertiser": {
14760
+ type: "string",
14761
+ description: "Drop these advertiser ids \u2014 your own brand + already-used references",
14762
+ required: false
14763
+ },
14764
+ "first-seen-after": {
14765
+ type: "string",
14766
+ description: "ISO datetime; only ads first seen after this",
14767
+ required: false
14768
+ },
14769
+ "first-seen-before": {
14770
+ type: "string",
14771
+ description: "ISO datetime; only ads first seen before this",
14772
+ required: false
14773
+ }
14774
+ }
14775
+ });
14776
+ function splitList(value) {
14777
+ if (!value) {
14778
+ return [];
14779
+ }
14780
+ return value.split(",").map((v) => v.trim()).filter(Boolean);
14781
+ }
14782
+ function setNumber(body, key, value) {
14783
+ if (value !== void 0 && value !== "") {
14784
+ body[key] = Number(value);
14785
+ }
14786
+ }
14787
+ function setList(target, key, value) {
14788
+ const list = splitList(value);
14789
+ if (list.length) {
14790
+ target[key] = list;
14791
+ }
14792
+ }
14793
+ function setString(target, key, value) {
14794
+ if (value) {
14795
+ target[key] = value;
14796
+ }
14797
+ }
14798
+ function buildSearchBody(args) {
14799
+ const body = {};
14800
+ if (args.query) {
14801
+ body.free_text_query = args.query;
14802
+ }
14803
+ if (args.refAdId) {
14804
+ body.ref_ad_id = args.refAdId;
14805
+ }
14806
+ setNumber(body, "limit", args.limit);
14807
+ setNumber(body, "max_per_advertiser", args.maxPerAdvertiser);
14808
+ setNumber(body, "min_relevance", args.minRelevance);
14809
+ const hardFilters = {};
14810
+ setList(hardFilters, "platform", args.platform);
14811
+ setList(hardFilters, "format", args.format);
14812
+ setList(hardFilters, "winner_category", args.winnerCategory);
14813
+ setList(hardFilters, "awareness_stage", args.awareness);
14814
+ setList(hardFilters, "country", args.country);
14815
+ setList(hardFilters, "language", args.language);
14816
+ setList(hardFilters, "advertiser_ids", args.advertiserId);
14817
+ setList(hardFilters, "exclude_advertiser_ids", args.excludeAdvertiser);
14818
+ setString(hardFilters, "first_seen_after", args.firstSeenAfter);
14819
+ setString(hardFilters, "first_seen_before", args.firstSeenBefore);
14820
+ if (Object.keys(hardFilters).length) {
14821
+ body.hard_filters = hardFilters;
14822
+ }
14823
+ return body;
14824
+ }
14825
+ var searchCommand4 = defineCommand139({
14826
+ meta: {
14827
+ name: "search",
14828
+ 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"
14829
+ },
14830
+ args: {
14831
+ query: { type: "positional", description: "Free-text search query", required: false },
14832
+ "ref-ad-id": { type: "string", description: "Find ads similar to this ad id", required: false },
14833
+ limit: {
14834
+ type: "string",
14835
+ description: "Max results 1-100 (default 10 \u2014 a shortlist)",
14836
+ required: false,
14837
+ default: "10"
14838
+ },
14839
+ "max-per-advertiser": {
14840
+ type: "string",
14841
+ description: "Cap results per advertiser 1-50 (default 3)",
14842
+ required: false
14843
+ },
14844
+ "min-relevance": { type: "string", description: "Relevance floor 0-1", required: false },
14845
+ platform: {
14846
+ type: "string",
14847
+ description: "Comma list of platforms (meta,linkedin,tiktok,\u2026) \u2014 search one or many",
14848
+ required: false
14849
+ },
14850
+ format: { type: "string", description: "Comma list of formats (video,static,carousel)", required: false },
14851
+ "winner-category": {
14852
+ type: "string",
14853
+ description: "Comma list of winner categories (default: all)",
14854
+ required: false
14855
+ },
14856
+ awareness: { type: "string", description: "Comma list of awareness stages", required: false },
14857
+ country: { type: "string", description: "Comma list of country codes", required: false },
14858
+ language: { type: "string", description: "Comma list of language codes", required: false },
14859
+ "advertiser-id": { type: "string", description: "Restrict to these advertiser ids", required: false },
14860
+ "exclude-advertiser": {
14861
+ type: "string",
14862
+ description: "Drop these advertiser ids (your own + already-used)",
14863
+ required: false
14864
+ },
14865
+ "first-seen-after": { type: "string", description: "ISO datetime lower bound", required: false },
14866
+ "first-seen-before": { type: "string", description: "ISO datetime upper bound", required: false },
14867
+ output: { type: "string", description: "Output format: json|files|md", required: false, default: "json" },
14868
+ fields: { type: "string", description: "Comma-separated field names to include", required: false },
14869
+ full: {
14870
+ type: "boolean",
14871
+ description: "Include DNA detail (angle, persona, hook) + longevity",
14872
+ required: false,
14873
+ default: false
14874
+ }
14875
+ },
14876
+ run: async ({ args }) => {
14877
+ try {
14878
+ const searchArgs = {
14879
+ query: args.query,
14880
+ refAdId: args["ref-ad-id"],
14881
+ limit: args.limit,
14882
+ maxPerAdvertiser: args["max-per-advertiser"],
14883
+ minRelevance: args["min-relevance"],
14884
+ platform: args.platform,
14885
+ format: args.format,
14886
+ winnerCategory: args["winner-category"],
14887
+ awareness: args.awareness,
14888
+ country: args.country,
14889
+ language: args.language,
14890
+ advertiserId: args["advertiser-id"],
14891
+ excludeAdvertiser: args["exclude-advertiser"],
14892
+ firstSeenAfter: args["first-seen-after"],
14893
+ firstSeenBefore: args["first-seen-before"]
14894
+ };
14895
+ const body = buildSearchBody(searchArgs);
14896
+ if (!("free_text_query" in body) && !("ref_ad_id" in body) && !("hard_filters" in body)) {
14897
+ writeJson({
14898
+ ok: false,
14899
+ error: { code: "VALIDATION_ERROR", message: "Provide a query, --ref-ad-id, or a filter flag" }
14900
+ });
14901
+ process.exit(1);
14902
+ }
14903
+ const data = await apiPost(
14904
+ "/api/winning-ads/search",
14905
+ body
14906
+ );
14907
+ const output = args.output || "json";
14908
+ const full = args.full;
14909
+ const rawResults = Array.isArray(data?.results) ? data.results : [];
14910
+ if (output === "json") {
14911
+ const results = rawResults.map((r) => winningAdNormalizer(r, full));
14912
+ writeJson({
14913
+ ok: true,
14914
+ data: { results, pool_size: data?.pool_size ?? null, match_confidence: data?.match_confidence ?? null }
14915
+ });
14916
+ return;
14917
+ }
14918
+ writeOutput(
14919
+ { ok: true, data: rawResults },
14920
+ output,
14921
+ args.fields ? args.fields.split(",") : void 0,
14922
+ full,
14923
+ winningAdNormalizer
14924
+ );
14925
+ } catch (err) {
14926
+ if (err instanceof ApiError) {
14927
+ writeJson({ ok: false, error: { code: err.code, message: err.message } });
14928
+ process.exit(1);
14929
+ }
14930
+ writeJson({ ok: false, error: { code: "INTERNAL_ERROR", message: "Unexpected error" } });
14931
+ process.exit(1);
14932
+ }
14933
+ }
14934
+ });
14935
+
14936
+ // src/commands/winning-ads/index.ts
14937
+ var winningAdsCommand = defineCommand140({
14938
+ meta: {
14939
+ name: "winning-ads",
14940
+ description: `Search the ad-dna corpus of scored "winning" ads for reference creatives to reproduce. Proxied through the Baker backend (BAKER_API_KEY) \u2014 no separate token needed.
14941
+
14942
+ Auth: BAKER_API_KEY (must start with bk_) + BAKER_API_URL (your Convex .convex.site URL).
14943
+
14944
+ Subcommands:
14945
+ baker winning-ads search "<free text>" \u2014 semantic search; returns a lean shortlist (advertiser, summary, scores, media_url)
14946
+ baker winning-ads advertisers "<brand>" \u2014 resolve a brand \u2192 advertiser_id(s), to --exclude-advertiser (your own) or --advertiser-id (a competitor)
14947
+
14948
+ Examples:
14949
+ baker winning-ads search "B2B SaaS ad: before/after of an overworked team replaced by AI automation" --platform meta --format static
14950
+ baker winning-ads search "skincare UGC testimonial" --platform tiktok --format video --output md
14951
+ baker winning-ads search "fintech onboarding" --winner-category winner --exclude-advertiser adv_ourbrand,adv_usedbefore --output md
14952
+ baker winning-ads advertisers "Acme" --output md # find our own advertiser id to exclude`
14953
+ },
14954
+ subCommands: {
14955
+ search: searchCommand4,
14956
+ advertisers: advertisersCommand2
14957
+ }
14958
+ });
14959
+
14603
14960
  // src/version.ts
14604
14961
  import { readFileSync as readFileSync8 } from "fs";
14605
14962
  function packageJsonUrl() {
@@ -14617,7 +14974,7 @@ function getCliVersion() {
14617
14974
  }
14618
14975
 
14619
14976
  // src/cli.ts
14620
- var main = defineCommand138({
14977
+ var main = defineCommand141({
14621
14978
  meta: {
14622
14979
  name: "baker",
14623
14980
  version: getCliVersion(),
@@ -14640,6 +14997,7 @@ Introspection: Run 'baker schema <command>' to inspect argument schemas.`
14640
14997
  videos: videosCommand,
14641
14998
  testimonials: testimonialsCommand,
14642
14999
  canvas: canvasCommand,
15000
+ "winning-ads": winningAdsCommand,
14643
15001
  schema: schemaCommand
14644
15002
  }
14645
15003
  });