@koda-sl/baker-cli 0.201.0 → 0.202.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
@@ -2797,8 +2797,8 @@ baker actions create --name "Build hero" --priority high --tags landing,creative
2797
2797
  # surface — doing the work is the default, an action is the exception. One advisory hint per call: the
2798
2798
  # scheduled redirect wins when both match.
2799
2799
  # `draftCreateCount` is how many creates this chat has staged (counted after this op, so it drops again
2800
- # after `draft remove`). At 3 or more, create emits a fan-out hint: re-read the draft and merge actions
2801
- # that touch the same system into one.
2800
+ # after `draft remove`). At exactly 2, create emits a one-line merge nudge; at 3 or more it emits the
2801
+ # fuller fan-out hint: re-read the draft and merge actions that touch the same system into one.
2802
2802
  # If no --tags are passed, create emits a MISSING --tags hint — the action is invisible to the backlog's tag filter.
2803
2803
  # If no --priority is passed, create emits a MISSING --priority hint — the action ranks as normal/medium in the do-first order.
2804
2804
  baker actions update <action-id> --name "Better name" --tags google-ads,strategy # --tags REPLACES the set; '' clears
package/dist/cli.js CHANGED
@@ -587,6 +587,9 @@ function parseTagList(value) {
587
587
  const tags = value.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
588
588
  return [...new Set(tags)];
589
589
  }
590
+ function fold(text2) {
591
+ return text2.normalize("NFD").replace(/[̀-ͯ]/g, "");
592
+ }
590
593
  var SCHEDULE_SIGNALS = [
591
594
  /\brecurr(?:ing|ence)\b/i,
592
595
  /\bcadence\b/i,
@@ -626,28 +629,95 @@ function parsePriority(value, { allowClear }) {
626
629
  );
627
630
  }
