@koda-sl/baker-cli 0.232.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,643 +565,155 @@ 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
- );
568
+ // ../api/src/actions.ts
569
+ import { z } from "zod";
570
+ var actionStatusSchema = z.enum(["pending", "in_progress", "completed", "discarded"]);
571
+ var actionPrioritySchema = z.enum(["urgent", "high", "medium", "low"]);
572
+ var actionRefResolutionStatusSchema = z.union([actionStatusSchema, z.enum(["draft", "not_found"])]);
573
+ var actionDocSchema = z.object({
574
+ _id: z.string(),
575
+ _creationTime: z.number(),
576
+ companyId: z.string(),
577
+ name: z.string(),
578
+ description: z.string(),
579
+ status: actionStatusSchema,
580
+ priority: actionPrioritySchema.optional(),
581
+ requiresHuman: z.boolean().optional(),
582
+ tags: z.array(z.string()).optional(),
583
+ createdByType: z.enum(["user", "chat", "schedule"]),
584
+ createdByUserId: z.string().optional(),
585
+ createdByChatId: z.string().optional(),
586
+ createdByScheduleId: z.string().optional(),
587
+ createdFromTempId: z.string().optional(),
588
+ scheduledActionTrigger: z.enum(["scheduled", "manual"]).optional(),
589
+ scheduledActionTriggerChatId: z.string().optional(),
590
+ scheduledActionTriggerActorType: z.enum(["user", "api", "system"]).optional(),
591
+ scheduledActionTriggerActorUserId: z.string().optional(),
592
+ assigneeUserId: z.string().optional(),
593
+ activeChatId: z.string().optional(),
594
+ activeClaimedAt: z.number().optional(),
595
+ completedAt: z.number().optional(),
596
+ completedByType: z.enum(["user", "chat"]).optional(),
597
+ completedByChatId: z.string().optional(),
598
+ completedNote: z.string().optional(),
599
+ discardedAt: z.number().optional(),
600
+ discardedReason: z.string().optional(),
601
+ createdAt: z.number(),
602
+ updatedAt: z.number(),
603
+ searchText: z.string().optional()
604
+ });
605
+ var actionAssigneeSchema = z.object({
606
+ userId: z.string(),
607
+ name: z.string(),
608
+ image: z.string().optional()
609
+ });
610
+ var actionDepRefSchema = z.object({
611
+ id: z.string(),
612
+ name: z.string(),
613
+ status: actionStatusSchema
614
+ });
615
+ var actionWithMetaSchema = z.object({
616
+ action: actionDocSchema,
617
+ blockerCount: z.number(),
618
+ openBlockerCount: z.number(),
619
+ isBlocked: z.boolean(),
620
+ openBlockingCount: z.number(),
621
+ blockers: z.array(actionDepRefSchema),
622
+ blocking: z.array(actionDepRefSchema),
623
+ assignee: actionAssigneeSchema.nullable()
624
+ });
625
+ var actionBucketEntrySchema = z.object({
626
+ id: z.string(),
627
+ name: z.string(),
628
+ description: z.string(),
629
+ status: actionStatusSchema,
630
+ hint: z.string(),
631
+ blockedBy: z.array(z.object({ id: z.string(), name: z.string() })).optional(),
632
+ claimedByChat: z.object({ id: z.string(), title: z.string() }).optional(),
633
+ draftStatus: z.enum(["in-progress", "completing", "discarding", "updating"]).optional()
634
+ });
635
+ var draftActionEntrySchema = z.object({
636
+ tempId: z.string(),
637
+ name: z.string(),
638
+ description: z.string(),
639
+ tags: z.array(z.string()),
640
+ hint: z.string(),
641
+ draftStatus: z.literal("creating")
642
+ });
643
+ var actionBucketsSchema = z.object({
644
+ claimable: z.array(actionBucketEntrySchema),
645
+ myClaims: z.array(actionBucketEntrySchema),
646
+ blocked: z.array(actionBucketEntrySchema),
647
+ claimedByOthers: z.array(actionBucketEntrySchema),
648
+ completed: z.array(actionBucketEntrySchema),
649
+ discarded: z.array(actionBucketEntrySchema),
650
+ draftCreates: z.array(draftActionEntrySchema)
651
+ });
652
+ var actionRefStatusResultSchema = z.object({
653
+ ref: z.string(),
654
+ status: actionRefResolutionStatusSchema,
655
+ actionId: z.string().optional(),
656
+ name: z.string().optional(),
657
+ hint: z.string().optional()
658
+ });
659
+ var actionDetailSchema = z.object({
660
+ action: actionDocSchema,
661
+ blockers: z.array(actionDocSchema),
662
+ blocked: z.array(actionDocSchema),
663
+ activeChat: z.object({ _id: z.string(), title: z.string() }).nullable()
664
+ });
665
+ var actionDraftOpKindSchema = z.enum(["create", "update", "complete", "discard", "link", "unlink"]);
666
+ var chatActionDraftOpViewSchema = z.object({
667
+ kind: actionDraftOpKindSchema,
668
+ targetName: z.string(),
669
+ targetActionId: z.string().optional(),
670
+ tempId: z.string().optional(),
671
+ tags: z.array(z.string()).optional(),
672
+ reason: z.string().optional(),
673
+ note: z.string().optional(),
674
+ name: z.string().optional(),
675
+ description: z.string().optional(),
676
+ blockerName: z.string().optional(),
677
+ blockerActionId: z.string().optional(),
678
+ blockerTempId: z.string().optional()
679
+ });
680
+ var chatDraftViewSchema = z.object({
681
+ status: z.enum(["active", "publishing", "discarded", "applied", "none"]),
682
+ count: z.number(),
683
+ ops: z.array(chatActionDraftOpViewSchema),
684
+ warnings: z.array(z.object({ tempId: z.string(), name: z.string() }))
685
+ });
686
+ var actionsClaimDataSchema = z.object({
687
+ id: z.string(),
688
+ name: z.string(),
689
+ description: z.string(),
690
+ tags: z.array(z.string())
691
+ });
692
+ var skillRecommendationSchema = z.object({
693
+ name: z.string(),
694
+ reason: z.string()
695
+ });
696
+ var removeDraftOpTargetSchema = z.discriminatedUnion("kind", [
697
+ z.object({ kind: z.literal("tempId"), tempId: z.string() }),
698
+ z.object({
699
+ kind: z.literal("actionId"),
700
+ actionId: z.string(),
701
+ opKind: z.enum(["update", "complete", "discard"])
702
+ })
703
+ ]);
704
+ function okResponse(data) {
705
+ return z.object({ ok: z.literal(true), data });
669
706
  }
