@koda-sl/baker-cli 0.231.0 → 0.232.1
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 +9 -0
- package/dist/cli.js +657 -534
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -565,498 +565,6 @@ function listSchemas() {
|
|
|
565
565
|
return [...registry.keys()];
|
|
566
566
|
}
|
|
567
567
|
|
|
568
|
-
// src/commands/actions/tagError.ts
|
|
569
|
-
var UNKNOWN_TAG_RE = /Unknown action tag\(s\): ([^.]+)\. Valid tags: ([^.]+)\./;
|
|
570
|
-
function splitSlugs(list) {
|
|
571
|
-
return list.split(",").map((slug) => slug.trim()).filter((slug) => slug.length > 0);
|
|
572
|
-
}
|
|
573
|
-
function actionTagFix(message) {
|
|
574
|
-
const match = UNKNOWN_TAG_RE.exec(message);
|
|
575
|
-
if (!match) {
|
|
576
|
-
return null;
|
|
577
|
-
}
|
|
578
|
-
const [, unknownList, validList] = match;
|
|
579
|
-
const unknownTags = splitSlugs(unknownList ?? "");
|
|
580
|
-
const validTags = splitSlugs(validList ?? "");
|
|
581
|
-
const first = unknownTags[0];
|
|
582
|
-
if (first === void 0 || validTags.length === 0) {
|
|
583
|
-
return null;
|
|
584
|
-
}
|
|
585
|
-
return {
|
|
586
|
-
action: "retry_with_a_valid_tag",
|
|
587
|
-
explanation: `The tag(s) ${unknownTags.join(", ")} are not in this company's taxonomy. \`validTags\` below is the complete accepted set \u2014 pick from it and re-run the same command. Only mint a new tag when none of them names this work; a near-duplicate of an existing tag splits the backlog's filter instead of extending it.`,
|
|
588
|
-
unknownTags,
|
|
589
|
-
validTags,
|
|
590
|
-
mintCommand: `baker actions tags create --slug ${first} --description "<what ${first} groups>"`,
|
|
591
|
-
listCommand: "baker actions tags list"
|
|
592
|
-
};
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
// src/commands/actions/shared.ts
|
|
596
|
-
function writeOk(data) {
|
|
597
|
-
writeJson({ ok: true, data: data ?? null });
|
|
598
|
-
}
|
|
599
|
-
function failValidation(message) {
|
|
600
|
-
writeJson({ ok: false, error: { code: "VALIDATION_ERROR", message } });
|
|
601
|
-
process.exit(1);
|
|
602
|
-
}
|
|
603
|
-
function failApi(err) {
|
|
604
|
-
if (err instanceof ApiError) {
|
|
605
|
-
const fix = actionTagFix(err.message);
|
|
606
|
-
writeJson({ ok: false, error: { code: err.code, message: err.message, ...fix ? { fix } : {} } });
|
|
607
|
-
process.exit(1);
|
|
608
|
-
}
|
|
609
|
-
if (err instanceof Error) {
|
|
610
|
-
writeJson({ ok: false, error: { code: "INTERNAL_ERROR", message: err.message } });
|
|
611
|
-
process.exit(1);
|
|
612
|
-
}
|
|
613
|
-
writeJson({ ok: false, error: { code: "INTERNAL_ERROR", message: "Unexpected error" } });
|
|
614
|
-
process.exit(1);
|
|
615
|
-
}
|
|
616
|
-
function generateTempId() {
|
|
617
|
-
return `temp_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
618
|
-
}
|
|
619
|
-
function isTempId(id) {
|
|
620
|
-
return id.startsWith("temp_");
|
|
621
|
-
}
|
|
622
|
-
function parseTagList(value) {
|
|
623
|
-
if (typeof value !== "string") {
|
|
624
|
-
return void 0;
|
|
625
|
-
}
|
|
626
|
-
const tags = value.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
|
|
627
|
-
return [...new Set(tags)];
|
|
628
|
-
}
|
|
629
|
-
function fold(text2) {
|
|
630
|
-
return text2.normalize("NFD").replace(/[̀-ͯ]/g, "");
|
|
631
|
-
}
|
|
632
|
-
var SCHEDULE_SIGNALS = [
|
|
633
|
-
/\brecurr(?:ing|ence)\b/i,
|
|
634
|
-
/\bcadence\b/i,
|
|
635
|
-
/\bcron\b/i,
|
|
636
|
-
/\bschedule[ds]?\b/i,
|
|
637
|
-
/\b(?:daily|weekly|monthly|quarterly|annual(?:ly)?|biweekly|nightly)\b/i,
|
|
638
|
-
/\bevery\s+(?:day|week|month|quarter|year|morning|monday|tuesday|wednesday|thursday|friday|saturday|sunday|\d)/i,
|
|
639
|
-
/\beach\s+(?:day|week|month|monday|tuesday|wednesday|thursday|friday|saturday|sunday)\b/i,
|
|
640
|
-
/\bremind(?:er|s)?\b/i,
|
|
641
|
-
/\brun\s+at\b/i
|
|
642
|
-
];
|
|
643
|
-
var ACTION_PRIORITIES = ["urgent", "high", "medium", "low"];
|
|
644
|
-
function parsePriority(value, { allowClear }) {
|
|
645
|
-
if (value === void 0) {
|
|
646
|
-
return void 0;
|
|
647
|
-
}
|
|
648
|
-
if (typeof value !== "string") {
|
|
649
|
-
failValidation(
|
|
650
|
-
`--priority must be one of: ${ACTION_PRIORITIES.join(", ")}${allowClear ? ", or 'none' to clear" : ""}.`
|
|
651
|
-
);
|
|
652
|
-
}
|
|
653
|
-
const trimmed = value.trim().toLowerCase();
|
|
654
|
-
if (trimmed === "") {
|
|
655
|
-
if (allowClear) {
|
|
656
|
-
return null;
|
|
657
|
-
}
|
|
658
|
-
return void 0;
|
|
659
|
-
}
|
|
660
|
-
if (allowClear && (trimmed === "none" || trimmed === "clear")) {
|
|
661
|
-
return null;
|
|
662
|
-
}
|
|
663
|
-
if (ACTION_PRIORITIES.includes(trimmed)) {
|
|
664
|
-
return trimmed;
|
|
665
|
-
}
|
|
666
|
-
failValidation(
|
|
667
|
-
`Unknown --priority "${value}". Expected one of: ${ACTION_PRIORITIES.join(", ")}${allowClear ? ", or 'none' to clear" : ""}.`
|
|
668
|
-
);
|
|
669
|
-
}
|
|
670
|
-
function looksScheduled(name, description) {
|
|
671
|
-
const haystack = fold(`${name}
|
|
672
|
-
${description}`);
|
|
673
|
-
return SCHEDULE_SIGNALS.some((re2) => re2.test(haystack));
|
|
674
|
-
}
|
|
675
|
-
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)`;
|
|
676
|
-
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)";
|
|
677
|
-
var BUDGET_VERB = "(?:cap|raise|lower|increase|decrease|adjust|change|set|update|reallocate|shift|limitar|subir|bajar|ajustar|aplicar|reasignar)";
|
|
678
|
-
var re = (...parts) => new RegExp(parts.join(""), "i");
|
|
679
|
-
var SURFACE_RULES = [
|
|
680
|
-
{
|
|
681
|
-
surface: "ask-user",
|
|
682
|
-
signals: [
|
|
683
|
-
/\bproduction\s+(?:domain|url|site|hostname)\b/i,
|
|
684
|
-
/\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,
|
|
685
|
-
/\bmissing\s+(?:the\s+)?(?:value|values|id|ids|url|budget|domain|credentials?|account\s+id|measurement\s+id)\b/i,
|
|
686
|
-
/\b(?:ask|get)\s+(?:the\s+)?(?:client|user)\s+for\b/i,
|
|
687
|
-
/\b(?:pedir|solicitar)\s+(?:al\s+)?cliente\b/i
|
|
688
|
-
],
|
|
689
|
-
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."
|
|
690
|
-
},
|
|
691
|
-
{
|
|
692
|
-
surface: "analysis",
|
|
693
|
-
nameOnly: true,
|
|
694
|
-
signals: [
|
|
695
|
-
/^\s*(?:re-?)?(?:investigat|analy[sz]|audit|review|document|assess|evaluat|research|map|inventor|examin|diagnos|verif|validat|benchmark)\w*\b/i,
|
|
696
|
-
/^\s*(?:auditar|auditoria|investigar|investigacion|revisar|revision|analizar|analisis|documentar|evaluar|evaluacion|diagnosticar|diagnostico|mapear|inventariar|verificar|verificacion|comprobar|estudiar|estudio)\b/i
|
|
697
|
-
],
|
|
698
|
-
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`)."
|
|
699
|
-
},
|
|
700
|
-
{
|
|
701
|
-
surface: "ads-write",
|
|
702
|
-
// "Approve X" is a human decision, not a staged write — and the budget
|
|
703
|
-
// signals are broad enough to swallow it otherwise. Name-anchored so a
|
|
704
|
-
// description recalling an earlier approval doesn't veto real work.
|
|
705
|
-
exceptName: [/^\s*(?:approve|approval|sign[-\s]?off|aprobar|aprobacion)\b/i],
|
|
706
|
-
// Bare `keywords` is the work when it's the subject of the Task, but plain
|
|
707
|
-
// context when a description happens to discuss search terms.
|
|
708
|
-
nameOnlySignals: [/\b(?:keywords?|palabras\s+clave)\b/i],
|
|
709
|
-
signals: [
|
|
710
|
-
// Targeting.
|
|
711
|
-
/\b(?:geo|location|geographic|geografic\w*)[\s-]*(?:targeting|target|segmentacion)\b/i,
|
|
712
|
-
/\bsegmentacion\s+(?:geografica|por\s+ubicacion)\b/i,
|
|
713
|
-
/\bpresence[-\s_]?(?:only|or[-\s_]?interest)\b/i,
|
|
714
|
-
/\b(?:ad\s*schedul\w*|dayparting|calendario\s+de\s+anuncios)\b/i,
|
|
715
|
-
// Extensions / assets.
|
|
716
|
-
/\b(?:callouts?|sitelinks?|site\s*links?|structured\s+snippets?|price\s+extensions?|lead\s+form\s+extensions?|extensiones|asset\s+groups?)\b/i,
|
|
717
|
-
// Ad copy. Compound forms only — bare `headline` and `copy` belong to
|
|
718
|
-
// `landing`, and an RSA headline has to say so.
|
|
719
|
-
/\b(?:rsas?|responsive\s+search\s+ads?|ad\s+copy|copy\s+de\s+(?:los\s+)?anuncios)\b/i,
|
|
720
|
-
/\b(?:headline|titular|description)\s+(?:pool|set|slots?)\b/i,
|
|
721
|
-
/\bdisplay\s+paths?\b|\brutas?\s+visibles?\b/i,
|
|
722
|
-
// Keywords. `negatives` is bare in English because in this product's
|
|
723
|
-
// vocabulary the noun is always negative keywords; the Spanish side is
|
|
724
|
-
// feminine-only, because "resultados negativos" is a real phrase.
|
|
725
|
-
/\bmatch\s+types?\b|\bconcordancias?\b/i,
|
|
726
|
-
/\bnegatives?\b/i,
|
|
727
|
-
/\b(?:palabras\s+clave\s+)?negativas\b/i,
|
|
728
|
-
/\bkeyword\s+lists?\b|\blistas?\s+de\s+palabras\s+clave\b/i,
|
|
729
|
-
// Audiences. `LAL` is the operators' own shorthand for a lookalike and
|
|
730
|
-
// has no other reading in this corpus.
|
|
731
|
-
/\b(?:in-?market|affinity|audience\s+signals?|senales?\s+de\s+audiencia)\b/i,
|
|
732
|
-
/\b(?:audiencias?|lal)\b/i,
|
|
733
|
-
/\blistas?\s+de\s+(?:exclusion|remarketing)\b|\bexclusion\s+lists?\b/i,
|
|
734
|
-
// URLs.
|
|
735
|
-
/\btracking\s+(?:url\s+)?templates?\b|\bfinal\s+urls?\b|\bfinal\s+url\s+suffix\b/i,
|
|
736
|
-
// Bidding.
|
|
737
|
-
/\bbid\s+(?:adjustments?|modifiers?|strateg\w*)\b|\bajustes?\s+de\s+puja\b/i,
|
|
738
|
-
/\b(?:tcpa|troas|target\s+cpa|target\s+roas)\b/i,
|
|
739
|
-
// Lifecycle. Bare `campaign` is deliberately NOT a signal — it appears
|
|
740
|
-
// everywhere and would strip tag-manager and landing of correct
|
|
741
|
-
// routings, so every campaign-shaped rule needs a verb or a specific
|
|
742
|
-
// setting noun next to it.
|
|
743
|
-
// Bounded, and never across the name/description boundary. `ads` is an
|
|
744
|
-
// ordinary word in this domain, so an unbounded gap lets a verb in the
|
|
745
|
-
// name pair with a passing mention in the description — and since
|
|
746
|
-
// ads-write is evaluated first, that spurious match outranks the surface
|
|
747
|
-
// which should have answered.
|
|
748
|
-
re(String.raw`\b`, LIFECYCLE_VERB, String.raw`\b[^\n]{0,40}\b`, CAMPAIGN_ENTITY, String.raw`\b`),
|
|
749
|
-
// Budget. Two complementary shapes — verb before, quantity after — so a
|
|
750
|
-
// change stated either way lands, guarded by the `exceptName` veto above.
|
|
751
|
-
/\b(?:daily|campaign|lifetime|ad\s*set|minimum|min|competitor)\s+budgets?\b/i,
|
|
752
|
-
/\bpresupuestos?\b/i,
|
|
753
|
-
re(String.raw`\b`, BUDGET_VERB, String.raw`\b[\s\S]{0,24}\bbudgets?\b`),
|
|
754
|
-
/\bbudgets?\b[\s\S]{0,24}\b(?:at|to|cap|down|up|\d|%|€|\$)/i,
|
|
755
|
-
/\bnegative\s+keywords?\b/i,
|
|
756
|
-
/\b(?:lookalike|custom)\s+audience\b/i,
|
|
757
|
-
/\bswap\s+(?:the\s+)?creative\b/i
|
|
758
|
-
],
|
|
759
|
-
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."
|
|
760
|
-
},
|
|
761
|
-
{
|
|
762
|
-
surface: "tag-manager",
|
|
763
|
-
signals: [
|
|
764
|
-
/\bgtm\b/i,
|
|
765
|
-
/\btag\s*manager\b/i,
|
|
766
|
-
/\bdata\s*layer\b/i,
|
|
767
|
-
/\bfiring\s+trigger\b/i,
|
|
768
|
-
/\bbuilt-?in\s+variables?\b/i,
|
|
769
|
-
/\bconsent\s+(?:mode|settings?|state)\b/i,
|
|
770
|
-
/\bga4\s+(?:config|configuration|event)\b/i,
|
|
771
|
-
/\bcontainer\s+(?:tag|trigger|variable)s?\b/i,
|
|
772
|
-
/\b(?:conversion|form|submit|submission|click|purchase|lead|custom)\s+event\b/i,
|
|
773
|
-
/\btrack\w*\b[\s\S]*\bevent\b/i,
|
|
774
|
-
/\beventos?\s+de\s+(?:formulario|conversion|clic|compra)\b/i,
|
|
775
|
-
/\bconsentimiento\b/i,
|
|
776
|
-
/\bmedicion\b/i
|
|
777
|
-
],
|
|
778
|
-
except: [/\bserver-?\s?side\b/i, /\bsgtm\b/i, /\bhosting\b/i],
|
|
779
|
-
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."
|
|
780
|
-
},
|
|
781
|
-
{
|
|
782
|
-
surface: "site-tags",
|
|
783
|
-
signals: [
|
|
784
|
-
/\bpixel\b/i,
|
|
785
|
-
/\binsight\s+tag\b/i,
|
|
786
|
-
/\bclarity\b/i,
|
|
787
|
-
/\bhotjar\b/i,
|
|
788
|
-
/\bgtm\s+snippet\b/i,
|
|
789
|
-
/\bcapi\b/i,
|
|
790
|
-
/\b(?:install|add|remove|swap|instalar|colocar)\b[\s\S]*\b(?:snippet|script|tag)\b/i
|
|
791
|
-
],
|
|
792
|
-
// Two vetoes. A structured snippet is a Google Ads extension, not a script
|
|
793
|
-
// on the page — `ads-write` claims it above, and this stops both rules
|
|
794
|
-
// being able to answer. Server-side delivery is infrastructure nobody here
|
|
795
|
-
// can provision, the same call `tag-manager` already makes for sGTM.
|
|
796
|
-
except: [/\bstructured\s+snippets?\b/i, /\bserver-?\s?side\b/i, /\bhosting\b/i],
|
|
797
|
-
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."
|
|
798
|
-
},
|
|
799
|
-
{
|
|
800
|
-
surface: "landing",
|
|
801
|
-
nameOnlySignals: [/\b(?:landing\s+pages?|landings?|hero|headline|above\s+the\s+fold|pagina)\b/i],
|
|
802
|
-
signals: [/\b(?:rewrite|tighten|restyle)\b[\s\S]*\b(?:copy|page|section)\b/i],
|
|
803
|
-
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."
|
|
804
|
-
},
|
|
805
|
-
{
|
|
806
|
-
surface: "flow",
|
|
807
|
-
signals: [/\b(?:form|flow)\s+(?:step|steps|branching|logic|redirect|behaviou?r)\b/i, /\bthank\s*you\s+redirect\b/i],
|
|
808
|
-
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."
|
|
809
|
-
}
|
|
810
|
-
];
|
|
811
|
-
var FAN_OUT_NUDGE_THRESHOLD = 2;
|
|
812
|
-
var FAN_OUT_THRESHOLD = 3;
|
|
813
|
-
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.';
|
|
814
|
-
function looksExecutable(name, description) {
|
|
815
|
-
const foldedName = fold(name);
|
|
816
|
-
const full = `${foldedName}
|
|
817
|
-
${fold(description)}`;
|
|
818
|
-
for (const rule of SURFACE_RULES) {
|
|
819
|
-
const haystack = rule.nameOnly ? foldedName : full;
|
|
820
|
-
if (rule.except?.some((re2) => re2.test(full))) {
|
|
821
|
-
continue;
|
|
822
|
-
}
|
|
823
|
-
if (rule.exceptName?.some((re2) => re2.test(foldedName))) {
|
|
824
|
-
continue;
|
|
825
|
-
}
|
|
826
|
-
if (rule.signals.some((re2) => re2.test(haystack))) {
|
|
827
|
-
return { surface: rule.surface, hint: rule.hint };
|
|
828
|
-
}
|
|
829
|
-
if (rule.nameOnlySignals?.some((re2) => re2.test(foldedName))) {
|
|
830
|
-
return { surface: rule.surface, hint: rule.hint };
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
return null;
|
|
834
|
-
}
|
|
835
|
-
function buildCreateHints({
|
|
836
|
-
name,
|
|
837
|
-
description,
|
|
838
|
-
tempId,
|
|
839
|
-
tags,
|
|
840
|
-
prioritySet,
|
|
841
|
-
draftCreateCount
|
|
842
|
-
}) {
|
|
843
|
-
const hints = [];
|
|
844
|
-
const advisory = advisoryHint(name, description);
|
|
845
|
-
if (advisory) {
|
|
846
|
-
hints.push(
|
|
847
|
-
`${advisory.hint} If nothing actually blocks it: baker actions draft remove ${tempId}, then do the work.`
|
|
848
|
-
);
|
|
849
|
-
}
|
|
850
|
-
if (draftCreateCount >= FAN_OUT_THRESHOLD) {
|
|
851
|
-
hints.push(
|
|
852
|
-
`${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>\`.`
|
|
853
|
-
);
|
|
854
|
-
} else if (draftCreateCount === FAN_OUT_NUDGE_THRESHOLD) {
|
|
855
|
-
hints.push(
|
|
856
|
-
`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}\`.`
|
|
857
|
-
);
|
|
858
|
-
}
|
|
859
|
-
hints.push(`Link dependencies: baker actions link --blocker <id> --blocked ${tempId}`);
|
|
860
|
-
if (!description) {
|
|
861
|
-
hints.push("Add description: baker actions update <tempId> --description '...' (what/why/where/done-when)");
|
|
862
|
-
}
|
|
863
|
-
if (!tags || tags.length === 0) {
|
|
864
|
-
hints.push(
|
|
865
|
-
"MISSING --tags. This action is invisible to the backlog's tag filter. Re-run with --tags <slug,...> (`baker actions tags list` for the taxonomy, `baker actions tags create --slug <slug>` to mint) \u2014 or `baker actions update <tempId> --tags <slug,...>`."
|
|
866
|
-
);
|
|
867
|
-
}
|
|
868
|
-
if (!prioritySet) {
|
|
869
|
-
hints.push(
|
|
870
|
-
`MISSING --priority. Without it this action ranks as 'normal' (medium) in the do-first ordering, so urgent/high client work won't surface first. Re-run with --priority ${ACTION_PRIORITIES.join("|")} \u2014 or \`baker actions update <tempId> --priority <level>\`.`
|
|
871
|
-
);
|
|
872
|
-
}
|
|
873
|
-
return hints;
|
|
874
|
-
}
|
|
875
|
-
function advisoryHint(name, description) {
|
|
876
|
-
if (looksScheduled(name, description)) {
|
|
877
|
-
return { kind: "scheduled", hint: SCHEDULED_HINT };
|
|
878
|
-
}
|
|
879
|
-
const executable = looksExecutable(name, description);
|
|
880
|
-
return executable ? { kind: "executable", ...executable } : null;
|
|
881
|
-
}
|
|
882
|
-
var ACTIONS_LIST_DEFAULT_LIMIT = 500;
|
|
883
|
-
function buildListHints({ returned, limit }) {
|
|
884
|
-
if (returned < limit) {
|
|
885
|
-
return [];
|
|
886
|
-
}
|
|
887
|
-
return [
|
|
888
|
-
`This list is capped at ${limit} Tasks and came back full, so older Tasks exist that are NOT shown. Do not treat it as the whole backlog \u2014 re-run with --limit ${limit * 2} (or narrow with --status / --q).`
|
|
889
|
-
];
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
// src/commands/actions/skillCatalog.ts
|
|
893
|
-
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
894
|
-
import { dirname, join } from "path";
|
|
895
|
-
var DESCRIPTION_MAX = 600;
|
|
896
|
-
var SKILLS_SUBPATH = join(".claude", "skills");
|
|
897
|
-
var EXCLUDED_SKILLS = /* @__PURE__ */ new Set(["actions"]);
|
|
898
|
-
function stripQuotes(value) {
|
|
899
|
-
const trimmed = value.trim();
|
|
900
|
-
if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
|
|
901
|
-
return trimmed.slice(1, -1).replace(/\\"/g, '"').replace(/\\n/g, "\n");
|
|
902
|
-
}
|
|
903
|
-
if (trimmed.startsWith("'") && trimmed.endsWith("'")) {
|
|
904
|
-
return trimmed.slice(1, -1).replace(/''/g, "'");
|
|
905
|
-
}
|
|
906
|
-
return trimmed;
|
|
907
|
-
}
|
|
908
|
-
function extractFrontmatterLines(md) {
|
|
909
|
-
if (!md.startsWith("---")) return null;
|
|
910
|
-
const end = md.indexOf("\n---", 3);
|
|
911
|
-
if (end === -1) return null;
|
|
912
|
-
return md.slice(md.indexOf("\n", 3) + 1, end).split("\n");
|
|
913
|
-
}
|
|
914
|
-
var BLOCK_SCALAR = /^([|>])[+-]?$/;
|
|
915
|
-
function collectBlockLines(lines, start) {
|
|
916
|
-
const collected = [];
|
|
917
|
-
for (let i = start; i < lines.length; i++) {
|
|
918
|
-
const line = lines[i] ?? "";
|
|
919
|
-
if (line.trim() === "") {
|
|
920
|
-
collected.push("");
|
|
921
|
-
} else if (/^\s/.test(line)) {
|
|
922
|
-
collected.push(line.trim());
|
|
923
|
-
} else {
|
|
924
|
-
break;
|
|
925
|
-
}
|
|
926
|
-
}
|
|
927
|
-
while (collected.length > 0 && collected.at(-1) === "") collected.pop();
|
|
928
|
-
return collected;
|
|
929
|
-
}
|
|
930
|
-
function foldLines(collected) {
|
|
931
|
-
const paragraphs = [];
|
|
932
|
-
let buffer = [];
|
|
933
|
-
for (const line of collected) {
|
|
934
|
-
if (line === "") {
|
|
935
|
-
if (buffer.length > 0) paragraphs.push(buffer.join(" "));
|
|
936
|
-
buffer = [];
|
|
937
|
-
} else {
|
|
938
|
-
buffer.push(line);
|
|
939
|
-
}
|
|
940
|
-
}
|
|
941
|
-
if (buffer.length > 0) paragraphs.push(buffer.join(" "));
|
|
942
|
-
return paragraphs.join("\n");
|
|
943
|
-
}
|
|
944
|
-
function readBlockScalar(lines, start, style) {
|
|
945
|
-
const collected = collectBlockLines(lines, start);
|
|
946
|
-
return style === "|" ? collected.join("\n") : foldLines(collected);
|
|
947
|
-
}
|
|
948
|
-
function readField(lines, field) {
|
|
949
|
-
for (let i = 0; i < lines.length; i++) {
|
|
950
|
-
const match = (lines[i] ?? "").match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
|
|
951
|
-
if (!match || match[1] !== field) continue;
|
|
952
|
-
const inline = (match[2] ?? "").trim();
|
|
953
|
-
const scalar = inline.match(BLOCK_SCALAR);
|
|
954
|
-
if (scalar) {
|
|
955
|
-
return readBlockScalar(lines, i + 1, scalar[1]);
|
|
956
|
-
}
|
|
957
|
-
return stripQuotes(inline);
|
|
958
|
-
}
|
|
959
|
-
return null;
|
|
960
|
-
}
|
|
961
|
-
function parseSkillFrontmatter(md) {
|
|
962
|
-
const lines = extractFrontmatterLines(md);
|
|
963
|
-
if (!lines) return null;
|
|
964
|
-
const name = readField(lines, "name");
|
|
965
|
-
const description = readField(lines, "description");
|
|
966
|
-
if (!name || !description) return null;
|
|
967
|
-
const trimmed = description.length > DESCRIPTION_MAX ? `${description.slice(0, DESCRIPTION_MAX)}\u2026` : description;
|
|
968
|
-
return { name, description: trimmed };
|
|
969
|
-
}
|
|
970
|
-
function findSkillsDir(startDir) {
|
|
971
|
-
let dir = startDir;
|
|
972
|
-
for (; ; ) {
|
|
973
|
-
const candidate = join(dir, SKILLS_SUBPATH);
|
|
974
|
-
if (existsSync(candidate)) return candidate;
|
|
975
|
-
const parent = dirname(dir);
|
|
976
|
-
if (parent === dir) return null;
|
|
977
|
-
dir = parent;
|
|
978
|
-
}
|
|
979
|
-
}
|
|
980
|
-
function readSkillCatalog(startDir) {
|
|
981
|
-
const skillsDir = findSkillsDir(startDir);
|
|
982
|
-
if (!skillsDir) return [];
|
|
983
|
-
const entries = [];
|
|
984
|
-
for (const dirent of readdirSync(skillsDir, { withFileTypes: true })) {
|
|
985
|
-
if (!dirent.isDirectory() || EXCLUDED_SKILLS.has(dirent.name)) continue;
|
|
986
|
-
const skillFile = join(skillsDir, dirent.name, "SKILL.md");
|
|
987
|
-
if (!existsSync(skillFile)) continue;
|
|
988
|
-
try {
|
|
989
|
-
const parsed = parseSkillFrontmatter(readFileSync(skillFile, "utf8"));
|
|
990
|
-
if (parsed) entries.push(parsed);
|
|
991
|
-
} catch {
|
|
992
|
-
}
|
|
993
|
-
}
|
|
994
|
-
return entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
// src/commands/actions/claim.ts
|
|
998
|
-
registerSchema({
|
|
999
|
-
command: "actions.claim",
|
|
1000
|
-
description: "Claim an action for the current chat (live \u2014 visible to other chats immediately). Claim only what you're actively working on now: it's required before `complete`, but NOT for `update` or `discard` (those stage without a claim). Returns action details plus a fast-model recommendation of which skills to load for the work (`recommendedSkills`) and a routing hint: load every skill that owns part of the work and the tool doc for each baker CLI family you'll use.",
|
|
1001
|
-
args: {
|
|
1002
|
-
id: { type: "string", description: "Action ID", required: true }
|
|
1003
|
-
}
|
|
1004
|
-
});
|
|
1005
|
-
async function recommendSkills(actionId) {
|
|
1006
|
-
const skills = readSkillCatalog(process.cwd());
|
|
1007
|
-
if (skills.length === 0) return [];
|
|
1008
|
-
const response = await apiPost("/api/actions/recommend-skills", {
|
|
1009
|
-
actionId,
|
|
1010
|
-
skills
|
|
1011
|
-
});
|
|
1012
|
-
return response.data.recommendations;
|
|
1013
|
-
}
|
|
1014
|
-
var ROUTING_HINT = "Actions often span several surfaces (a landing page plus a Google Ads change, a sheet pull plus an audience upload). Before starting, load EVERY skill that owns part of the work and read the tool doc (__tooling__/docs/tools/baker/<family>.md) for each baker CLI family you'll use. If the action references source material living in a connected tool (an email attachment, a call, a CRM record, a spreadsheet), reach for that tool too. If it describes recurring or future-dated work, it belongs on a schedule \u2014 see __tooling__/docs/tools/baker/scheduled-actions.md.";
|
|
1015
|
-
function buildHints(recommendations) {
|
|
1016
|
-
if (recommendations.length === 0) {
|
|
1017
|
-
return ["Review the action name, description, and tags above.", ROUTING_HINT];
|
|
1018
|
-
}
|
|
1019
|
-
return [
|
|
1020
|
-
"Recommended skills for this action (load the ones you'll use):",
|
|
1021
|
-
...recommendations.map((r) => ` /${r.name} \u2014 ${r.reason}`),
|
|
1022
|
-
"Suggestions come from the action's name/description/tags only \u2014 the work may need more.",
|
|
1023
|
-
ROUTING_HINT
|
|
1024
|
-
];
|
|
1025
|
-
}
|
|
1026
|
-
var claimCommand = defineCommand({
|
|
1027
|
-
meta: {
|
|
1028
|
-
name: "claim",
|
|
1029
|
-
description: "Claim an action so other chats see you're working on it. Required before `complete` (update and discard don't need a claim). Example: baker actions claim <action-id>"
|
|
1030
|
-
},
|
|
1031
|
-
args: {
|
|
1032
|
-
id: { type: "positional", description: "Action ID", required: false },
|
|
1033
|
-
"action-id": { type: "string", description: "Action ID", required: false }
|
|
1034
|
-
},
|
|
1035
|
-
run: async ({ args }) => {
|
|
1036
|
-
try {
|
|
1037
|
-
const id = args.id || args["action-id"];
|
|
1038
|
-
if (!id) {
|
|
1039
|
-
failValidation("Action ID is required.");
|
|
1040
|
-
}
|
|
1041
|
-
validateConvexId(id);
|
|
1042
|
-
const chatId = requireChatId();
|
|
1043
|
-
const response = await apiPost("/api/actions/claim", {
|
|
1044
|
-
actionId: id,
|
|
1045
|
-
chatId
|
|
1046
|
-
});
|
|
1047
|
-
let recommendedSkills = [];
|
|
1048
|
-
try {
|
|
1049
|
-
recommendedSkills = await recommendSkills(id);
|
|
1050
|
-
} catch {
|
|
1051
|
-
}
|
|
1052
|
-
const data = response.data ? { ...response.data, recommendedSkills } : response.data;
|
|
1053
|
-
writeJson({ ok: response.ok, data, hints: buildHints(recommendedSkills) });
|
|
1054
|
-
} catch (err) {
|
|
1055
|
-
failApi(err);
|
|
1056
|
-
}
|
|
1057
|
-
}
|
|
1058
|
-
});
|
|
1059
|
-
|
|
1060
568
|
// ../api/src/actions.ts
|
|
1061
569
|
import { z } from "zod";
|
|
1062
570
|
var actionStatusSchema = z.enum(["pending", "in_progress", "completed", "discarded"]);
|
|
@@ -1199,9 +707,13 @@ function okResponse(data) {
|
|
|
1199
707
|
function okOnlySchema() {
|
|
1200
708
|
return z.object({ ok: z.literal(true) });
|
|
1201
709
|
}
|
|
710
|
+
var ACTION_TEMP_ID_PREFIX = "temp_";
|
|
711
|
+
var actionTempIdSchema = z.string().refine((value) => value.startsWith(ACTION_TEMP_ID_PREFIX) && value.length > ACTION_TEMP_ID_PREFIX.length, {
|
|
712
|
+
message: "tempId must start with `temp_` and have something after it (e.g. temp_hero_copy). Omit --temp-id to get a generated one."
|
|
713
|
+
});
|
|
1202
714
|
var actionsCreateRequestSchema = z.object({
|
|
1203
715
|
chatId: z.string(),
|
|
1204
|
-
tempId:
|
|
716
|
+
tempId: actionTempIdSchema,
|
|
1205
717
|
name: z.string().min(1),
|
|
1206
718
|
description: z.string(),
|
|
1207
719
|
tags: z.array(z.string()).optional(),
|
|
@@ -6665,6 +6177,498 @@ var videosIngestResponseSchema = z21.object({
|
|
|
6665
6177
|
var videosDeleteRequestSchema = z21.object({ id: z21.string().min(1, "Missing video ID") });
|
|
6666
6178
|
var videosDeleteResponseSchema = z21.object({ ok: z21.literal(true) });
|
|
6667
6179
|
|
|
6180
|
+
// src/commands/actions/tagError.ts
|
|
6181
|
+
var UNKNOWN_TAG_RE = /Unknown action tag\(s\): ([^.]+)\. Valid tags: ([^.]+)\./;
|
|
6182
|
+
function splitSlugs(list) {
|
|
6183
|
+
return list.split(",").map((slug) => slug.trim()).filter((slug) => slug.length > 0);
|
|
6184
|
+
}
|
|
6185
|
+
function actionTagFix(message) {
|
|
6186
|
+
const match = UNKNOWN_TAG_RE.exec(message);
|
|
6187
|
+
if (!match) {
|
|
6188
|
+
return null;
|
|
6189
|
+
}
|
|
6190
|
+
const [, unknownList, validList] = match;
|
|
6191
|
+
const unknownTags = splitSlugs(unknownList ?? "");
|
|
6192
|
+
const validTags = splitSlugs(validList ?? "");
|
|
6193
|
+
const first = unknownTags[0];
|
|
6194
|
+
if (first === void 0 || validTags.length === 0) {
|
|
6195
|
+
return null;
|
|
6196
|
+
}
|
|
6197
|
+
return {
|
|
6198
|
+
action: "retry_with_a_valid_tag",
|
|
6199
|
+
explanation: `The tag(s) ${unknownTags.join(", ")} are not in this company's taxonomy. \`validTags\` below is the complete accepted set \u2014 pick from it and re-run the same command. Only mint a new tag when none of them names this work; a near-duplicate of an existing tag splits the backlog's filter instead of extending it.`,
|
|
6200
|
+
unknownTags,
|
|
6201
|
+
validTags,
|
|
6202
|
+
mintCommand: `baker actions tags create --slug ${first} --description "<what ${first} groups>"`,
|
|
6203
|
+
listCommand: "baker actions tags list"
|
|
6204
|
+
};
|
|
6205
|
+
}
|
|
6206
|
+
|
|
6207
|
+
// src/commands/actions/shared.ts
|
|
6208
|
+
function writeOk(data) {
|
|
6209
|
+
writeJson({ ok: true, data: data ?? null });
|
|
6210
|
+
}
|
|
6211
|
+
function failValidation(message) {
|
|
6212
|
+
writeJson({ ok: false, error: { code: "VALIDATION_ERROR", message } });
|
|
6213
|
+
process.exit(1);
|
|
6214
|
+
}
|
|
6215
|
+
function failApi(err) {
|
|
6216
|
+
if (err instanceof ApiError) {
|
|
6217
|
+
const fix = actionTagFix(err.message);
|
|
6218
|
+
writeJson({ ok: false, error: { code: err.code, message: err.message, ...fix ? { fix } : {} } });
|
|
6219
|
+
process.exit(1);
|
|
6220
|
+
}
|
|
6221
|
+
if (err instanceof Error) {
|
|
6222
|
+
writeJson({ ok: false, error: { code: "INTERNAL_ERROR", message: err.message } });
|
|
6223
|
+
process.exit(1);
|
|
6224
|
+
}
|
|
6225
|
+
writeJson({ ok: false, error: { code: "INTERNAL_ERROR", message: "Unexpected error" } });
|
|
6226
|
+
process.exit(1);
|
|
6227
|
+
}
|
|
6228
|
+
function generateTempId() {
|
|
6229
|
+
return `${ACTION_TEMP_ID_PREFIX}${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
6230
|
+
}
|
|
6231
|
+
function isTempId(id) {
|
|
6232
|
+
return id.startsWith(ACTION_TEMP_ID_PREFIX);
|
|
6233
|
+
}
|
|
6234
|
+
function parseTagList(value) {
|
|
6235
|
+
if (typeof value !== "string") {
|
|
6236
|
+
return void 0;
|
|
6237
|
+
}
|
|
6238
|
+
const tags = value.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
|
|
6239
|
+
return [...new Set(tags)];
|
|
6240
|
+
}
|
|
6241
|
+
function fold(text2) {
|
|
6242
|
+
return text2.normalize("NFD").replace(/[̀-ͯ]/g, "");
|
|
6243
|
+
}
|
|
6244
|
+
var SCHEDULE_SIGNALS = [
|
|
6245
|
+
/\brecurr(?:ing|ence)\b/i,
|
|
6246
|
+
/\bcadence\b/i,
|
|
6247
|
+
/\bcron\b/i,
|
|
6248
|
+
/\bschedule[ds]?\b/i,
|
|
6249
|
+
/\b(?:daily|weekly|monthly|quarterly|annual(?:ly)?|biweekly|nightly)\b/i,
|
|
6250
|
+
/\bevery\s+(?:day|week|month|quarter|year|morning|monday|tuesday|wednesday|thursday|friday|saturday|sunday|\d)/i,
|
|
6251
|
+
/\beach\s+(?:day|week|month|monday|tuesday|wednesday|thursday|friday|saturday|sunday)\b/i,
|
|
6252
|
+
/\bremind(?:er|s)?\b/i,
|
|
6253
|
+
/\brun\s+at\b/i
|
|
6254
|
+
];
|
|
6255
|
+
var ACTION_PRIORITIES = ["urgent", "high", "medium", "low"];
|
|
6256
|
+
function parsePriority(value, { allowClear }) {
|
|
6257
|
+
if (value === void 0) {
|
|
6258
|
+
return void 0;
|
|
6259
|
+
}
|
|
6260
|
+
if (typeof value !== "string") {
|
|
6261
|
+
failValidation(
|
|
6262
|
+
`--priority must be one of: ${ACTION_PRIORITIES.join(", ")}${allowClear ? ", or 'none' to clear" : ""}.`
|
|
6263
|
+
);
|
|
6264
|
+
}
|
|
6265
|
+
const trimmed = value.trim().toLowerCase();
|
|
6266
|
+
if (trimmed === "") {
|
|
6267
|
+
if (allowClear) {
|
|
6268
|
+
return null;
|
|
6269
|
+
}
|
|
6270
|
+
return void 0;
|
|
6271
|
+
}
|
|
6272
|
+
if (allowClear && (trimmed === "none" || trimmed === "clear")) {
|
|
6273
|
+
return null;
|
|
6274
|
+
}
|
|
6275
|
+
if (ACTION_PRIORITIES.includes(trimmed)) {
|
|
6276
|
+
return trimmed;
|
|
6277
|
+
}
|
|
6278
|
+
failValidation(
|
|
6279
|
+
`Unknown --priority "${value}". Expected one of: ${ACTION_PRIORITIES.join(", ")}${allowClear ? ", or 'none' to clear" : ""}.`
|
|
6280
|
+
);
|
|
6281
|
+
}
|
|
6282
|
+
function looksScheduled(name, description) {
|
|
6283
|
+
const haystack = fold(`${name}
|
|
6284
|
+
${description}`);
|
|
6285
|
+
return SCHEDULE_SIGNALS.some((re2) => re2.test(haystack));
|
|
6286
|
+
}
|
|
6287
|
+
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)`;
|
|
6288
|
+
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)";
|
|
6289
|
+
var BUDGET_VERB = "(?:cap|raise|lower|increase|decrease|adjust|change|set|update|reallocate|shift|limitar|subir|bajar|ajustar|aplicar|reasignar)";
|
|
6290
|
+
var re = (...parts) => new RegExp(parts.join(""), "i");
|
|
6291
|
+
var SURFACE_RULES = [
|
|
6292
|
+
{
|
|
6293
|
+
surface: "ask-user",
|
|
6294
|
+
signals: [
|
|
6295
|
+
/\bproduction\s+(?:domain|url|site|hostname)\b/i,
|
|
6296
|
+
/\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,
|
|
6297
|
+
/\bmissing\s+(?:the\s+)?(?:value|values|id|ids|url|budget|domain|credentials?|account\s+id|measurement\s+id)\b/i,
|
|
6298
|
+
/\b(?:ask|get)\s+(?:the\s+)?(?:client|user)\s+for\b/i,
|
|
6299
|
+
/\b(?:pedir|solicitar)\s+(?:al\s+)?cliente\b/i
|
|
6300
|
+
],
|
|
6301
|
+
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."
|
|
6302
|
+
},
|
|
6303
|
+
{
|
|
6304
|
+
surface: "analysis",
|
|
6305
|
+
nameOnly: true,
|
|
6306
|
+
signals: [
|
|
6307
|
+
/^\s*(?:re-?)?(?:investigat|analy[sz]|audit|review|document|assess|evaluat|research|map|inventor|examin|diagnos|verif|validat|benchmark)\w*\b/i,
|
|
6308
|
+
/^\s*(?:auditar|auditoria|investigar|investigacion|revisar|revision|analizar|analisis|documentar|evaluar|evaluacion|diagnosticar|diagnostico|mapear|inventariar|verificar|verificacion|comprobar|estudiar|estudio)\b/i
|
|
6309
|
+
],
|
|
6310
|
+
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`)."
|
|
6311
|
+
},
|
|
6312
|
+
{
|
|
6313
|
+
surface: "ads-write",
|
|
6314
|
+
// "Approve X" is a human decision, not a staged write — and the budget
|
|
6315
|
+
// signals are broad enough to swallow it otherwise. Name-anchored so a
|
|
6316
|
+
// description recalling an earlier approval doesn't veto real work.
|
|
6317
|
+
exceptName: [/^\s*(?:approve|approval|sign[-\s]?off|aprobar|aprobacion)\b/i],
|
|
6318
|
+
// Bare `keywords` is the work when it's the subject of the Task, but plain
|
|
6319
|
+
// context when a description happens to discuss search terms.
|
|
6320
|
+
nameOnlySignals: [/\b(?:keywords?|palabras\s+clave)\b/i],
|
|
6321
|
+
signals: [
|
|
6322
|
+
// Targeting.
|
|
6323
|
+
/\b(?:geo|location|geographic|geografic\w*)[\s-]*(?:targeting|target|segmentacion)\b/i,
|
|
6324
|
+
/\bsegmentacion\s+(?:geografica|por\s+ubicacion)\b/i,
|
|
6325
|
+
/\bpresence[-\s_]?(?:only|or[-\s_]?interest)\b/i,
|
|
6326
|
+
/\b(?:ad\s*schedul\w*|dayparting|calendario\s+de\s+anuncios)\b/i,
|
|
6327
|
+
// Extensions / assets.
|
|
6328
|
+
/\b(?:callouts?|sitelinks?|site\s*links?|structured\s+snippets?|price\s+extensions?|lead\s+form\s+extensions?|extensiones|asset\s+groups?)\b/i,
|
|
6329
|
+
// Ad copy. Compound forms only — bare `headline` and `copy` belong to
|
|
6330
|
+
// `landing`, and an RSA headline has to say so.
|
|
6331
|
+
/\b(?:rsas?|responsive\s+search\s+ads?|ad\s+copy|copy\s+de\s+(?:los\s+)?anuncios)\b/i,
|
|
6332
|
+
/\b(?:headline|titular|description)\s+(?:pool|set|slots?)\b/i,
|
|
6333
|
+
/\bdisplay\s+paths?\b|\brutas?\s+visibles?\b/i,
|
|
6334
|
+
// Keywords. `negatives` is bare in English because in this product's
|
|
6335
|
+
// vocabulary the noun is always negative keywords; the Spanish side is
|
|
6336
|
+
// feminine-only, because "resultados negativos" is a real phrase.
|
|
6337
|
+
/\bmatch\s+types?\b|\bconcordancias?\b/i,
|
|
6338
|
+
/\bnegatives?\b/i,
|
|
6339
|
+
/\b(?:palabras\s+clave\s+)?negativas\b/i,
|
|
6340
|
+
/\bkeyword\s+lists?\b|\blistas?\s+de\s+palabras\s+clave\b/i,
|
|
6341
|
+
// Audiences. `LAL` is the operators' own shorthand for a lookalike and
|
|
6342
|
+
// has no other reading in this corpus.
|
|
6343
|
+
/\b(?:in-?market|affinity|audience\s+signals?|senales?\s+de\s+audiencia)\b/i,
|
|
6344
|
+
/\b(?:audiencias?|lal)\b/i,
|
|
6345
|
+
/\blistas?\s+de\s+(?:exclusion|remarketing)\b|\bexclusion\s+lists?\b/i,
|
|
6346
|
+
// URLs.
|
|
6347
|
+
/\btracking\s+(?:url\s+)?templates?\b|\bfinal\s+urls?\b|\bfinal\s+url\s+suffix\b/i,
|
|
6348
|
+
// Bidding.
|
|
6349
|
+
/\bbid\s+(?:adjustments?|modifiers?|strateg\w*)\b|\bajustes?\s+de\s+puja\b/i,
|
|
6350
|
+
/\b(?:tcpa|troas|target\s+cpa|target\s+roas)\b/i,
|
|
6351
|
+
// Lifecycle. Bare `campaign` is deliberately NOT a signal — it appears
|
|
6352
|
+
// everywhere and would strip tag-manager and landing of correct
|
|
6353
|
+
// routings, so every campaign-shaped rule needs a verb or a specific
|
|
6354
|
+
// setting noun next to it.
|
|
6355
|
+
// Bounded, and never across the name/description boundary. `ads` is an
|
|
6356
|
+
// ordinary word in this domain, so an unbounded gap lets a verb in the
|
|
6357
|
+
// name pair with a passing mention in the description — and since
|
|
6358
|
+
// ads-write is evaluated first, that spurious match outranks the surface
|
|
6359
|
+
// which should have answered.
|
|
6360
|
+
re(String.raw`\b`, LIFECYCLE_VERB, String.raw`\b[^\n]{0,40}\b`, CAMPAIGN_ENTITY, String.raw`\b`),
|
|
6361
|
+
// Budget. Two complementary shapes — verb before, quantity after — so a
|
|
6362
|
+
// change stated either way lands, guarded by the `exceptName` veto above.
|
|
6363
|
+
/\b(?:daily|campaign|lifetime|ad\s*set|minimum|min|competitor)\s+budgets?\b/i,
|
|
6364
|
+
/\bpresupuestos?\b/i,
|
|
6365
|
+
re(String.raw`\b`, BUDGET_VERB, String.raw`\b[\s\S]{0,24}\bbudgets?\b`),
|
|
6366
|
+
/\bbudgets?\b[\s\S]{0,24}\b(?:at|to|cap|down|up|\d|%|€|\$)/i,
|
|
6367
|
+
/\bnegative\s+keywords?\b/i,
|
|
6368
|
+
/\b(?:lookalike|custom)\s+audience\b/i,
|
|
6369
|
+
/\bswap\s+(?:the\s+)?creative\b/i
|
|
6370
|
+
],
|
|
6371
|
+
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."
|
|
6372
|
+
},
|
|
6373
|
+
{
|
|
6374
|
+
surface: "tag-manager",
|
|
6375
|
+
signals: [
|
|
6376
|
+
/\bgtm\b/i,
|
|
6377
|
+
/\btag\s*manager\b/i,
|
|
6378
|
+
/\bdata\s*layer\b/i,
|
|
6379
|
+
/\bfiring\s+trigger\b/i,
|
|
6380
|
+
/\bbuilt-?in\s+variables?\b/i,
|
|
6381
|
+
/\bconsent\s+(?:mode|settings?|state)\b/i,
|
|
6382
|
+
/\bga4\s+(?:config|configuration|event)\b/i,
|
|
6383
|
+
/\bcontainer\s+(?:tag|trigger|variable)s?\b/i,
|
|
6384
|
+
/\b(?:conversion|form|submit|submission|click|purchase|lead|custom)\s+event\b/i,
|
|
6385
|
+
/\btrack\w*\b[\s\S]*\bevent\b/i,
|
|
6386
|
+
/\beventos?\s+de\s+(?:formulario|conversion|clic|compra)\b/i,
|
|
6387
|
+
/\bconsentimiento\b/i,
|
|
6388
|
+
/\bmedicion\b/i
|
|
6389
|
+
],
|
|
6390
|
+
except: [/\bserver-?\s?side\b/i, /\bsgtm\b/i, /\bhosting\b/i],
|
|
6391
|
+
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."
|
|
6392
|
+
},
|
|
6393
|
+
{
|
|
6394
|
+
surface: "site-tags",
|
|
6395
|
+
signals: [
|
|
6396
|
+
/\bpixel\b/i,
|
|
6397
|
+
/\binsight\s+tag\b/i,
|
|
6398
|
+
/\bclarity\b/i,
|
|
6399
|
+
/\bhotjar\b/i,
|
|
6400
|
+
/\bgtm\s+snippet\b/i,
|
|
6401
|
+
/\bcapi\b/i,
|
|
6402
|
+
/\b(?:install|add|remove|swap|instalar|colocar)\b[\s\S]*\b(?:snippet|script|tag)\b/i
|
|
6403
|
+
],
|
|
6404
|
+
// Two vetoes. A structured snippet is a Google Ads extension, not a script
|
|
6405
|
+
// on the page — `ads-write` claims it above, and this stops both rules
|
|
6406
|
+
// being able to answer. Server-side delivery is infrastructure nobody here
|
|
6407
|
+
// can provision, the same call `tag-manager` already makes for sGTM.
|
|
6408
|
+
except: [/\bstructured\s+snippets?\b/i, /\bserver-?\s?side\b/i, /\bhosting\b/i],
|
|
6409
|
+
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."
|
|
6410
|
+
},
|
|
6411
|
+
{
|
|
6412
|
+
surface: "landing",
|
|
6413
|
+
nameOnlySignals: [/\b(?:landing\s+pages?|landings?|hero|headline|above\s+the\s+fold|pagina)\b/i],
|
|
6414
|
+
signals: [/\b(?:rewrite|tighten|restyle)\b[\s\S]*\b(?:copy|page|section)\b/i],
|
|
6415
|
+
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."
|
|
6416
|
+
},
|
|
6417
|
+
{
|
|
6418
|
+
surface: "flow",
|
|
6419
|
+
signals: [/\b(?:form|flow)\s+(?:step|steps|branching|logic|redirect|behaviou?r)\b/i, /\bthank\s*you\s+redirect\b/i],
|
|
6420
|
+
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."
|
|
6421
|
+
}
|
|
6422
|
+
];
|
|
6423
|
+
var FAN_OUT_NUDGE_THRESHOLD = 2;
|
|
6424
|
+
var FAN_OUT_THRESHOLD = 3;
|
|
6425
|
+
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.';
|
|
6426
|
+
function looksExecutable(name, description) {
|
|
6427
|
+
const foldedName = fold(name);
|
|
6428
|
+
const full = `${foldedName}
|
|
6429
|
+
${fold(description)}`;
|
|
6430
|
+
for (const rule of SURFACE_RULES) {
|
|
6431
|
+
const haystack = rule.nameOnly ? foldedName : full;
|
|
6432
|
+
if (rule.except?.some((re2) => re2.test(full))) {
|
|
6433
|
+
continue;
|
|
6434
|
+
}
|
|
6435
|
+
if (rule.exceptName?.some((re2) => re2.test(foldedName))) {
|
|
6436
|
+
continue;
|
|
6437
|
+
}
|
|
6438
|
+
if (rule.signals.some((re2) => re2.test(haystack))) {
|
|
6439
|
+
return { surface: rule.surface, hint: rule.hint };
|
|
6440
|
+
}
|
|
6441
|
+
if (rule.nameOnlySignals?.some((re2) => re2.test(foldedName))) {
|
|
6442
|
+
return { surface: rule.surface, hint: rule.hint };
|
|
6443
|
+
}
|
|
6444
|
+
}
|
|
6445
|
+
return null;
|
|
6446
|
+
}
|
|
6447
|
+
function buildCreateHints({
|
|
6448
|
+
name,
|
|
6449
|
+
description,
|
|
6450
|
+
tempId,
|
|
6451
|
+
tags,
|
|
6452
|
+
prioritySet,
|
|
6453
|
+
draftCreateCount
|
|
6454
|
+
}) {
|
|
6455
|
+
const hints = [];
|
|
6456
|
+
const advisory = advisoryHint(name, description);
|
|
6457
|
+
if (advisory) {
|
|
6458
|
+
hints.push(
|
|
6459
|
+
`${advisory.hint} If nothing actually blocks it: baker actions draft remove ${tempId}, then do the work.`
|
|
6460
|
+
);
|
|
6461
|
+
}
|
|
6462
|
+
if (draftCreateCount >= FAN_OUT_THRESHOLD) {
|
|
6463
|
+
hints.push(
|
|
6464
|
+
`${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>\`.`
|
|
6465
|
+
);
|
|
6466
|
+
} else if (draftCreateCount === FAN_OUT_NUDGE_THRESHOLD) {
|
|
6467
|
+
hints.push(
|
|
6468
|
+
`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}\`.`
|
|
6469
|
+
);
|
|
6470
|
+
}
|
|
6471
|
+
hints.push(`Link dependencies: baker actions link --blocker <id> --blocked ${tempId}`);
|
|
6472
|
+
if (!description) {
|
|
6473
|
+
hints.push("Add description: baker actions update <tempId> --description '...' (what/why/where/done-when)");
|
|
6474
|
+
}
|
|
6475
|
+
if (!tags || tags.length === 0) {
|
|
6476
|
+
hints.push(
|
|
6477
|
+
"MISSING --tags. This action is invisible to the backlog's tag filter. Re-run with --tags <slug,...> (`baker actions tags list` for the taxonomy, `baker actions tags create --slug <slug>` to mint) \u2014 or `baker actions update <tempId> --tags <slug,...>`."
|
|
6478
|
+
);
|
|
6479
|
+
}
|
|
6480
|
+
if (!prioritySet) {
|
|
6481
|
+
hints.push(
|
|
6482
|
+
`MISSING --priority. Without it this action ranks as 'normal' (medium) in the do-first ordering, so urgent/high client work won't surface first. Re-run with --priority ${ACTION_PRIORITIES.join("|")} \u2014 or \`baker actions update <tempId> --priority <level>\`.`
|
|
6483
|
+
);
|
|
6484
|
+
}
|
|
6485
|
+
return hints;
|
|
6486
|
+
}
|
|
6487
|
+
function advisoryHint(name, description) {
|
|
6488
|
+
if (looksScheduled(name, description)) {
|
|
6489
|
+
return { kind: "scheduled", hint: SCHEDULED_HINT };
|
|
6490
|
+
}
|
|
6491
|
+
const executable = looksExecutable(name, description);
|
|
6492
|
+
return executable ? { kind: "executable", ...executable } : null;
|
|
6493
|
+
}
|
|
6494
|
+
var ACTIONS_LIST_DEFAULT_LIMIT = 500;
|
|
6495
|
+
function buildListHints({ returned, limit }) {
|
|
6496
|
+
if (returned < limit) {
|
|
6497
|
+
return [];
|
|
6498
|
+
}
|
|
6499
|
+
return [
|
|
6500
|
+
`This list is capped at ${limit} Tasks and came back full, so older Tasks exist that are NOT shown. Do not treat it as the whole backlog \u2014 re-run with --limit ${limit * 2} (or narrow with --status / --q).`
|
|
6501
|
+
];
|
|
6502
|
+
}
|
|
6503
|
+
|
|
6504
|
+
// src/commands/actions/skillCatalog.ts
|
|
6505
|
+
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
6506
|
+
import { dirname, join } from "path";
|
|
6507
|
+
var DESCRIPTION_MAX = 600;
|
|
6508
|
+
var SKILLS_SUBPATH = join(".claude", "skills");
|
|
6509
|
+
var EXCLUDED_SKILLS = /* @__PURE__ */ new Set(["actions"]);
|
|
6510
|
+
function stripQuotes(value) {
|
|
6511
|
+
const trimmed = value.trim();
|
|
6512
|
+
if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
|
|
6513
|
+
return trimmed.slice(1, -1).replace(/\\"/g, '"').replace(/\\n/g, "\n");
|
|
6514
|
+
}
|
|
6515
|
+
if (trimmed.startsWith("'") && trimmed.endsWith("'")) {
|
|
6516
|
+
return trimmed.slice(1, -1).replace(/''/g, "'");
|
|
6517
|
+
}
|
|
6518
|
+
return trimmed;
|
|
6519
|
+
}
|
|
6520
|
+
function extractFrontmatterLines(md) {
|
|
6521
|
+
if (!md.startsWith("---")) return null;
|
|
6522
|
+
const end = md.indexOf("\n---", 3);
|
|
6523
|
+
if (end === -1) return null;
|
|
6524
|
+
return md.slice(md.indexOf("\n", 3) + 1, end).split("\n");
|
|
6525
|
+
}
|
|
6526
|
+
var BLOCK_SCALAR = /^([|>])[+-]?$/;
|
|
6527
|
+
function collectBlockLines(lines, start) {
|
|
6528
|
+
const collected = [];
|
|
6529
|
+
for (let i = start; i < lines.length; i++) {
|
|
6530
|
+
const line = lines[i] ?? "";
|
|
6531
|
+
if (line.trim() === "") {
|
|
6532
|
+
collected.push("");
|
|
6533
|
+
} else if (/^\s/.test(line)) {
|
|
6534
|
+
collected.push(line.trim());
|
|
6535
|
+
} else {
|
|
6536
|
+
break;
|
|
6537
|
+
}
|
|
6538
|
+
}
|
|
6539
|
+
while (collected.length > 0 && collected.at(-1) === "") collected.pop();
|
|
6540
|
+
return collected;
|
|
6541
|
+
}
|
|
6542
|
+
function foldLines(collected) {
|
|
6543
|
+
const paragraphs = [];
|
|
6544
|
+
let buffer = [];
|
|
6545
|
+
for (const line of collected) {
|
|
6546
|
+
if (line === "") {
|
|
6547
|
+
if (buffer.length > 0) paragraphs.push(buffer.join(" "));
|
|
6548
|
+
buffer = [];
|
|
6549
|
+
} else {
|
|
6550
|
+
buffer.push(line);
|
|
6551
|
+
}
|
|
6552
|
+
}
|
|
6553
|
+
if (buffer.length > 0) paragraphs.push(buffer.join(" "));
|
|
6554
|
+
return paragraphs.join("\n");
|
|
6555
|
+
}
|
|
6556
|
+
function readBlockScalar(lines, start, style) {
|
|
6557
|
+
const collected = collectBlockLines(lines, start);
|
|
6558
|
+
return style === "|" ? collected.join("\n") : foldLines(collected);
|
|
6559
|
+
}
|
|
6560
|
+
function readField(lines, field) {
|
|
6561
|
+
for (let i = 0; i < lines.length; i++) {
|
|
6562
|
+
const match = (lines[i] ?? "").match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
|
|
6563
|
+
if (!match || match[1] !== field) continue;
|
|
6564
|
+
const inline = (match[2] ?? "").trim();
|
|
6565
|
+
const scalar = inline.match(BLOCK_SCALAR);
|
|
6566
|
+
if (scalar) {
|
|
6567
|
+
return readBlockScalar(lines, i + 1, scalar[1]);
|
|
6568
|
+
}
|
|
6569
|
+
return stripQuotes(inline);
|
|
6570
|
+
}
|
|
6571
|
+
return null;
|
|
6572
|
+
}
|
|
6573
|
+
function parseSkillFrontmatter(md) {
|
|
6574
|
+
const lines = extractFrontmatterLines(md);
|
|
6575
|
+
if (!lines) return null;
|
|
6576
|
+
const name = readField(lines, "name");
|
|
6577
|
+
const description = readField(lines, "description");
|
|
6578
|
+
if (!name || !description) return null;
|
|
6579
|
+
const trimmed = description.length > DESCRIPTION_MAX ? `${description.slice(0, DESCRIPTION_MAX)}\u2026` : description;
|
|
6580
|
+
return { name, description: trimmed };
|
|
6581
|
+
}
|
|
6582
|
+
function findSkillsDir(startDir) {
|
|
6583
|
+
let dir = startDir;
|
|
6584
|
+
for (; ; ) {
|
|
6585
|
+
const candidate = join(dir, SKILLS_SUBPATH);
|
|
6586
|
+
if (existsSync(candidate)) return candidate;
|
|
6587
|
+
const parent = dirname(dir);
|
|
6588
|
+
if (parent === dir) return null;
|
|
6589
|
+
dir = parent;
|
|
6590
|
+
}
|
|
6591
|
+
}
|
|
6592
|
+
function readSkillCatalog(startDir) {
|
|
6593
|
+
const skillsDir = findSkillsDir(startDir);
|
|
6594
|
+
if (!skillsDir) return [];
|
|
6595
|
+
const entries = [];
|
|
6596
|
+
for (const dirent of readdirSync(skillsDir, { withFileTypes: true })) {
|
|
6597
|
+
if (!dirent.isDirectory() || EXCLUDED_SKILLS.has(dirent.name)) continue;
|
|
6598
|
+
const skillFile = join(skillsDir, dirent.name, "SKILL.md");
|
|
6599
|
+
if (!existsSync(skillFile)) continue;
|
|
6600
|
+
try {
|
|
6601
|
+
const parsed = parseSkillFrontmatter(readFileSync(skillFile, "utf8"));
|
|
6602
|
+
if (parsed) entries.push(parsed);
|
|
6603
|
+
} catch {
|
|
6604
|
+
}
|
|
6605
|
+
}
|
|
6606
|
+
return entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
6607
|
+
}
|
|
6608
|
+
|
|
6609
|
+
// src/commands/actions/claim.ts
|
|
6610
|
+
registerSchema({
|
|
6611
|
+
command: "actions.claim",
|
|
6612
|
+
description: "Claim an action for the current chat (live \u2014 visible to other chats immediately). Claim only what you're actively working on now: it's required before `complete`, but NOT for `update` or `discard` (those stage without a claim). Returns action details plus a fast-model recommendation of which skills to load for the work (`recommendedSkills`) and a routing hint: load every skill that owns part of the work and the tool doc for each baker CLI family you'll use.",
|
|
6613
|
+
args: {
|
|
6614
|
+
id: { type: "string", description: "Action ID", required: true }
|
|
6615
|
+
}
|
|
6616
|
+
});
|
|
6617
|
+
async function recommendSkills(actionId) {
|
|
6618
|
+
const skills = readSkillCatalog(process.cwd());
|
|
6619
|
+
if (skills.length === 0) return [];
|
|
6620
|
+
const response = await apiPost("/api/actions/recommend-skills", {
|
|
6621
|
+
actionId,
|
|
6622
|
+
skills
|
|
6623
|
+
});
|
|
6624
|
+
return response.data.recommendations;
|
|
6625
|
+
}
|
|
6626
|
+
var ROUTING_HINT = "Actions often span several surfaces (a landing page plus a Google Ads change, a sheet pull plus an audience upload). Before starting, load EVERY skill that owns part of the work and read the tool doc (__tooling__/docs/tools/baker/<family>.md) for each baker CLI family you'll use. If the action references source material living in a connected tool (an email attachment, a call, a CRM record, a spreadsheet), reach for that tool too. If it describes recurring or future-dated work, it belongs on a schedule \u2014 see __tooling__/docs/tools/baker/scheduled-actions.md.";
|
|
6627
|
+
function buildHints(recommendations) {
|
|
6628
|
+
if (recommendations.length === 0) {
|
|
6629
|
+
return ["Review the action name, description, and tags above.", ROUTING_HINT];
|
|
6630
|
+
}
|
|
6631
|
+
return [
|
|
6632
|
+
"Recommended skills for this action (load the ones you'll use):",
|
|
6633
|
+
...recommendations.map((r) => ` /${r.name} \u2014 ${r.reason}`),
|
|
6634
|
+
"Suggestions come from the action's name/description/tags only \u2014 the work may need more.",
|
|
6635
|
+
ROUTING_HINT
|
|
6636
|
+
];
|
|
6637
|
+
}
|
|
6638
|
+
var claimCommand = defineCommand({
|
|
6639
|
+
meta: {
|
|
6640
|
+
name: "claim",
|
|
6641
|
+
description: "Claim an action so other chats see you're working on it. Required before `complete` (update and discard don't need a claim). Example: baker actions claim <action-id>"
|
|
6642
|
+
},
|
|
6643
|
+
args: {
|
|
6644
|
+
id: { type: "positional", description: "Action ID", required: false },
|
|
6645
|
+
"action-id": { type: "string", description: "Action ID", required: false }
|
|
6646
|
+
},
|
|
6647
|
+
run: async ({ args }) => {
|
|
6648
|
+
try {
|
|
6649
|
+
const id = args.id || args["action-id"];
|
|
6650
|
+
if (!id) {
|
|
6651
|
+
failValidation("Action ID is required.");
|
|
6652
|
+
}
|
|
6653
|
+
validateConvexId(id);
|
|
6654
|
+
const chatId = requireChatId();
|
|
6655
|
+
const response = await apiPost("/api/actions/claim", {
|
|
6656
|
+
actionId: id,
|
|
6657
|
+
chatId
|
|
6658
|
+
});
|
|
6659
|
+
let recommendedSkills = [];
|
|
6660
|
+
try {
|
|
6661
|
+
recommendedSkills = await recommendSkills(id);
|
|
6662
|
+
} catch {
|
|
6663
|
+
}
|
|
6664
|
+
const data = response.data ? { ...response.data, recommendedSkills } : response.data;
|
|
6665
|
+
writeJson({ ok: response.ok, data, hints: buildHints(recommendedSkills) });
|
|
6666
|
+
} catch (err) {
|
|
6667
|
+
failApi(err);
|
|
6668
|
+
}
|
|
6669
|
+
}
|
|
6670
|
+
});
|
|
6671
|
+
|
|
6668
6672
|
// src/commands/actions/complete.ts
|
|
6669
6673
|
import { defineCommand as defineCommand2 } from "citty";
|
|
6670
6674
|
registerSchema({
|
|
@@ -6739,7 +6743,11 @@ registerSchema({
|
|
|
6739
6743
|
description: `User priority for the do-first ordering. One of: ${ACTION_PRIORITIES.join(", ")}.`,
|
|
6740
6744
|
required: false
|
|
6741
6745
|
},
|
|
6742
|
-
"temp-id": {
|
|
6746
|
+
"temp-id": {
|
|
6747
|
+
type: "string",
|
|
6748
|
+
description: "Custom tempId \u2014 must start with `temp_` (e.g. temp_hero_copy). Auto-generated if omitted; omitting it is fine.",
|
|
6749
|
+
required: false
|
|
6750
|
+
}
|
|
6743
6751
|
}
|
|
6744
6752
|
});
|
|
6745
6753
|
var createCommand = defineCommand3({
|
|
@@ -6752,7 +6760,7 @@ var createCommand = defineCommand3({
|
|
|
6752
6760
|
description: { type: "string", description: "Description", required: false, default: "" },
|
|
6753
6761
|
tags: { type: "string", description: "Comma-separated tag slugs (see `baker actions tags list`)", required: false },
|
|
6754
6762
|
priority: { type: "string", description: `User priority: ${ACTION_PRIORITIES.join("|")}`, required: false },
|
|
6755
|
-
"temp-id": { type: "string", description: "Optional custom tempId", required: false }
|
|
6763
|
+
"temp-id": { type: "string", description: "Optional custom tempId (must start with `temp_`)", required: false }
|
|
6756
6764
|
},
|
|
6757
6765
|
run: async ({ args }) => {
|
|
6758
6766
|
try {
|
|
@@ -6760,8 +6768,15 @@ var createCommand = defineCommand3({
|
|
|
6760
6768
|
if (!name || name.trim().length === 0) {
|
|
6761
6769
|
failValidation("--name is required.");
|
|
6762
6770
|
}
|
|
6771
|
+
const customTempId = args["temp-id"];
|
|
6772
|
+
if (customTempId) {
|
|
6773
|
+
const parsed = actionTempIdSchema.safeParse(customTempId);
|
|
6774
|
+
if (!parsed.success) {
|
|
6775
|
+
failValidation(parsed.error.issues[0]?.message ?? "Invalid --temp-id.");
|
|
6776
|
+
}
|
|
6777
|
+
}
|
|
6763
6778
|
const chatId = requireChatId();
|
|
6764
|
-
const tempId =
|
|
6779
|
+
const tempId = customTempId || generateTempId();
|
|
6765
6780
|
const tags = parseTagList(args.tags);
|
|
6766
6781
|
const priority = parsePriority(args.priority, { allowClear: false });
|
|
6767
6782
|
const response = await apiPost("/api/actions/create", {
|
|
@@ -7114,7 +7129,8 @@ registerSchema({
|
|
|
7114
7129
|
type: "string",
|
|
7115
7130
|
description: `How many Tasks a flat list reads, newest first (only with --bucketed=false). Default: ${ACTIONS_LIST_DEFAULT_LIMIT}. When the read comes back full, hints[] says so \u2014 raise --limit to reach older Tasks.`,
|
|
7116
7131
|
required: false
|
|
7117
|
-
}
|
|
7132
|
+
},
|
|
7133
|
+
output: { type: "string", description: "Output format: json|md. Default: json", required: false }
|
|
7118
7134
|
}
|
|
7119
7135
|
});
|
|
7120
7136
|
var listCommand2 = defineCommand8({
|
|
@@ -7126,33 +7142,102 @@ var listCommand2 = defineCommand8({
|
|
|
7126
7142
|
bucketed: { type: "boolean", description: "Pre-bucket by chat", required: false, default: true },
|
|
7127
7143
|
status: { type: "string", description: "Status filter (raw mode)", required: false },
|
|
7128
7144
|
sort: { type: "string", description: "Order (raw mode): priority|recent", required: false },
|
|
7129
|
-
limit: { type: "string", description: "How many Tasks to read (raw mode)", required: false }
|
|
7145
|
+
limit: { type: "string", description: "How many Tasks to read (raw mode)", required: false },
|
|
7146
|
+
output: { type: "string", description: "Output format: json|md", required: false }
|
|
7130
7147
|
},
|
|
7131
7148
|
run: async ({ args }) => {
|
|
7132
7149
|
try {
|
|
7133
|
-
const env = getEnv();
|
|
7134
7150
|
const bucketed = args.bucketed !== false;
|
|
7135
|
-
const body = { bucketed };
|
|
7136
|
-
if (bucketed && env.BAKER_CHAT_ID) {
|
|
7137
|
-
body.chatId = env.BAKER_CHAT_ID;
|
|
7138
|
-
}
|
|
7139
7151
|
const parsedLimit = args.limit === void 0 ? Number.NaN : Number.parseInt(String(args.limit), 10);
|
|
7140
7152
|
const limit = Number.isFinite(parsedLimit) && parsedLimit > 0 ? parsedLimit : ACTIONS_LIST_DEFAULT_LIMIT;
|
|
7141
|
-
|
|
7142
|
-
if (args.status) {
|
|
7143
|
-
body.status = args.status;
|
|
7144
|
-
}
|
|
7145
|
-
body.sort = args.sort === "recent" ? "recent" : "priority";
|
|
7146
|
-
body.limit = limit;
|
|
7147
|
-
}
|
|
7153
|
+
const body = buildRequestBody(args, bucketed, limit);
|
|
7148
7154
|
const response = await apiPost("/api/actions/list", body);
|
|
7149
7155
|
const hints = !bucketed && response.ok && Array.isArray(response.data) ? buildListHints({ returned: response.data.length, limit }) : [];
|
|
7150
|
-
|
|
7156
|
+
const envelope = hints.length > 0 ? { ...response, hints } : response;
|
|
7157
|
+
if (args.output === "md" && envelope.ok) {
|
|
7158
|
+
process.stdout.write(renderMarkdown(envelope.data, hints));
|
|
7159
|
+
return;
|
|
7160
|
+
}
|
|
7161
|
+
writeJson(envelope);
|
|
7151
7162
|
} catch (err) {
|
|
7152
7163
|
failApi(err);
|
|
7153
7164
|
}
|
|
7154
7165
|
}
|
|
7155
7166
|
});
|
|
7167
|
+
function buildRequestBody(args, bucketed, limit) {
|
|
7168
|
+
const env = getEnv();
|
|
7169
|
+
const body = { bucketed };
|
|
7170
|
+
if (bucketed) {
|
|
7171
|
+
if (env.BAKER_CHAT_ID) {
|
|
7172
|
+
body.chatId = env.BAKER_CHAT_ID;
|
|
7173
|
+
}
|
|
7174
|
+
return body;
|
|
7175
|
+
}
|
|
7176
|
+
if (args.status) {
|
|
7177
|
+
body.status = args.status;
|
|
7178
|
+
}
|
|
7179
|
+
body.sort = args.sort === "recent" ? "recent" : "priority";
|
|
7180
|
+
body.limit = limit;
|
|
7181
|
+
return body;
|
|
7182
|
+
}
|
|
7183
|
+
function toRow(entry) {
|
|
7184
|
+
const nested = entry.action;
|
|
7185
|
+
const doc = nested && typeof nested === "object" ? nested : entry;
|
|
7186
|
+
const id = doc.id ?? doc._id ?? entry.id ?? entry._id ?? entry.tempId ?? "?";
|
|
7187
|
+
const status = doc.status ?? entry.status ?? entry.draftStatus ?? "";
|
|
7188
|
+
let hint = typeof entry.hint === "string" ? entry.hint.trim() : "";
|
|
7189
|
+
if (!hint && entry.isBlocked === true) {
|
|
7190
|
+
const open = typeof entry.openBlockerCount === "number" ? entry.openBlockerCount : null;
|
|
7191
|
+
hint = open === null ? "blocked" : `blocked by ${open} open Task${open === 1 ? "" : "s"}`;
|
|
7192
|
+
}
|
|
7193
|
+
return {
|
|
7194
|
+
id: String(id),
|
|
7195
|
+
name: String(doc.name ?? entry.name ?? "(unnamed)"),
|
|
7196
|
+
status: String(status),
|
|
7197
|
+
hint
|
|
7198
|
+
};
|
|
7199
|
+
}
|
|
7200
|
+
function renderEntry(entry) {
|
|
7201
|
+
const row = toRow(entry);
|
|
7202
|
+
const suffix = row.status ? ` [${row.status}]` : "";
|
|
7203
|
+
const lines = [`- \`${row.id}\` ${row.name}${suffix}`];
|
|
7204
|
+
if (row.hint) {
|
|
7205
|
+
lines.push(` ${row.hint}`);
|
|
7206
|
+
}
|
|
7207
|
+
return lines;
|
|
7208
|
+
}
|
|
7209
|
+
function renderBuckets(data) {
|
|
7210
|
+
const lines = [];
|
|
7211
|
+
for (const [bucket, entries] of Object.entries(data)) {
|
|
7212
|
+
if (!Array.isArray(entries) || entries.length === 0) {
|
|
7213
|
+
continue;
|
|
7214
|
+
}
|
|
7215
|
+
lines.push(`## ${bucket} (${entries.length})`);
|
|
7216
|
+
for (const entry of entries) {
|
|
7217
|
+
lines.push(...renderEntry(entry));
|
|
7218
|
+
}
|
|
7219
|
+
lines.push("");
|
|
7220
|
+
}
|
|
7221
|
+
return lines;
|
|
7222
|
+
}
|
|
7223
|
+
function renderMarkdown(data, hints) {
|
|
7224
|
+
let lines;
|
|
7225
|
+
if (Array.isArray(data)) {
|
|
7226
|
+
lines = data.flatMap((entry) => renderEntry(entry));
|
|
7227
|
+
} else if (data && typeof data === "object") {
|
|
7228
|
+
lines = renderBuckets(data);
|
|
7229
|
+
} else {
|
|
7230
|
+
lines = [];
|
|
7231
|
+
}
|
|
7232
|
+
if (lines.length === 0) {
|
|
7233
|
+
return "No Tasks matched.\n";
|
|
7234
|
+
}
|
|
7235
|
+
for (const hint of hints) {
|
|
7236
|
+
lines.push(`> ${hint}`);
|
|
7237
|
+
}
|
|
7238
|
+
return `${lines.join("\n").trimEnd()}
|
|
7239
|
+
`;
|
|
7240
|
+
}
|
|
7156
7241
|
|
|
7157
7242
|
// src/commands/actions/log.ts
|
|
7158
7243
|
import { defineCommand as defineCommand9 } from "citty";
|
|
@@ -14278,7 +14363,7 @@ function sortFindings(findings) {
|
|
|
14278
14363
|
return SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity];
|
|
14279
14364
|
});
|
|
14280
14365
|
}
|
|
14281
|
-
function
|
|
14366
|
+
function renderMarkdown2(result) {
|
|
14282
14367
|
const lines = [];
|
|
14283
14368
|
lines.push(`# LinkedIn Ads Audit \u2014 ${result.account.name} (${result.account.id})`);
|
|
14284
14369
|
lines.push("");
|
|
@@ -14363,7 +14448,7 @@ Examples:
|
|
|
14363
14448
|
const result = { ...data, findings: sorted };
|
|
14364
14449
|
const fmt = args.format ?? "json";
|
|
14365
14450
|
if (fmt === "md") {
|
|
14366
|
-
process.stdout.write(`${
|
|
14451
|
+
process.stdout.write(`${renderMarkdown2(result)}
|
|
14367
14452
|
`);
|
|
14368
14453
|
return;
|
|
14369
14454
|
}
|
|
@@ -32200,7 +32285,7 @@ function outputRows(rows, args, response, cached) {
|
|
|
32200
32285
|
}
|
|
32201
32286
|
writeJsonEnvelope({ ...response, ...cached && { cached: true } });
|
|
32202
32287
|
}
|
|
32203
|
-
function
|
|
32288
|
+
function buildRequestBody2(args, propertyId, useCache) {
|
|
32204
32289
|
const body = { propertyId };
|
|
32205
32290
|
if (args.preset) body.preset = args.preset;
|
|
32206
32291
|
if (args.dimensions) body.dimensions = args.dimensions.split(",").map((d) => d.trim());
|
|
@@ -32275,7 +32360,7 @@ Free-form (escape hatch):
|
|
|
32275
32360
|
}
|
|
32276
32361
|
const propertyId = await resolvePropertyId(args);
|
|
32277
32362
|
const useCache = !args["no-cache"];
|
|
32278
|
-
const body =
|
|
32363
|
+
const body = buildRequestBody2(args, propertyId, useCache);
|
|
32279
32364
|
const cacheKey = buildQueryCacheKey(propertyId, JSON.stringify(body));
|
|
32280
32365
|
if (useCache) {
|
|
32281
32366
|
const cached = cacheGet("ga4-queries", cacheKey);
|
|
@@ -32871,7 +32956,7 @@ function outputRows2(rows, args, response, cached) {
|
|
|
32871
32956
|
}
|
|
32872
32957
|
writeJsonEnvelope({ ...response, ...cached && { cached: true } });
|
|
32873
32958
|
}
|
|
32874
|
-
function
|
|
32959
|
+
function buildRequestBody3(args, siteUrl, useCache) {
|
|
32875
32960
|
const body = { siteUrl };
|
|
32876
32961
|
if (args.preset) body.preset = args.preset;
|
|
32877
32962
|
if (args.brand) body.brand = args.brand;
|
|
@@ -32955,7 +33040,7 @@ Free-form (escape hatch):
|
|
|
32955
33040
|
}
|
|
32956
33041
|
const siteUrl = await resolveSiteUrl(args);
|
|
32957
33042
|
const useCache = !args["no-cache"];
|
|
32958
|
-
const body =
|
|
33043
|
+
const body = buildRequestBody3(args, siteUrl, useCache);
|
|
32959
33044
|
const cacheKey = buildQueryCacheKey(siteUrl, JSON.stringify(body));
|
|
32960
33045
|
if (useCache) {
|
|
32961
33046
|
const cached = cacheGet("gsc-queries", cacheKey);
|
|
@@ -34121,7 +34206,13 @@ registerSchema({
|
|
|
34121
34206
|
command: "images.dimensions",
|
|
34122
34207
|
description: "Return width, height, aspect ratio, and format for a local file or remote URL.",
|
|
34123
34208
|
args: {
|
|
34124
|
-
target: { type: "string", description: "Local file path or remote http(s) URL", required: true }
|
|
34209
|
+
target: { type: "string", description: "Local file path or remote http(s) URL", required: true },
|
|
34210
|
+
output: { type: "string", description: "Output format: json|md. Default: json", required: false },
|
|
34211
|
+
fields: {
|
|
34212
|
+
type: "string",
|
|
34213
|
+
description: "Comma-separated field names to include (e.g. width,height)",
|
|
34214
|
+
required: false
|
|
34215
|
+
}
|
|
34125
34216
|
}
|
|
34126
34217
|
});
|
|
34127
34218
|
var dimensionsCommand = defineCommand130({
|
|
@@ -34130,7 +34221,13 @@ var dimensionsCommand = defineCommand130({
|
|
|
34130
34221
|
description: "Read image dimensions without decoding the full file.\n\nExample: baker images dimensions ./logo.png\nExample: baker images dimensions https://acme.com/hero.png"
|
|
34131
34222
|
},
|
|
34132
34223
|
args: {
|
|
34133
|
-
target: { type: "positional", description: "Local file path or remote http(s) URL", required: false }
|
|
34224
|
+
target: { type: "positional", description: "Local file path or remote http(s) URL", required: false },
|
|
34225
|
+
output: { type: "string", description: "Output format: json|md", required: false },
|
|
34226
|
+
// --fields is the flag an agent reaches for when it wants two numbers out of
|
|
34227
|
+
// six. It used to be accepted silently and ignored (citty drops unknown
|
|
34228
|
+
// flags), so the caller assumed the projection had failed and fell back to
|
|
34229
|
+
// regexing raw JSON. Accepting it is cheaper than explaining it.
|
|
34230
|
+
fields: { type: "string", description: "Comma-separated field names to include", required: false }
|
|
34134
34231
|
},
|
|
34135
34232
|
run: async ({ args }) => {
|
|
34136
34233
|
try {
|
|
@@ -34148,17 +34245,23 @@ var dimensionsCommand = defineCommand130({
|
|
|
34148
34245
|
});
|
|
34149
34246
|
process.exit(1);
|
|
34150
34247
|
}
|
|
34151
|
-
|
|
34152
|
-
|
|
34153
|
-
|
|
34154
|
-
|
|
34155
|
-
|
|
34156
|
-
|
|
34157
|
-
|
|
34158
|
-
|
|
34159
|
-
|
|
34160
|
-
|
|
34161
|
-
|
|
34248
|
+
writeOutput(
|
|
34249
|
+
{
|
|
34250
|
+
ok: true,
|
|
34251
|
+
data: {
|
|
34252
|
+
target,
|
|
34253
|
+
source: isRemoteUrl(target) ? "url" : "file",
|
|
34254
|
+
width: dims.width,
|
|
34255
|
+
height: dims.height,
|
|
34256
|
+
aspectRatio: Math.round(dims.aspectRatio * 1e4) / 1e4,
|
|
34257
|
+
format: dims.format
|
|
34258
|
+
}
|
|
34259
|
+
},
|
|
34260
|
+
args.output || "json",
|
|
34261
|
+
args.fields ? args.fields.split(",") : void 0,
|
|
34262
|
+
false,
|
|
34263
|
+
(record) => record
|
|
34264
|
+
);
|
|
34162
34265
|
} catch (err) {
|
|
34163
34266
|
const message = err instanceof Error ? err.message : "Unexpected error";
|
|
34164
34267
|
writeJson({ ok: false, error: { code: "IMAGE_PROCESSING_ERROR", message } });
|
|
@@ -35216,12 +35319,13 @@ var iconCommand = defineCommand138({
|
|
|
35216
35319
|
|
|
35217
35320
|
// src/commands/images/ingest.ts
|
|
35218
35321
|
import { defineCommand as defineCommand139 } from "citty";
|
|
35322
|
+
var SOURCE_VALUES = imageSourceSchema.options.join(" | ");
|
|
35219
35323
|
registerSchema({
|
|
35220
35324
|
command: "images.ingest",
|
|
35221
35325
|
description: "Ingest a remote image URL into the library (full describe + embed).",
|
|
35222
35326
|
args: {
|
|
35223
35327
|
url: { type: "string", description: "Image URL to ingest", required: true },
|
|
35224
|
-
source: { type: "string", description:
|
|
35328
|
+
source: { type: "string", description: `Where the image came from. One of: ${SOURCE_VALUES}`, required: true },
|
|
35225
35329
|
"external-id": { type: "string", description: "Provider asset id", required: false },
|
|
35226
35330
|
"external-url": { type: "string", description: "Canonical page URL", required: false },
|
|
35227
35331
|
context: { type: "string", description: "Description context hint", required: false },
|
|
@@ -35236,7 +35340,7 @@ var ingestCommand = defineCommand139({
|
|
|
35236
35340
|
},
|
|
35237
35341
|
args: {
|
|
35238
35342
|
url: { type: "positional", description: "Image URL", required: false },
|
|
35239
|
-
source: { type: "string", description:
|
|
35343
|
+
source: { type: "string", description: `Where the image came from. One of: ${SOURCE_VALUES}`, required: false },
|
|
35240
35344
|
"external-id": { type: "string", description: "Provider asset id", required: false },
|
|
35241
35345
|
"external-url": { type: "string", description: "Canonical page URL", required: false },
|
|
35242
35346
|
context: { type: "string", description: "Description context", required: false },
|
|
@@ -35252,10 +35356,29 @@ var ingestCommand = defineCommand139({
|
|
|
35252
35356
|
process.exit(1);
|
|
35253
35357
|
}
|
|
35254
35358
|
if (!source) {
|
|
35255
|
-
writeJson({
|
|
35359
|
+
writeJson({
|
|
35360
|
+
ok: false,
|
|
35361
|
+
error: {
|
|
35362
|
+
code: "VALIDATION_ERROR",
|
|
35363
|
+
message: "--source is required",
|
|
35364
|
+
fix: `Pass one of: ${SOURCE_VALUES}. Use "website" for an asset pulled off a company's own site.`
|
|
35365
|
+
}
|
|
35366
|
+
});
|
|
35367
|
+
process.exit(1);
|
|
35368
|
+
}
|
|
35369
|
+
const parsedSource = imageSourceSchema.safeParse(source);
|
|
35370
|
+
if (!parsedSource.success) {
|
|
35371
|
+
writeJson({
|
|
35372
|
+
ok: false,
|
|
35373
|
+
error: {
|
|
35374
|
+
code: "VALIDATION_ERROR",
|
|
35375
|
+
message: `--source "${source}" is not a known image source`,
|
|
35376
|
+
fix: `Pass one of: ${SOURCE_VALUES}. Use "website" for an asset pulled off a company's own site.`
|
|
35377
|
+
}
|
|
35378
|
+
});
|
|
35256
35379
|
process.exit(1);
|
|
35257
35380
|
}
|
|
35258
|
-
const body = { url, source };
|
|
35381
|
+
const body = { url, source: parsedSource.data };
|
|
35259
35382
|
if (args["external-id"]) body.externalId = args["external-id"];
|
|
35260
35383
|
if (args["external-url"]) body.externalUrl = args["external-url"];
|
|
35261
35384
|
if (args.context) body.descriptionContext = args.context;
|