628
631
  function looksScheduled(name, description) {
629
- const haystack = `${name}
630
- ${description}`;
631
- return SCHEDULE_SIGNALS.some((re) => re.test(haystack));
632
- }
632
+ const haystack = fold(`${name}
633
+ ${description}`);
634
+ return SCHEDULE_SIGNALS.some((re2) => re2.test(haystack));
635
+ }
636
+ var CAMPAIGN_ENTITY = String.raw`(?:campaigns?|campanas?|ad\s*sets?|adsets?|ad\s*groups?|adgroups?|ads?|anuncios?|conjuntos?\s+de\s+anuncios|grupos?\s+de\s+anuncios)`;
637
+ var LIFECYCLE_VERB = "(?:pause|unpause|enable|disable|launch|restructure|duplicate|clone|rebuild|prune|reweight|split|consolidate|pausar|pausa|activar|podar|poda|reponderar|duplicar|reestructurar|consolidar)";
638
+ var BUDGET_VERB = "(?:cap|raise|lower|increase|decrease|adjust|change|set|update|reallocate|shift|limitar|subir|bajar|ajustar|aplicar|reasignar)";
639
+ var re = (...parts) => new RegExp(parts.join(""), "i");
633
640
  var SURFACE_RULES = [
641
+ {
642
+ surface: "ask-user",
643
+ signals: [
644
+ /\bproduction\s+(?:domain|url|site|hostname)\b/i,
645
+ /\b(?:confirm|check|verify|confirmar|verificar)\s+(?:with|w\/|con)\s+(?:the\s+|el\s+|la\s+)?(?:client|user|customer|team|cliente|usuario|equipo)\b/i,
646
+ /\bmissing\s+(?:the\s+)?(?:value|values|id|ids|url|budget|domain|credentials?|account\s+id|measurement\s+id)\b/i,
647
+ /\b(?:ask|get)\s+(?:the\s+)?(?:client|user)\s+for\b/i,
648
+ /\b(?:pedir|solicitar)\s+(?:al\s+)?cliente\b/i
649
+ ],
650
+ hint: "This is a missing input, not blocked work. Ask for it with AskUserQuestion (one question), then do the work in the same turn \u2014 filing the Task AND asking the question is the same request twice."
651
+ },
634
652
  {
635
653
  surface: "analysis",
636
654
  nameOnly: true,
637
655
  signals: [
638
- /^\s*(?:re-?)?(?:investigate|analyse|analyze|audit|review|document|assess|evaluate|research|map|inventory|examine|diagnose)\b/i
656
+ /^\s*(?:re-?)?(?:investigat|analy[sz]|audit|review|document|assess|evaluat|research|map|inventor|examin|diagnos|verif|validat|benchmark)\w*\b/i,
657
+ /^\s*(?:auditar|auditoria|investigar|investigacion|revisar|revision|analizar|analisis|documentar|evaluar|evaluacion|diagnosticar|diagnostico|mapear|inventariar|verificar|verificacion|comprobar|estudiar|estudio)\b/i
639
658
  ],
640
659
  hint: "This names the analysis itself \u2014 investigating, auditing and documenting are work you do now, not work you file. Run it in this chat and deliver the finding; a Task is for a fix something genuinely blocks (bar: `__tooling__/docs/tools/baker/actions.md`)."
641
660
  },
642
661
  {
643
- surface: "ask-user",
662
+ surface: "ads-write",
663
+ // "Approve X" is a human decision, not a staged write — and the budget
664
+ // signals are broad enough to swallow it otherwise. Name-anchored so a
665
+ // description recalling an earlier approval doesn't veto real work.
666
+ exceptName: [/^\s*(?:approve|approval|sign[-\s]?off|aprobar|aprobacion)\b/i],
667
+ // Bare `keywords` is the work when it's the subject of the Task, but plain
668
+ // context when a description happens to discuss search terms.
669
+ nameOnlySignals: [/\b(?:keywords?|palabras\s+clave)\b/i],
644
670
  signals: [
645
- /\bproduction\s+(?:domain|url|site|hostname)\b/i,
646
- /\b(?:confirm|check|verify)\s+(?:with|w\/)\s+(?:the\s+)?(?:client|user|customer|team)\b/i,
647
- /\bmissing\s+(?:the\s+)?(?:value|values|id|ids|url|budget|domain|credentials?|account\s+id|measurement\s+id)\b/i,
648
- /\b(?:ask|get)\s+(?:the\s+)?(?:client|user)\s+for\b/i
671
+ // Targeting.
672
+ /\b(?:geo|location|geographic|geografic\w*)[\s-]*(?:targeting|target|segmentacion)\b/i,
673
+ /\bsegmentacion\s+(?:geografica|por\s+ubicacion)\b/i,
674
+ /\bpresence[-\s_]?(?:only|or[-\s_]?interest)\b/i,
675
+ /\b(?:ad\s*schedul\w*|dayparting|calendario\s+de\s+anuncios)\b/i,
676
+ // Extensions / assets.
677
+ /\b(?:callouts?|sitelinks?|site\s*links?|structured\s+snippets?|price\s+extensions?|lead\s+form\s+extensions?|extensiones|asset\s+groups?)\b/i,
678
+ // Ad copy. Compound forms only — bare `headline` and `copy` belong to
679
+ // `landing`, and an RSA headline has to say so.
680
+ /\b(?:rsas?|responsive\s+search\s+ads?|ad\s+copy|copy\s+de\s+(?:los\s+)?anuncios)\b/i,
681
+ /\b(?:headline|titular|description)\s+(?:pool|set|slots?)\b/i,
682
+ /\bdisplay\s+paths?\b|\brutas?\s+visibles?\b/i,
683
+ // Keywords. `negatives` is bare in English because in this product's
684
+ // vocabulary the noun is always negative keywords; the Spanish side is
685
+ // feminine-only, because "resultados negativos" is a real phrase.
686
+ /\bmatch\s+types?\b|\bconcordancias?\b/i,
687
+ /\bnegatives?\b/i,
688
+ /\b(?:palabras\s+clave\s+)?negativas\b/i,
689
+ /\bkeyword\s+lists?\b|\blistas?\s+de\s+palabras\s+clave\b/i,
690
+ // Audiences. `LAL` is the operators' own shorthand for a lookalike and
691
+ // has no other reading in this corpus.
692
+ /\b(?:in-?market|affinity|audience\s+signals?|senales?\s+de\s+audiencia)\b/i,
693
+ /\b(?:audiencias?|lal)\b/i,
694
+ /\blistas?\s+de\s+(?:exclusion|remarketing)\b|\bexclusion\s+lists?\b/i,
695
+ // URLs.
696
+ /\btracking\s+(?:url\s+)?templates?\b|\bfinal\s+urls?\b|\bfinal\s+url\s+suffix\b/i,
697
+ // Bidding.
698
+ /\bbid\s+(?:adjustments?|modifiers?|strateg\w*)\b|\bajustes?\s+de\s+puja\b/i,
699
+ /\b(?:tcpa|troas|target\s+cpa|target\s+roas)\b/i,
700
+ // Lifecycle. Bare `campaign` is deliberately NOT a signal — it appears
701
+ // everywhere and would strip tag-manager and landing of correct
702
+ // routings, so every campaign-shaped rule needs a verb or a specific
703
+ // setting noun next to it.
704
+ // Bounded, and never across the name/description boundary. `ads` is an
705
+ // ordinary word in this domain, so an unbounded gap lets a verb in the
706
+ // name pair with a passing mention in the description — and since
707
+ // ads-write is evaluated first, that spurious match outranks the surface
708
+ // which should have answered.
709
+ re(String.raw`\b`, LIFECYCLE_VERB, String.raw`\b[^\n]{0,40}\b`, CAMPAIGN_ENTITY, String.raw`\b`),
710
+ // Budget. Two complementary shapes — verb before, quantity after — so a
711
+ // change stated either way lands, guarded by the `exceptName` veto above.
712
+ /\b(?:daily|campaign|lifetime|ad\s*set|minimum|min|competitor)\s+budgets?\b/i,
713
+ /\bpresupuestos?\b/i,
714
+ re(String.raw`\b`, BUDGET_VERB, String.raw`\b[\s\S]{0,24}\bbudgets?\b`),
715
+ /\bbudgets?\b[\s\S]{0,24}\b(?:at|to|cap|down|up|\d|%|€|\$)/i,
716
+ /\bnegative\s+keywords?\b/i,
717
+ /\b(?:lookalike|custom)\s+audience\b/i,
718
+ /\bswap\s+(?:the\s+)?creative\b/i
649
719
  ],
650
- hint: "This is a missing input, not blocked work. Ask for it with AskUserQuestion (one question), then do the work in the same turn \u2014 filing the Task AND asking the question is the same request twice."
720
+ hint: "This is an ad-platform change the write surface covers \u2014 stage it now with `baker ads google|meta|linkedin` and it applies at publish (guides: `__tooling__/docs/tools/baker/ads-google.md`, `__tooling__/docs/tools/baker/ads-meta.md`, `__tooling__/docs/tools/baker/ads-linkedin.md`). Staging is not going live."
651
721
  },
652
722
  {
653
723
  surface: "tag-manager",
@@ -661,7 +731,10 @@ var SURFACE_RULES = [
661
731
  /\bga4\s+(?:config|configuration|event)\b/i,
662
732
  /\bcontainer\s+(?:tag|trigger|variable)s?\b/i,
663
733
  /\b(?:conversion|form|submit|submission|click|purchase|lead|custom)\s+event\b/i,
664
- /\btrack\w*\b[\s\S]*\bevent\b/i
734
+ /\btrack\w*\b[\s\S]*\bevent\b/i,
735
+ /\beventos?\s+de\s+(?:formulario|conversion|clic|compra)\b/i,
736
+ /\bconsentimiento\b/i,
737
+ /\bmedicion\b/i
665
738
  ],
666
739
  except: [/\bserver-?\s?side\b/i, /\bsgtm\b/i, /\bhosting\b/i],
667
740
  hint: "This is a change inside the GTM container \u2014 `baker tag-manager` stages it now and it applies at publish (guide: `__tooling__/docs/tools/baker/tag-manager.md`). Stage it instead of filing it, and if several container fixes really are blocked, they belong in ONE Task, not one per finding."
@@ -674,30 +747,20 @@ var SURFACE_RULES = [
674
747
  /\bclarity\b/i,
675
748
  /\bhotjar\b/i,
676
749
  /\bgtm\s+snippet\b/i,
677
- /\b(?:install|add|remove|swap)\b[\s\S]*\b(?:snippet|script|tag)\b/i
750
+ /\bcapi\b/i,
751
+ /\b(?:install|add|remove|swap|instalar|colocar)\b[\s\S]*\b(?:snippet|script|tag)\b/i
678
752
  ],
753
+ // Two vetoes. A structured snippet is a Google Ads extension, not a script
754
+ // on the page — `ads-write` claims it above, and this stops both rules
755
+ // being able to answer. Server-side delivery is infrastructure nobody here
756
+ // can provision, the same call `tag-manager` already makes for sGTM.
757
+ except: [/\bstructured\s+snippets?\b/i, /\bserver-?\s?side\b/i, /\bhosting\b/i],
679
758
  hint: "This is a tag or script on the site \u2014 it goes through the `request_tag_input` approval form in this chat, which also collects any secret values (guide: `__tooling__/docs/tools/baker/tags.md`). Show the form instead of filing a Task."
680
759
  },
681
- {
682
- surface: "ads-write",
683
- signals: [
684
- /\b(?:pause|unpause|enable|disable|launch|restructure|duplicate|clone|rebuild)\b[\s\S]*\b(?:campaign|ad\s*set|ad\s*group|adgroup)\b/i,
685
- /\b(?:tcpa|troas|target\s+cpa|target\s+roas)\b/i,
686
- /\b(?:daily|campaign|lifetime|ad\s*set)\s+budget\b/i,
687
- /\b(?:raise|lower|increase|decrease|adjust|change|set|update)\s+(?:the\s+)?budget\b/i,
688
- /\bbid\s+strateg/i,
689
- /\bnegative\s+keywords?\b/i,
690
- /\b(?:lookalike|custom)\s+audience\b/i,
691
- /\bswap\s+(?:the\s+)?creative\b/i
692
- ],
693
- hint: "This is an ad-platform change the write surface covers \u2014 stage it now with `baker ads google|meta|linkedin` and it applies at publish (guides: `__tooling__/docs/tools/baker/ads-google.md`, `__tooling__/docs/tools/baker/ads-meta.md`, `__tooling__/docs/tools/baker/ads-linkedin.md`). Staging is not going live."
694
- },
695
760
  {
696
761
  surface: "landing",
697
- signals: [
698
- /\b(?:landing\s+page|hero|headline|above\s+the\s+fold)\b/i,
699
- /\b(?:rewrite|tighten|restyle)\b[\s\S]*\b(?:copy|page|section)\b/i
700
- ],
762
+ nameOnlySignals: [/\b(?:landing\s+pages?|landings?|hero|headline|above\s+the\s+fold|pagina)\b/i],
763
+ signals: [/\b(?:rewrite|tighten|restyle)\b[\s\S]*\b(?:copy|page|section)\b/i],
701
764
  hint: "This is a page change \u2014 build it in this chat with the `/landing` skill. Only file it if something outside the page blocks the work."
702
765
  },
703
766
  {
@@ -706,17 +769,25 @@ var SURFACE_RULES = [
706
769
  hint: "This is form behaviour \u2014 change it in this chat with the `/flow-builder` skill. Only file it if something outside the form blocks the work."
707
770
  }
708
771
  ];
772
+ var FAN_OUT_NUDGE_THRESHOLD = 2;
709
773
  var FAN_OUT_THRESHOLD = 3;
710
774
  var SCHEDULED_HINT = 'This reads as recurring/scheduled work. If it should run on a cadence or a future date, create a Scheduled Action instead \u2014 baker scheduled-actions create --cron "0 9 * * MON" (or --run-at; guide: `__tooling__/docs/tools/baker/scheduled-actions.md`). Do NOT capture "set up a scheduled action" as a Work Action.';
711
775
  function looksExecutable(name, description) {
712
- const full = `${name}
713
- ${description}`;
776
+ const foldedName = fold(name);
777
+ const full = `${foldedName}
778
+ ${fold(description)}`;
714
779
  for (const rule of SURFACE_RULES) {
715
- const haystack = rule.nameOnly ? name : full;
716
- if (rule.except?.some((re) => re.test(full))) {
780
+ const haystack = rule.nameOnly ? foldedName : full;
781
+ if (rule.except?.some((re2) => re2.test(full))) {
782
+ continue;
783
+ }
784
+ if (rule.exceptName?.some((re2) => re2.test(foldedName))) {
717
785
  continue;
718
786
  }
719
- if (rule.signals.some((re) => re.test(haystack))) {
787
+ if (rule.signals.some((re2) => re2.test(haystack))) {
788
+ return { surface: rule.surface, hint: rule.hint };
789
+ }
790
+ if (rule.nameOnlySignals?.some((re2) => re2.test(foldedName))) {
720
791
  return { surface: rule.surface, hint: rule.hint };
721
792
  }
722
793
  }
@@ -741,6 +812,10 @@ function buildCreateHints({
741
812
  hints.push(
742
813
  `${draftCreateCount} Tasks are staged in this chat. Re-read them with \`baker actions draft\`: can any be done now with a surface you already have, and do any two touch the same system (same container, same campaign, same page)? Those belong in ONE Task \u2014 fold them together with \`baker actions update\` and drop the extras with \`baker actions draft remove <tempId>\`.`
743
814
  );
815
+ } else if (draftCreateCount === FAN_OUT_NUDGE_THRESHOLD) {
816
+ hints.push(
817
+ `Second Task staged in this chat. If these two land on the same system (same container, same campaign, same page) they are ONE Task \u2014 fold them with \`baker actions update\` and drop the extra with \`baker actions draft remove ${tempId}\`.`
818
+ );
744
819
  }
745
820
  hints.push(`Link dependencies: baker actions link --blocker <id> --blocked ${tempId}`);
746
821
  if (!description) {
@@ -8357,8 +8432,8 @@ var FALSE_RANGE_PATTERNS = [
8357
8432
  function countFalseRanges(text2) {
8358
8433
  let count = 0;
8359
8434
  let first = "";
8360
- for (const re of FALSE_RANGE_PATTERNS) {
8361
- for (const m of text2.matchAll(re)) {
8435
+ for (const re2 of FALSE_RANGE_PATTERNS) {
8436
+ for (const m of text2.matchAll(re2)) {
8362
8437
  const left = m[1] ?? "";
8363
8438
  const right = m[2] ?? "";
8364
8439
  if (/\d/.test(left) || /\d/.test(right)) continue;
@@ -8421,8 +8496,8 @@ function countPhraseTell(text2, id, phrases) {
8421
8496
  }
8422
8497
  return { count, first };
8423
8498
  }
8424
- function countPattern(text2, re) {
8425
- const matches = text2.match(re) ?? [];
8499
+ function countPattern(text2, re2) {
8500
+ const matches = text2.match(re2) ?? [];
8426
8501
  return { count: matches.length, first: matches.join("") };
8427
8502
  }
8428
8503
  function detectCopyTells(text2) {
@@ -20048,8 +20123,8 @@ import { defineCommand as defineCommand90 } from "citty";
20048
20123
  var GOOGLE_CSS2 = "https://fonts.googleapis.com/css2";
20049
20124
  function parseGoogleFontFaces(css) {
20050
20125
  const faces = [];
20051
- const re = /\/\*\s*([\w-]+)\s*\*\/\s*@font-face\s*\{([^}]*)\}/g;
20052
- for (const match of String(css).matchAll(re)) {
20126
+ const re2 = /\/\*\s*([\w-]+)\s*\*\/\s*@font-face\s*\{([^}]*)\}/g;
20127
+ for (const match of String(css).matchAll(re2)) {
20053
20128
  const subset = match[1] ?? "";
20054
20129
  const body = match[2] ?? "";
20055
20130
  const family = body.match(/font-family\s*:\s*['"]?([^'";]+)['"]?\s*;/i)?.[1]?.trim();
@@ -22645,8 +22720,8 @@ function parseVoiceTraits(description) {
22645
22720
  const out = {};
22646
22721
  if (/\b(female|woman|women|girl|lady)\b/.test(d)) out.gender = "female";
22647
22722
  else if (/\b(male|man|men|guy|boy)\b/.test(d)) out.gender = "male";
22648
- for (const [re, name] of LANGUAGE_WORDS) {
22649
- if (re.test(d)) {
22723
+ for (const [re2, name] of LANGUAGE_WORDS) {
22724
+ if (re2.test(d)) {
22650
22725
  out.language = name;
22651
22726
  break;
22652
22727
  }