670
- function looksScheduled(name, description) {
671
- const haystack = fold(`${name}
672
- ${description}`);
673
- return SCHEDULE_SIGNALS.some((re2) => re2.test(haystack));
707
+ function okOnlySchema() {
708
+ return z.object({ ok: z.literal(true) });
674
709
  }
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
- // ../api/src/actions.ts
1061
- import { z } from "zod";
1062
- var actionStatusSchema = z.enum(["pending", "in_progress", "completed", "discarded"]);
1063
- var actionPrioritySchema = z.enum(["urgent", "high", "medium", "low"]);
1064
- var actionRefResolutionStatusSchema = z.union([actionStatusSchema, z.enum(["draft", "not_found"])]);
1065
- var actionDocSchema = z.object({
1066
- _id: z.string(),
1067
- _creationTime: z.number(),
1068
- companyId: z.string(),
1069
- name: z.string(),
1070
- description: z.string(),
1071
- status: actionStatusSchema,
1072
- priority: actionPrioritySchema.optional(),
1073
- requiresHuman: z.boolean().optional(),
1074
- tags: z.array(z.string()).optional(),
1075
- createdByType: z.enum(["user", "chat", "schedule"]),
1076
- createdByUserId: z.string().optional(),
1077
- createdByChatId: z.string().optional(),
1078
- createdByScheduleId: z.string().optional(),
1079
- createdFromTempId: z.string().optional(),
1080
- scheduledActionTrigger: z.enum(["scheduled", "manual"]).optional(),
1081
- scheduledActionTriggerChatId: z.string().optional(),
1082
- scheduledActionTriggerActorType: z.enum(["user", "api", "system"]).optional(),
1083
- scheduledActionTriggerActorUserId: z.string().optional(),
1084
- assigneeUserId: z.string().optional(),
1085
- activeChatId: z.string().optional(),
1086
- activeClaimedAt: z.number().optional(),
1087
- completedAt: z.number().optional(),
1088
- completedByType: z.enum(["user", "chat"]).optional(),
1089
- completedByChatId: z.string().optional(),
1090
- completedNote: z.string().optional(),
1091
- discardedAt: z.number().optional(),
1092
- discardedReason: z.string().optional(),
1093
- createdAt: z.number(),
1094
- updatedAt: z.number(),
1095
- searchText: z.string().optional()
1096
- });
1097
- var actionAssigneeSchema = z.object({
1098
- userId: z.string(),
1099
- name: z.string(),
1100
- image: z.string().optional()
1101
- });
1102
- var actionDepRefSchema = z.object({
1103
- id: z.string(),
1104
- name: z.string(),
1105
- status: actionStatusSchema
1106
- });
1107
- var actionWithMetaSchema = z.object({
1108
- action: actionDocSchema,
1109
- blockerCount: z.number(),
1110
- openBlockerCount: z.number(),
1111
- isBlocked: z.boolean(),
1112
- openBlockingCount: z.number(),
1113
- blockers: z.array(actionDepRefSchema),
1114
- blocking: z.array(actionDepRefSchema),
1115
- assignee: actionAssigneeSchema.nullable()
1116
- });
1117
- var actionBucketEntrySchema = z.object({
1118
- id: z.string(),
1119
- name: z.string(),
1120
- description: z.string(),
1121
- status: actionStatusSchema,
1122
- hint: z.string(),
1123
- blockedBy: z.array(z.object({ id: z.string(), name: z.string() })).optional(),
1124
- claimedByChat: z.object({ id: z.string(), title: z.string() }).optional(),
1125
- draftStatus: z.enum(["in-progress", "completing", "discarding", "updating"]).optional()
1126
- });
1127
- var draftActionEntrySchema = z.object({
1128
- tempId: z.string(),
1129
- name: z.string(),
1130
- description: z.string(),
1131
- tags: z.array(z.string()),
1132
- hint: z.string(),
1133
- draftStatus: z.literal("creating")
1134
- });
1135
- var actionBucketsSchema = z.object({
1136
- claimable: z.array(actionBucketEntrySchema),
1137
- myClaims: z.array(actionBucketEntrySchema),
1138
- blocked: z.array(actionBucketEntrySchema),
1139
- claimedByOthers: z.array(actionBucketEntrySchema),
1140
- completed: z.array(actionBucketEntrySchema),
1141
- discarded: z.array(actionBucketEntrySchema),
1142
- draftCreates: z.array(draftActionEntrySchema)
1143
- });
1144
- var actionRefStatusResultSchema = z.object({
1145
- ref: z.string(),
1146
- status: actionRefResolutionStatusSchema,
1147
- actionId: z.string().optional(),
1148
- name: z.string().optional(),
1149
- hint: z.string().optional()
1150
- });
1151
- var actionDetailSchema = z.object({
1152
- action: actionDocSchema,
1153
- blockers: z.array(actionDocSchema),
1154
- blocked: z.array(actionDocSchema),
1155
- activeChat: z.object({ _id: z.string(), title: z.string() }).nullable()
1156
- });
1157
- var actionDraftOpKindSchema = z.enum(["create", "update", "complete", "discard", "link", "unlink"]);
1158
- var chatActionDraftOpViewSchema = z.object({
1159
- kind: actionDraftOpKindSchema,
1160
- targetName: z.string(),
1161
- targetActionId: z.string().optional(),
1162
- tempId: z.string().optional(),
1163
- tags: z.array(z.string()).optional(),
1164
- reason: z.string().optional(),
1165
- note: z.string().optional(),
1166
- name: z.string().optional(),
1167
- description: z.string().optional(),
1168
- blockerName: z.string().optional(),
1169
- blockerActionId: z.string().optional(),
1170
- blockerTempId: z.string().optional()
1171
- });
1172
- var chatDraftViewSchema = z.object({
1173
- status: z.enum(["active", "publishing", "discarded", "applied", "none"]),
1174
- count: z.number(),
1175
- ops: z.array(chatActionDraftOpViewSchema),
1176
- warnings: z.array(z.object({ tempId: z.string(), name: z.string() }))
1177
- });
1178
- var actionsClaimDataSchema = z.object({
1179
- id: z.string(),
1180
- name: z.string(),
1181
- description: z.string(),
1182
- tags: z.array(z.string())
1183
- });
1184
- var skillRecommendationSchema = z.object({
1185
- name: z.string(),
1186
- reason: z.string()
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."
1187
713
  });
1188
- var removeDraftOpTargetSchema = z.discriminatedUnion("kind", [
1189
- z.object({ kind: z.literal("tempId"), tempId: z.string() }),
1190
- z.object({
1191
- kind: z.literal("actionId"),
1192
- actionId: z.string(),
1193
- opKind: z.enum(["update", "complete", "discard"])
1194
- })
1195
- ]);
1196
- function okResponse(data) {
1197
- return z.object({ ok: z.literal(true), data });
1198
- }
1199
- function okOnlySchema() {
1200
- return z.object({ ok: z.literal(true) });
1201
- }
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(),
@@ -6407,263 +5919,755 @@ var tagsDraftListResponseSchema = z19.object({
6407
5919
  status: z19.enum(["active", "publishing", "applied", "discarded", "none"]),
6408
5920
  ops: z19.array(tagDraftOpViewSchema)
6409
5921
  });
6410
- var tagInputRequestSchema = z19.object({
6411
- // Every tag change is a tab in the approval form: create/edit show the full
6412
- // body; delete shows a confirm. No tag change bypasses this approval.
6413
- mode: z19.enum(["create", "edit", "delete"]),
6414
- tagType: tagTypeSchema,
6415
- /** Edit/delete mode — the real tag id or `tag_temp_*` ref being changed. */
6416
- ref: z19.string().optional(),
6417
- /** Non-secret values the agent proposes to prefill. Secret keys are stripped at every boundary. */
6418
- prefilledConfig: z19.record(z19.string(), z19.string()).optional(),
6419
- /** Secret field names the agent asks the user to provide. */
6420
- requestedSecretFields: z19.array(z19.string()).optional(),
6421
- /** Short message shown above the form explaining why the input is needed. */
6422
- message: z19.string().optional()
5922
+ var tagInputRequestSchema = z19.object({
5923
+ // Every tag change is a tab in the approval form: create/edit show the full
5924
+ // body; delete shows a confirm. No tag change bypasses this approval.
5925
+ mode: z19.enum(["create", "edit", "delete"]),
5926
+ tagType: tagTypeSchema,
5927
+ /** Edit/delete mode — the real tag id or `tag_temp_*` ref being changed. */
5928
+ ref: z19.string().optional(),
5929
+ /** Non-secret values the agent proposes to prefill. Secret keys are stripped at every boundary. */
5930
+ prefilledConfig: z19.record(z19.string(), z19.string()).optional(),
5931
+ /** Secret field names the agent asks the user to provide. */
5932
+ requestedSecretFields: z19.array(z19.string()).optional(),
5933
+ /** Short message shown above the form explaining why the input is needed. */
5934
+ message: z19.string().optional()
5935
+ });
5936
+ var tagChangeToolInputSchema = z19.object({
5937
+ changes: z19.array(tagInputRequestSchema).min(1).max(8)
5938
+ });
5939
+ var tagInputResultSchema = z19.discriminatedUnion("status", [
5940
+ z19.object({
5941
+ status: z19.literal("submitted"),
5942
+ ref: z19.string(),
5943
+ type: tagTypeSchema,
5944
+ /** Identifying field name → value (non-secret), when the type has one. */
5945
+ identifier: z19.record(z19.string(), z19.string()).optional(),
5946
+ secretFieldsSet: z19.array(z19.string()),
5947
+ note: z19.string().optional()
5948
+ }),
5949
+ z19.object({
5950
+ status: z19.literal("declined"),
5951
+ reason: z19.string().optional(),
5952
+ /** Set when the platform could not apply an approved change, rather than the user skipping it. */
5953
+ failed: z19.boolean().optional()
5954
+ })
5955
+ ]);
5956
+ var tagChangeToolResultSchema = z19.object({
5957
+ results: z19.array(tagInputResultSchema)
5958
+ });
5959
+
5960
+ // ../api/src/testimonials.ts
5961
+ import { z as z20 } from "zod";
5962
+ var testimonialSourceTypeSchema = z20.enum(["google", "trustpilot"]);
5963
+ var testimonialStatusSchema = z20.enum(["pending", "processing", "ready", "error"]);
5964
+ var testimonialSentimentSchema = z20.enum(["positive", "neutral", "negative"]);
5965
+ var testimonialDocSchema = z20.object({
5966
+ _id: z20.string(),
5967
+ _creationTime: z20.number(),
5968
+ companyId: z20.string(),
5969
+ sourceId: z20.string(),
5970
+ sourceType: testimonialSourceTypeSchema,
5971
+ reviewText: z20.string(),
5972
+ reviewTitle: z20.string().optional(),
5973
+ searchText: z20.string().optional(),
5974
+ reviewerName: z20.string().optional(),
5975
+ reviewerImageUrl: z20.string().optional(),
5976
+ reviewerImageId: z20.string().optional(),
5977
+ reviewerLocation: z20.string().optional(),
5978
+ rating: z20.number().optional(),
5979
+ reviewDate: z20.number().optional(),
5980
+ ownerAnswer: z20.string().optional(),
5981
+ mediaUrls: z20.array(z20.string()).optional(),
5982
+ imageIds: z20.array(z20.string()).optional(),
5983
+ videoIds: z20.array(z20.string()).optional(),
5984
+ sourceUrl: z20.string().optional(),
5985
+ rawData: z20.unknown().optional(),
5986
+ tags: z20.array(z20.string()),
5987
+ highlight: z20.string().optional(),
5988
+ language: z20.string().optional(),
5989
+ summary: z20.string().optional(),
5990
+ sentiment: testimonialSentimentSchema.optional(),
5991
+ textEmbedding: z20.array(z20.number()).optional(),
5992
+ externalId: z20.string().optional(),
5993
+ contentHash: z20.string().optional(),
5994
+ status: testimonialStatusSchema,
5995
+ errorMessage: z20.string().optional(),
5996
+ createdAt: z20.number(),
5997
+ updatedAt: z20.number()
5998
+ });
5999
+ var testimonialsListRequestSchema = z20.object({
6000
+ source: testimonialSourceTypeSchema.optional(),
6001
+ rating_min: z20.coerce.number().int().min(1).max(5).optional(),
6002
+ rating_max: z20.coerce.number().int().min(1).max(5).optional(),
6003
+ tags: z20.string().transform((s) => s.split(",").filter(Boolean)).optional(),
6004
+ status: testimonialStatusSchema.optional(),
6005
+ sentiment: testimonialSentimentSchema.optional(),
6006
+ language: z20.string().min(2).max(5).optional(),
6007
+ limit: z20.coerce.number().int().positive().max(200).optional()
6008
+ });
6009
+ var testimonialsListResponseSchema = z20.array(testimonialDocSchema);
6010
+ var testimonialsGetRequestSchema = z20.object({ id: z20.string().min(1, "Missing id parameter") });
6011
+ var testimonialsSearchRequestSchema = z20.object({
6012
+ query: z20.string().min(1),
6013
+ limit: z20.coerce.number().int().positive().max(100).optional(),
6014
+ source: testimonialSourceTypeSchema.optional(),
6015
+ rating_min: z20.coerce.number().int().min(1).max(5).optional(),
6016
+ rating_max: z20.coerce.number().int().min(1).max(5).optional(),
6017
+ tags: z20.array(z20.string()).optional(),
6018
+ status: testimonialStatusSchema.optional(),
6019
+ sentiment: testimonialSentimentSchema.optional(),
6020
+ language: z20.string().min(2).max(5).optional()
6021
+ }).refine(
6022
+ (data) => data.rating_min === void 0 || data.rating_max === void 0 || data.rating_min <= data.rating_max,
6023
+ { message: "rating_min must be less than or equal to rating_max" }
6024
+ );
6025
+ var testimonialsSearchResponseSchema = z20.array(testimonialDocSchema);
6026
+ var testimonialsOutscraperWebhookResponseSchema = z20.object({
6027
+ ok: z20.literal(true),
6028
+ note: z20.string().optional()
6029
+ });
6030
+
6031
+ // ../api/src/videos.ts
6032
+ import { z as z21 } from "zod";
6033
+ var videoStatusSchema = z21.enum(["uploading", "uploaded", "processing", "ready", "error"]);
6034
+ var videoSourceSchema = z21.enum([
6035
+ "uploaded",
6036
+ "url",
6037
+ "bilibili",
6038
+ "bluesky",
6039
+ "dailymotion",
6040
+ "facebook",
6041
+ "instagram",
6042
+ "loom",
6043
+ "newgrounds",
6044
+ "ok",
6045
+ "pinterest",
6046
+ "reddit",
6047
+ "rutube",
6048
+ "snapchat",
6049
+ "streamable",
6050
+ "tiktok",
6051
+ "tumblr",
6052
+ "twitch",
6053
+ "twitter",
6054
+ "vimeo",
6055
+ "vk",
6056
+ "youtube"
6057
+ ]);
6058
+ var videoTranscriptSegmentSchema = z21.object({
6059
+ text: z21.string(),
6060
+ startSecond: z21.number(),
6061
+ endSecond: z21.number()
6062
+ });
6063
+ var videoSceneSchema = z21.object({
6064
+ title: z21.string(),
6065
+ description: z21.string(),
6066
+ startSecond: z21.number(),
6067
+ endSecond: z21.number(),
6068
+ thumbnailTime: z21.number(),
6069
+ // Optional because rows analysed before the richer beat breakdown shipped
6070
+ // carry only the five fields above.
6071
+ shot: z21.string().optional(),
6072
+ visualChange: z21.string().optional(),
6073
+ onScreenText: z21.array(z21.string()).optional(),
6074
+ hasOverlay: z21.boolean().optional(),
6075
+ onCamera: z21.array(z21.string()).optional(),
6076
+ spoken: z21.string().optional()
6077
+ });
6078
+ var videoAnalysisSchema = z21.object({
6079
+ subject: z21.string(),
6080
+ people: z21.array(mediaPersonSchema),
6081
+ brandOwnership: z21.string(),
6082
+ spokenLanguages: z21.array(z21.string()),
6083
+ onScreenText: z21.array(z21.string()),
6084
+ shotStyle: z21.string(),
6085
+ audioStyle: z21.string(),
6086
+ hook: z21.string(),
6087
+ usageNotes: z21.string()
6088
+ });
6089
+ var videoDocSchema = z21.object({
6090
+ _id: z21.string(),
6091
+ _creationTime: z21.number(),
6092
+ companyId: z21.string(),
6093
+ muxAssetId: z21.string(),
6094
+ muxPlaybackId: z21.string(),
6095
+ muxUploadId: z21.string(),
6096
+ name: z21.string(),
6097
+ description: z21.string(),
6098
+ tags: z21.array(z21.string()),
6099
+ source: z21.string(),
6100
+ externalId: z21.string().optional(),
6101
+ externalUrl: z21.string().optional(),
6102
+ sourceId: z21.string().optional(),
6103
+ originalFilename: z21.string().optional(),
6104
+ descriptionContext: z21.string().optional(),
6105
+ analysis: videoAnalysisSchema.optional(),
6106
+ width: z21.number().optional(),
6107
+ height: z21.number().optional(),
6108
+ aspectRatio: z21.number().optional(),
6109
+ duration: z21.number().optional(),
6110
+ transcript: z21.string().optional(),
6111
+ transcriptSegments: z21.array(videoTranscriptSegmentSchema).optional(),
6112
+ scenes: z21.array(videoSceneSchema).optional(),
6113
+ descriptionEmbedding: z21.array(z21.number()).optional(),
6114
+ searchText: z21.string().optional(),
6115
+ status: videoStatusSchema,
6116
+ errorMessage: z21.string().optional(),
6117
+ createdAt: z21.number(),
6118
+ updatedAt: z21.number(),
6119
+ thumbnailUrl: z21.string()
6120
+ }).extend(assetGroupFieldsSchema.shape);
6121
+ var videosWebhookResponseSchema = z21.object({ ok: z21.literal(true) });
6122
+ var videosGetRequestSchema = z21.object({ id: z21.string().min(1, "Missing id parameter") });
6123
+ var videosGetResponseSchema = videoDocSchema.omit({ descriptionEmbedding: true, searchText: true });
6124
+ var videosSearchRequestSchema = z21.object({
6125
+ query: z21.string().min(1),
6126
+ limit: z21.coerce.number().int().positive().max(100).optional(),
6127
+ tags: z21.array(z21.string()).optional()
6128
+ });
6129
+ var videoSearchResultSchema = z21.object({
6130
+ _id: z21.string(),
6131
+ thumbnailUrl: z21.string(),
6132
+ name: z21.string(),
6133
+ description: z21.string(),
6134
+ tags: z21.array(z21.string()),
6135
+ status: z21.string(),
6136
+ duration: z21.number().optional(),
6137
+ muxPlaybackId: z21.string(),
6138
+ createdAt: z21.number(),
6139
+ // The agent picks a clip from this projection alone, so the facts that decide
6140
+ // a pick travel with it: how many beats, whether it is transcribed, and who
6141
+ // is in it.
6142
+ sceneCount: z21.number(),
6143
+ hasTranscript: z21.boolean(),
6144
+ analysis: videoAnalysisSchema.optional(),
6145
+ // Relevance, on the same footing as `imageSearchResultSchema.score`: Cohere's
6146
+ // rerank score when the rerank ran, the RRF fusion score when it did not. A
6147
+ // clip hit used to arrive with no measure of how good it was.
6148
+ score: z21.number(),
6149
+ /** The set this clip arrived in — a carousel mixes stills and clips. */
6150
+ group: assetGroupRefSchema.optional()
6151
+ });
6152
+ var videosSearchResponseSchema = z21.array(videoSearchResultSchema);
6153
+ var videosUploadRequestSchema = z21.object({
6154
+ originalFilename: z21.string().optional(),
6155
+ descriptionContext: z21.string().optional()
6423
6156
  });
6424
- var tagChangeToolInputSchema = z19.object({
6425
- changes: z19.array(tagInputRequestSchema).min(1).max(8)
6157
+ var videosUploadResponseSchema = z21.object({ uploadUrl: z21.string(), videoId: z21.string() });
6158
+ var videosIngestRequestSchema = z21.object({
6159
+ // Must be a direct media file Mux can pull (mp4/mov/webm/…). A page URL —
6160
+ // a YouTube watch link, a TikTok post — is not one: the CLI resolves those to
6161
+ // a media file before calling this route.
6162
+ url: z21.string().url(),
6163
+ source: videoSourceSchema,
6164
+ externalId: z21.string().optional(),
6165
+ externalUrl: z21.string().optional(),
6166
+ // What the person adding the clip knew that the frames cannot show — which
6167
+ // campaign it belongs to, whether the person on camera is a real customer.
6168
+ // The upload route has carried this since it existed; ingest silently dropped
6169
+ // it, so a link-added clip was analysed with strictly less to go on than the
6170
+ // same file uploaded by hand.
6171
+ descriptionContext: z21.string().optional()
6426
6172
  });
6427
- var tagInputResultSchema = z19.discriminatedUnion("status", [
6428
- z19.object({
6429
- status: z19.literal("submitted"),
6430
- ref: z19.string(),
6431
- type: tagTypeSchema,
6432
- /** Identifying field name → value (non-secret), when the type has one. */
6433
- identifier: z19.record(z19.string(), z19.string()).optional(),
6434
- secretFieldsSet: z19.array(z19.string()),
6435
- note: z19.string().optional()
6436
- }),
6437
- z19.object({
6438
- status: z19.literal("declined"),
6439
- reason: z19.string().optional(),
6440
- /** Set when the platform could not apply an approved change, rather than the user skipping it. */
6441
- failed: z19.boolean().optional()
6442
- })
6443
- ]);
6444
- var tagChangeToolResultSchema = z19.object({
6445
- results: z19.array(tagInputResultSchema)
6173
+ var videosIngestResponseSchema = z21.object({
6174
+ videoId: z21.string(),
6175
+ deduped: z21.boolean()
6446
6176
  });
6177
+ var videosDeleteRequestSchema = z21.object({ id: z21.string().min(1, "Missing video ID") });
6178
+ var videosDeleteResponseSchema = z21.object({ ok: z21.literal(true) });
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
+ }
6447
6503
 
6448
- // ../api/src/testimonials.ts
6449
- import { z as z20 } from "zod";
6450
- var testimonialSourceTypeSchema = z20.enum(["google", "trustpilot"]);
6451
- var testimonialStatusSchema = z20.enum(["pending", "processing", "ready", "error"]);
6452
- var testimonialSentimentSchema = z20.enum(["positive", "neutral", "negative"]);
6453
- var testimonialDocSchema = z20.object({
6454
- _id: z20.string(),
6455
- _creationTime: z20.number(),
6456
- companyId: z20.string(),
6457
- sourceId: z20.string(),
6458
- sourceType: testimonialSourceTypeSchema,
6459
- reviewText: z20.string(),
6460
- reviewTitle: z20.string().optional(),
6461
- searchText: z20.string().optional(),
6462
- reviewerName: z20.string().optional(),
6463
- reviewerImageUrl: z20.string().optional(),
6464
- reviewerImageId: z20.string().optional(),
6465
- reviewerLocation: z20.string().optional(),
6466
- rating: z20.number().optional(),
6467
- reviewDate: z20.number().optional(),
6468
- ownerAnswer: z20.string().optional(),
6469
- mediaUrls: z20.array(z20.string()).optional(),
6470
- imageIds: z20.array(z20.string()).optional(),
6471
- videoIds: z20.array(z20.string()).optional(),
6472
- sourceUrl: z20.string().optional(),
6473
- rawData: z20.unknown().optional(),
6474
- tags: z20.array(z20.string()),
6475
- highlight: z20.string().optional(),
6476
- language: z20.string().optional(),
6477
- summary: z20.string().optional(),
6478
- sentiment: testimonialSentimentSchema.optional(),
6479
- textEmbedding: z20.array(z20.number()).optional(),
6480
- externalId: z20.string().optional(),
6481
- contentHash: z20.string().optional(),
6482
- status: testimonialStatusSchema,
6483
- errorMessage: z20.string().optional(),
6484
- createdAt: z20.number(),
6485
- updatedAt: z20.number()
6486
- });
6487
- var testimonialsListRequestSchema = z20.object({
6488
- source: testimonialSourceTypeSchema.optional(),
6489
- rating_min: z20.coerce.number().int().min(1).max(5).optional(),
6490
- rating_max: z20.coerce.number().int().min(1).max(5).optional(),
6491
- tags: z20.string().transform((s) => s.split(",").filter(Boolean)).optional(),
6492
- status: testimonialStatusSchema.optional(),
6493
- sentiment: testimonialSentimentSchema.optional(),
6494
- language: z20.string().min(2).max(5).optional(),
6495
- limit: z20.coerce.number().int().positive().max(200).optional()
6496
- });
6497
- var testimonialsListResponseSchema = z20.array(testimonialDocSchema);
6498
- var testimonialsGetRequestSchema = z20.object({ id: z20.string().min(1, "Missing id parameter") });
6499
- var testimonialsSearchRequestSchema = z20.object({
6500
- query: z20.string().min(1),
6501
- limit: z20.coerce.number().int().positive().max(100).optional(),
6502
- source: testimonialSourceTypeSchema.optional(),
6503
- rating_min: z20.coerce.number().int().min(1).max(5).optional(),
6504
- rating_max: z20.coerce.number().int().min(1).max(5).optional(),
6505
- tags: z20.array(z20.string()).optional(),
6506
- status: testimonialStatusSchema.optional(),
6507
- sentiment: testimonialSentimentSchema.optional(),
6508
- language: z20.string().min(2).max(5).optional()
6509
- }).refine(
6510
- (data) => data.rating_min === void 0 || data.rating_max === void 0 || data.rating_min <= data.rating_max,
6511
- { message: "rating_min must be less than or equal to rating_max" }
6512
- );
6513
- var testimonialsSearchResponseSchema = z20.array(testimonialDocSchema);
6514
- var testimonialsOutscraperWebhookResponseSchema = z20.object({
6515
- ok: z20.literal(true),
6516
- note: z20.string().optional()
6517
- });
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
+ }
6518
6608
 
6519
- // ../api/src/videos.ts
6520
- import { z as z21 } from "zod";
6521
- var videoStatusSchema = z21.enum(["uploading", "uploaded", "processing", "ready", "error"]);
6522
- var videoSourceSchema = z21.enum([
6523
- "uploaded",
6524
- "url",
6525
- "bilibili",
6526
- "bluesky",
6527
- "dailymotion",
6528
- "facebook",
6529
- "instagram",
6530
- "loom",
6531
- "newgrounds",
6532
- "ok",
6533
- "pinterest",
6534
- "reddit",
6535
- "rutube",
6536
- "snapchat",
6537
- "streamable",
6538
- "tiktok",
6539
- "tumblr",
6540
- "twitch",
6541
- "twitter",
6542
- "vimeo",
6543
- "vk",
6544
- "youtube"
6545
- ]);
6546
- var videoTranscriptSegmentSchema = z21.object({
6547
- text: z21.string(),
6548
- startSecond: z21.number(),
6549
- endSecond: z21.number()
6550
- });
6551
- var videoSceneSchema = z21.object({
6552
- title: z21.string(),
6553
- description: z21.string(),
6554
- startSecond: z21.number(),
6555
- endSecond: z21.number(),
6556
- thumbnailTime: z21.number(),
6557
- // Optional because rows analysed before the richer beat breakdown shipped
6558
- // carry only the five fields above.
6559
- shot: z21.string().optional(),
6560
- visualChange: z21.string().optional(),
6561
- onScreenText: z21.array(z21.string()).optional(),
6562
- hasOverlay: z21.boolean().optional(),
6563
- onCamera: z21.array(z21.string()).optional(),
6564
- spoken: z21.string().optional()
6565
- });
6566
- var videoAnalysisSchema = z21.object({
6567
- subject: z21.string(),
6568
- people: z21.array(mediaPersonSchema),
6569
- brandOwnership: z21.string(),
6570
- spokenLanguages: z21.array(z21.string()),
6571
- onScreenText: z21.array(z21.string()),
6572
- shotStyle: z21.string(),
6573
- audioStyle: z21.string(),
6574
- hook: z21.string(),
6575
- usageNotes: z21.string()
6576
- });
6577
- var videoDocSchema = z21.object({
6578
- _id: z21.string(),
6579
- _creationTime: z21.number(),
6580
- companyId: z21.string(),
6581
- muxAssetId: z21.string(),
6582
- muxPlaybackId: z21.string(),
6583
- muxUploadId: z21.string(),
6584
- name: z21.string(),
6585
- description: z21.string(),
6586
- tags: z21.array(z21.string()),
6587
- source: z21.string(),
6588
- externalId: z21.string().optional(),
6589
- externalUrl: z21.string().optional(),
6590
- sourceId: z21.string().optional(),
6591
- originalFilename: z21.string().optional(),
6592
- descriptionContext: z21.string().optional(),
6593
- analysis: videoAnalysisSchema.optional(),
6594
- width: z21.number().optional(),
6595
- height: z21.number().optional(),
6596
- aspectRatio: z21.number().optional(),
6597
- duration: z21.number().optional(),
6598
- transcript: z21.string().optional(),
6599
- transcriptSegments: z21.array(videoTranscriptSegmentSchema).optional(),
6600
- scenes: z21.array(videoSceneSchema).optional(),
6601
- descriptionEmbedding: z21.array(z21.number()).optional(),
6602
- searchText: z21.string().optional(),
6603
- status: videoStatusSchema,
6604
- errorMessage: z21.string().optional(),
6605
- createdAt: z21.number(),
6606
- updatedAt: z21.number(),
6607
- thumbnailUrl: z21.string()
6608
- }).extend(assetGroupFieldsSchema.shape);
6609
- var videosWebhookResponseSchema = z21.object({ ok: z21.literal(true) });
6610
- var videosGetRequestSchema = z21.object({ id: z21.string().min(1, "Missing id parameter") });
6611
- var videosGetResponseSchema = videoDocSchema.omit({ descriptionEmbedding: true, searchText: true });
6612
- var videosSearchRequestSchema = z21.object({
6613
- query: z21.string().min(1),
6614
- limit: z21.coerce.number().int().positive().max(100).optional(),
6615
- tags: z21.array(z21.string()).optional()
6616
- });
6617
- var videoSearchResultSchema = z21.object({
6618
- _id: z21.string(),
6619
- thumbnailUrl: z21.string(),
6620
- name: z21.string(),
6621
- description: z21.string(),
6622
- tags: z21.array(z21.string()),
6623
- status: z21.string(),
6624
- duration: z21.number().optional(),
6625
- muxPlaybackId: z21.string(),
6626
- createdAt: z21.number(),
6627
- // The agent picks a clip from this projection alone, so the facts that decide
6628
- // a pick travel with it: how many beats, whether it is transcribed, and who
6629
- // is in it.
6630
- sceneCount: z21.number(),
6631
- hasTranscript: z21.boolean(),
6632
- analysis: videoAnalysisSchema.optional(),
6633
- // Relevance, on the same footing as `imageSearchResultSchema.score`: Cohere's
6634
- // rerank score when the rerank ran, the RRF fusion score when it did not. A
6635
- // clip hit used to arrive with no measure of how good it was.
6636
- score: z21.number(),
6637
- /** The set this clip arrived in — a carousel mixes stills and clips. */
6638
- group: assetGroupRefSchema.optional()
6639
- });
6640
- var videosSearchResponseSchema = z21.array(videoSearchResultSchema);
6641
- var videosUploadRequestSchema = z21.object({
6642
- originalFilename: z21.string().optional(),
6643
- descriptionContext: z21.string().optional()
6644
- });
6645
- var videosUploadResponseSchema = z21.object({ uploadUrl: z21.string(), videoId: z21.string() });
6646
- var videosIngestRequestSchema = z21.object({
6647
- // Must be a direct media file Mux can pull (mp4/mov/webm/…). A page URL —
6648
- // a YouTube watch link, a TikTok post — is not one: the CLI resolves those to
6649
- // a media file before calling this route.
6650
- url: z21.string().url(),
6651
- source: videoSourceSchema,
6652
- externalId: z21.string().optional(),
6653
- externalUrl: z21.string().optional(),
6654
- // What the person adding the clip knew that the frames cannot show — which
6655
- // campaign it belongs to, whether the person on camera is a real customer.
6656
- // The upload route has carried this since it existed; ingest silently dropped
6657
- // it, so a link-added clip was analysed with strictly less to go on than the
6658
- // same file uploaded by hand.
6659
- descriptionContext: z21.string().optional()
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
+ }
6660
6616
  });
6661
- var videosIngestResponseSchema = z21.object({
6662
- videoId: z21.string(),
6663
- deduped: z21.boolean()
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
+ }
6664
6670
  });
6665
- var videosDeleteRequestSchema = z21.object({ id: z21.string().min(1, "Missing video ID") });
6666
- var videosDeleteResponseSchema = z21.object({ ok: z21.literal(true) });
6667
6671
 
6668
6672
  // src/commands/actions/complete.ts
6669
6673
  import { defineCommand as defineCommand2 } from "citty";
@@ -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", {