@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/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: z.string(),
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": { type: "string", description: "Custom tempId (auto-generated if omitted)", required: false }
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 = args["temp-id"] || generateTempId();
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
- if (!bucketed) {
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
- writeJson(hints.length > 0 ? { ...response, hints } : response);
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 renderMarkdown(result) {
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(`${renderMarkdown(result)}
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 buildRequestBody(args, propertyId, useCache) {
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 = buildRequestBody(args, propertyId, useCache);
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 buildRequestBody2(args, siteUrl, useCache) {
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 = buildRequestBody2(args, siteUrl, useCache);
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
- writeJson({
34152
- ok: true,
34153
- data: {
34154
- target,
34155
- source: isRemoteUrl(target) ? "url" : "file",
34156
- width: dims.width,
34157
- height: dims.height,
34158
- aspectRatio: Math.round(dims.aspectRatio * 1e4) / 1e4,
34159
- format: dims.format
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: "Image source enum", required: true },
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: "Source enum", required: false },
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({ ok: false, error: { code: "VALIDATION_ERROR", message: "--source is required" } });
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;