@sitecoreai-labs/sitecoreai-cli 0.1.2 → 0.2.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.
Files changed (65) hide show
  1. package/dist/agents/tasks/agent.js +2 -2
  2. package/dist/agents/tasks/resources.js +2 -2
  3. package/dist/agents/tasks/space.js +1 -1
  4. package/dist/brand/api/auth.d.ts +16 -9
  5. package/dist/brand/api/auth.js +29 -19
  6. package/dist/brand/credential.d.ts +71 -1
  7. package/dist/brand/credential.js +119 -2
  8. package/dist/brand/recipe/diff.js +7 -2
  9. package/dist/brand/recipe/kind.js +0 -0
  10. package/dist/brand/recipe/schema.d.ts +113 -7
  11. package/dist/brand/recipe/schema.js +137 -8
  12. package/dist/brand/seed.d.ts +9 -5
  13. package/dist/brand/seed.js +30 -5
  14. package/dist/brief/api/briefs.d.ts +8 -0
  15. package/dist/brief/api/briefs.js +49 -11
  16. package/dist/brief/index.d.ts +1 -1
  17. package/dist/brief/index.js +2 -1
  18. package/dist/brief/recipe/index.d.ts +11 -2
  19. package/dist/brief/recipe/index.js +17 -3
  20. package/dist/brief/recipe/instance-diff.d.ts +4 -0
  21. package/dist/brief/recipe/instance-diff.js +77 -0
  22. package/dist/brief/recipe/instance-kind.d.ts +4 -0
  23. package/dist/brief/recipe/instance-kind.js +190 -0
  24. package/dist/brief/recipe/instance-schema.d.ts +61 -0
  25. package/dist/brief/recipe/instance-schema.js +68 -0
  26. package/dist/brief/recipe/schema.js +4 -1
  27. package/dist/brief/tasks/index.d.ts +35 -0
  28. package/dist/brief/tasks/index.js +62 -1
  29. package/dist/campaigns/recipe/schema.d.ts +39 -8
  30. package/dist/campaigns/recipe/schema.js +40 -10
  31. package/dist/commands/agents/sync.js +2 -2
  32. package/dist/commands/brand/seed.js +1 -1
  33. package/dist/commands/brand/sync.js +2 -2
  34. package/dist/commands/brief/create.d.ts +2 -0
  35. package/dist/commands/brief/create.js +56 -0
  36. package/dist/commands/brief/index.d.ts +3 -1
  37. package/dist/commands/brief/index.js +11 -1
  38. package/dist/commands/brief/sync.d.ts +9 -6
  39. package/dist/commands/brief/sync.js +54 -22
  40. package/dist/commands/brief/update.d.ts +2 -0
  41. package/dist/commands/brief/update.js +84 -0
  42. package/dist/commands/campaign/sync.js +2 -2
  43. package/dist/mcp/descriptions.js +3 -3
  44. package/dist/mcp/tools/brief-recipe.js +67 -23
  45. package/dist/mcp/tools/brief.js +83 -6
  46. package/dist/recipe/compile/design-parameters-template.js +5 -3
  47. package/dist/recipe/compile/enumeration.js +6 -11
  48. package/dist/recipe/compile/shared.js +12 -12
  49. package/dist/recipe/compile.js +4 -4
  50. package/dist/recipe/io.d.ts +8 -3
  51. package/dist/recipe/io.js +11 -81
  52. package/dist/recipe/items/read-current.js +31 -24
  53. package/dist/recipe/schema/recipe.d.ts +167 -84
  54. package/dist/recipe/schema/recipe.js +130 -46
  55. package/dist/recipe/schema/source-fields.d.ts +20 -0
  56. package/dist/recipe/schema/source-fields.js +25 -1
  57. package/dist/recipe/validate.d.ts +3 -3
  58. package/dist/recipe/validate.js +20 -10
  59. package/dist/sync/aggregate-kinds.js +1 -0
  60. package/dist/sync/aggregate.js +2 -2
  61. package/dist/sync/io.d.ts +13 -3
  62. package/dist/sync/io.js +43 -17
  63. package/dist/sync/typescript-recipe.d.ts +30 -0
  64. package/dist/sync/typescript-recipe.js +112 -0
  65. package/package.json +1 -1
@@ -0,0 +1,190 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.briefInstanceKind = void 0;
4
+ /**
5
+ * The `brief` recipe kind — wires the Sitecore Content Operations
6
+ * Brief instance API into the `sync` engine.
7
+ *
8
+ * `ref.id` is the brief's display NAME — recipes identify a brief by
9
+ * name, not UUID, mirroring how `campaign` identifies a project. The
10
+ * brief type is referenced by its stable codename (`briefTypeName`)
11
+ * and resolved to a server id at push-time via `listBriefTypes`.
12
+ *
13
+ * `apply` is straight CRUD: `createBrief` when the brief is absent,
14
+ * `updateBrief` (partial PUT) to converge an existing one. Repointing
15
+ * an existing brief at a different brief type is refused — the Brief
16
+ * API has no verified path for that.
17
+ *
18
+ * See docs/recipe-sync-architecture.md.
19
+ */
20
+ const brief_1 = require("../../brief");
21
+ const errors_1 = require("../../shared/errors");
22
+ const client_1 = require("./client");
23
+ const instance_diff_1 = require("./instance-diff");
24
+ const instance_schema_1 = require("./instance-schema");
25
+ /**
26
+ * Find a brief by its display name, paging the list endpoint until a
27
+ * match is found or the cursor is exhausted. The Brief list endpoint
28
+ * supports no server-side name filter (see `ListBriefsQuery`), so the
29
+ * walk is unavoidable.
30
+ */
31
+ const findBriefByName = async (client, name) => {
32
+ let cursor;
33
+ for (;;) {
34
+ const page = await (0, brief_1.listBriefs)(client, cursor ? { next: cursor } : undefined);
35
+ const match = page.data.find((brief) => brief.name === name);
36
+ if (match)
37
+ return match;
38
+ if (!page.next || page.data.length === 0)
39
+ return null;
40
+ cursor = page.next;
41
+ }
42
+ };
43
+ /** Find a brief type by its codename (mirrors `briefTypeKind`'s helper). */
44
+ const findTypeByName = async (client, name) => {
45
+ const page = await (0, brief_1.listBriefTypes)(client);
46
+ return page.data.find((type) => type.name === name) ?? null;
47
+ };
48
+ /** Enumerate every brief on the remote — fans out into the aggregate sync. */
49
+ const list = async (ctx) => {
50
+ const client = await (0, client_1.resolveBriefClient)(ctx);
51
+ const refs = [];
52
+ let cursor;
53
+ for (;;) {
54
+ const page = await (0, brief_1.listBriefs)(client, cursor ? { next: cursor } : undefined);
55
+ for (const brief of page.data) {
56
+ refs.push({ kind: "brief", id: brief.name });
57
+ }
58
+ if (!page.next || page.data.length === 0)
59
+ return refs;
60
+ cursor = page.next;
61
+ }
62
+ };
63
+ /** Project a live brief into the recipe shape, dropping server ids/metadata. */
64
+ const toRecipe = (brief, briefTypeName) => ({
65
+ name: brief.name,
66
+ briefTypeName,
67
+ locale: brief.locale || undefined,
68
+ status: brief.status,
69
+ isTemplate: brief.isTemplate,
70
+ fields: brief.fields ?? {},
71
+ });
72
+ /**
73
+ * Capture a live brief as a recipe. `null` when no brief has the name.
74
+ *
75
+ * The list endpoint omits the full nested `fields` map on some
76
+ * envelopes, so the matched id is re-read via `getBrief` for fidelity.
77
+ */
78
+ const readCurrent = async (ref, ctx) => {
79
+ const client = await (0, client_1.resolveBriefClient)(ctx);
80
+ const found = await findBriefByName(client, ref.id);
81
+ if (!found)
82
+ return null;
83
+ const brief = await (0, brief_1.getBrief)(client, found.id);
84
+ // Resolve the brief-type codename. The brief carries a `Link` to its
85
+ // type with the id but not the codename, so a follow-up listTypes
86
+ // resolves it. If the type has been deleted (orphan brief), surface
87
+ // the id verbatim so the pulled recipe still round-trips.
88
+ const types = await (0, brief_1.listBriefTypes)(client);
89
+ const briefTypeName = types.data.find((type) => type.id === brief.briefType.id)?.name ?? brief.briefType.id;
90
+ return toRecipe(brief, briefTypeName);
91
+ };
92
+ /** Resolve a recipe's `briefTypeName` to its server id, or fail with a hint. */
93
+ const resolveBriefTypeId = async (client, briefTypeName) => {
94
+ const type = await findTypeByName(client, briefTypeName);
95
+ if (!type) {
96
+ throw (0, errors_1.createScaiError)(`Brief type "${briefTypeName}" not found.`, "INPUT_INVALID", {
97
+ hint: "Push the brief-type recipe first, or check the `briefTypeName` codename with `scai ops brief types list`.",
98
+ });
99
+ }
100
+ return type.id;
101
+ };
102
+ /** Apply a plan — create the brief, or PUT-patch it onto the desired state. */
103
+ const apply = async (plan, ref, ctx) => {
104
+ const client = await (0, client_1.resolveBriefClient)(ctx);
105
+ const applied = [];
106
+ const skipped = [];
107
+ // The single `stage: "instance"` change carries the full desired
108
+ // recipe; per-element `stage: "field"` changes are descriptive only.
109
+ const instanceChange = plan.changes.find((change) => change.meta?.stage === "instance");
110
+ if (!instanceChange) {
111
+ // Nothing to write — an all-noop plan.
112
+ for (const change of plan.changes)
113
+ skipped.push(change);
114
+ return { applied, skipped };
115
+ }
116
+ const recipe = instanceChange.meta?.recipe;
117
+ if (!recipe) {
118
+ throw (0, errors_1.createScaiError)("Brief plan change is missing its recipe payload.", "INPUT_INVALID", {
119
+ hint: "This is an internal diff error — change.meta.recipe was not set.",
120
+ });
121
+ }
122
+ if (instanceChange.kind === "create") {
123
+ const briefTypeId = await resolveBriefTypeId(client, recipe.briefTypeName);
124
+ const input = {
125
+ name: recipe.name,
126
+ briefTypeId,
127
+ ...(recipe.locale !== undefined && { locale: recipe.locale }),
128
+ ...(Object.keys(recipe.fields ?? {}).length > 0 && { fields: recipe.fields }),
129
+ ...(recipe.isTemplate !== undefined && { isTemplate: recipe.isTemplate }),
130
+ };
131
+ ctx.logger?.info(`Creating brief "${recipe.name}".`);
132
+ const created = await (0, brief_1.createBrief)(client, input);
133
+ // `createBrief` accepts no `status` field — POSTs land in the
134
+ // server default ("Draft"). If the recipe pins a different status,
135
+ // converge with a follow-up PUT so the post-apply state matches.
136
+ if (recipe.status && recipe.status !== created.status) {
137
+ ctx.logger?.info(`Setting brief "${recipe.name}" status to "${recipe.status}".`);
138
+ await (0, brief_1.updateBrief)(client, created.id, { status: recipe.status });
139
+ }
140
+ }
141
+ else {
142
+ const existing = await findBriefByName(client, ref.id);
143
+ if (!existing) {
144
+ throw (0, errors_1.createScaiError)(`Brief "${ref.id}" not found.`, "INPUT_INVALID", {
145
+ hint: "The brief was expected to exist for an update — check the name or push a create.",
146
+ });
147
+ }
148
+ // The Brief API has no verified path to repoint a brief at a
149
+ // different type. Surface that loudly instead of silently dropping
150
+ // the change.
151
+ const types = await (0, brief_1.listBriefTypes)(client);
152
+ const existingTypeName = types.data.find((type) => type.id === existing.briefType.id)?.name ?? existing.briefType.id;
153
+ if (existingTypeName !== recipe.briefTypeName) {
154
+ throw (0, errors_1.createScaiError)(`Cannot repoint brief "${recipe.name}" from type "${existingTypeName}" to "${recipe.briefTypeName}".`, "INPUT_INVALID", {
155
+ hint: "The Brief API has no verified path to change a brief's type. Delete and recreate the brief, or correct the recipe's briefTypeName.",
156
+ });
157
+ }
158
+ const patch = {
159
+ name: recipe.name,
160
+ ...(recipe.locale !== undefined && { locale: recipe.locale }),
161
+ ...(recipe.isTemplate !== undefined && { isTemplate: recipe.isTemplate }),
162
+ ...(recipe.status !== undefined && { status: recipe.status }),
163
+ fields: recipe.fields ?? {},
164
+ };
165
+ ctx.logger?.info(`Updating brief "${recipe.name}" (${existing.id}).`);
166
+ await (0, brief_1.updateBrief)(client, existing.id, patch);
167
+ }
168
+ applied.push(instanceChange);
169
+ // Per-element changes are converged by the single PUT (or POST).
170
+ for (const change of plan.changes) {
171
+ if (change === instanceChange)
172
+ continue;
173
+ if (change.kind === "noop")
174
+ skipped.push(change);
175
+ else
176
+ applied.push(change);
177
+ }
178
+ return { applied, skipped };
179
+ };
180
+ /** Compute the plan to converge a brief onto `desired`. */
181
+ const plan = async (desired, ref, ctx) => (0, instance_diff_1.diffBriefInstance)(desired, await readCurrent(ref, ctx));
182
+ /** The `brief` recipe kind. */
183
+ exports.briefInstanceKind = {
184
+ name: "brief",
185
+ schema: instance_schema_1.BriefInstanceRecipeSchema,
186
+ readCurrent,
187
+ plan,
188
+ apply,
189
+ list,
190
+ };
@@ -0,0 +1,61 @@
1
+ /**
2
+ * `BriefInstanceRecipe` — the declarative definition of a Sitecore
3
+ * Content Operations *brief instance* (the unit of work — a populated
4
+ * brief built against a `BriefType` schema).
5
+ *
6
+ * Companion to `BriefTypeRecipeSchema` (this file's sibling `schema.ts`,
7
+ * which models the schema template). Brief instances reference their
8
+ * type by stable codename (`briefTypeName`) rather than UUID, exactly as
9
+ * a `campaign` recipe references nothing by id and identifies itself by
10
+ * `name`. See docs/recipe-sync-architecture.md.
11
+ *
12
+ * The schema captures the fields `createBrief` / `updateBrief` accept
13
+ * on the Brief API — `name`, `briefTypeName`, `locale`, `status`,
14
+ * `isTemplate`, and the per-field `fields` map. Sub-resources (tasks,
15
+ * comments, references, contributors, external mappings) are returned
16
+ * by the read endpoints but are not part of the brief write surface,
17
+ * so they stay out of the recipe.
18
+ */
19
+ import { z } from "zod";
20
+ /**
21
+ * Brief workflow status — the values the Brief API accepts on a write.
22
+ * Mirrors the `BriefStatus` wire type in `../api/schema.ts`; surfacing
23
+ * the literal set here means the model reads the enum directly off the
24
+ * recipe schema (and bad values fail at parse-time, not push-time).
25
+ *
26
+ * `InReview` is the wire form for the "In Review" UI label.
27
+ */
28
+ export declare const BriefInstanceStatusSchema: z.ZodEnum<{
29
+ Draft: "Draft";
30
+ InReview: "InReview";
31
+ Approved: "Approved";
32
+ Canceled: "Canceled";
33
+ Archived: "Archived";
34
+ }>;
35
+ /**
36
+ * Field values on a brief instance, keyed by `BriefField.name`. The
37
+ * per-field shape varies by field `type` on the brief's type (RichText
38
+ * values are ProseMirror docs; DateTime is an ISO string; Timeline and
39
+ * Budget carry their own object shape). The shape is left as
40
+ * `z.unknown()` so any field type round-trips losslessly between
41
+ * `recipe pull` and `recipe push` without the recipe schema chasing
42
+ * the per-field encoding. Validation is deferred to the server.
43
+ */
44
+ export declare const BriefInstanceFieldsSchema: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
45
+ /** The full brief-instance recipe. */
46
+ export declare const BriefInstanceRecipeSchema: z.ZodObject<{
47
+ name: z.ZodString;
48
+ briefTypeName: z.ZodString;
49
+ locale: z.ZodOptional<z.ZodString>;
50
+ status: z.ZodOptional<z.ZodEnum<{
51
+ Draft: "Draft";
52
+ InReview: "InReview";
53
+ Approved: "Approved";
54
+ Canceled: "Canceled";
55
+ Archived: "Archived";
56
+ }>>;
57
+ isTemplate: z.ZodOptional<z.ZodBoolean>;
58
+ fields: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
59
+ }, z.core.$strip>;
60
+ /** A validated brief-instance recipe. */
61
+ export type BriefInstanceRecipe = z.infer<typeof BriefInstanceRecipeSchema>;
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BriefInstanceRecipeSchema = exports.BriefInstanceFieldsSchema = exports.BriefInstanceStatusSchema = void 0;
4
+ /**
5
+ * `BriefInstanceRecipe` — the declarative definition of a Sitecore
6
+ * Content Operations *brief instance* (the unit of work — a populated
7
+ * brief built against a `BriefType` schema).
8
+ *
9
+ * Companion to `BriefTypeRecipeSchema` (this file's sibling `schema.ts`,
10
+ * which models the schema template). Brief instances reference their
11
+ * type by stable codename (`briefTypeName`) rather than UUID, exactly as
12
+ * a `campaign` recipe references nothing by id and identifies itself by
13
+ * `name`. See docs/recipe-sync-architecture.md.
14
+ *
15
+ * The schema captures the fields `createBrief` / `updateBrief` accept
16
+ * on the Brief API — `name`, `briefTypeName`, `locale`, `status`,
17
+ * `isTemplate`, and the per-field `fields` map. Sub-resources (tasks,
18
+ * comments, references, contributors, external mappings) are returned
19
+ * by the read endpoints but are not part of the brief write surface,
20
+ * so they stay out of the recipe.
21
+ */
22
+ const zod_1 = require("zod");
23
+ /**
24
+ * Brief workflow status — the values the Brief API accepts on a write.
25
+ * Mirrors the `BriefStatus` wire type in `../api/schema.ts`; surfacing
26
+ * the literal set here means the model reads the enum directly off the
27
+ * recipe schema (and bad values fail at parse-time, not push-time).
28
+ *
29
+ * `InReview` is the wire form for the "In Review" UI label.
30
+ */
31
+ exports.BriefInstanceStatusSchema = zod_1.z
32
+ .enum(["Draft", "InReview", "Approved", "Canceled", "Archived"])
33
+ .describe('Brief workflow status. Wire form — "InReview" is the "In Review" UI label. A brief must leave "Draft" before it can be linked to a campaign.');
34
+ /**
35
+ * Field values on a brief instance, keyed by `BriefField.name`. The
36
+ * per-field shape varies by field `type` on the brief's type (RichText
37
+ * values are ProseMirror docs; DateTime is an ISO string; Timeline and
38
+ * Budget carry their own object shape). The shape is left as
39
+ * `z.unknown()` so any field type round-trips losslessly between
40
+ * `recipe pull` and `recipe push` without the recipe schema chasing
41
+ * the per-field encoding. Validation is deferred to the server.
42
+ */
43
+ exports.BriefInstanceFieldsSchema = zod_1.z
44
+ .record(zod_1.z.string(), zod_1.z.unknown())
45
+ .default({})
46
+ .describe("Field values keyed by BriefField.name. Per-field value shape follows the brief type's field definitions (e.g. RichText is a ProseMirror doc node).");
47
+ /** The full brief-instance recipe. */
48
+ exports.BriefInstanceRecipeSchema = zod_1.z.object({
49
+ name: zod_1.z
50
+ .string()
51
+ .min(1)
52
+ .describe("Display name of the brief. Identifies the brief when pushing. Briefs are matched by name; the recipe pushes through to `createBrief` / `updateBrief`."),
53
+ briefTypeName: zod_1.z
54
+ .string()
55
+ .min(1)
56
+ .regex(/^[A-Za-z][A-Za-z0-9_]*$/, "Must start with a letter and contain only letters, digits, or underscores.")
57
+ .describe("Codename of the brief type this brief is built against — looked up at push-time and resolved to its server id. The brief type must already exist (push the type's recipe first if it doesn't)."),
58
+ locale: zod_1.z
59
+ .string()
60
+ .optional()
61
+ .describe('Brief locale — BCP-47-ish, e.g. "en-us". Omit to let the server default apply.'),
62
+ status: exports.BriefInstanceStatusSchema.optional().describe('Brief workflow status. Omit to leave at the server default ("Draft" on creation).'),
63
+ isTemplate: zod_1.z
64
+ .boolean()
65
+ .optional()
66
+ .describe("Whether the brief is a template (omit to leave at the server default)."),
67
+ fields: exports.BriefInstanceFieldsSchema,
68
+ });
@@ -69,7 +69,10 @@ exports.BudgetFieldSchema = zod_1.z
69
69
  type: zod_1.z.literal("Budget").describe("Discriminator — a budget/currency field."),
70
70
  ...briefFieldBaseShape,
71
71
  currencies: zod_1.z
72
- .array(zod_1.z.string())
72
+ .array(zod_1.z
73
+ .string()
74
+ .length(3)
75
+ .regex(/^[A-Z]{3}$/, "ISO-4217 currency code must be 3 uppercase letters (e.g. `USD`)."))
73
76
  .describe('ISO-4217 currency codes the field accepts, e.g. ["USD", "EUR"].'),
74
77
  })
75
78
  .describe("A budget/currency field.");
@@ -1,3 +1,4 @@
1
+ import { type CreateBriefInput } from "../api/briefs";
1
2
  import { type CreateBriefTypeInput } from "../api/brief-types";
2
3
  import type { Brief, BriefComment, BriefStatus, BriefTask, BriefType, PagedResult } from "../api/schema";
3
4
  /**
@@ -100,6 +101,40 @@ export declare const runBriefCommentsList: (options: RunBriefBaseOptions & {
100
101
  briefId?: string;
101
102
  limit?: number;
102
103
  }) => Promise<PagedResult<BriefComment>>;
104
+ /**
105
+ * Create a brief instance from a `CreateBriefInput`. Mirrors
106
+ * `runBriefTypeCreate` — honours `whatIf` for a plan-only dry run. The
107
+ * SDK `createBrief` is verified against the Agents tenant.
108
+ */
109
+ export declare const runBriefCreate: (options: RunBriefBaseOptions & {
110
+ input: CreateBriefInput;
111
+ whatIf?: boolean;
112
+ }) => Promise<Brief | {
113
+ plan: CreateBriefInput;
114
+ }>;
115
+ /**
116
+ * Update a brief instance by id with a partial patch (`PUT`). Accepts
117
+ * any subset of `CreateBriefInput` plus an optional `status`. Mirrors
118
+ * `runBriefTypeUpdate` — honours `whatIf` for a plan-only dry run. The
119
+ * status-only PUT path is verified (2026-05-15); other partial fields
120
+ * are wired the same way but not smoke-tested.
121
+ */
122
+ export declare const runBriefUpdate: (options: RunBriefBaseOptions & {
123
+ briefId: string;
124
+ patch: Partial<CreateBriefInput> & {
125
+ status?: BriefStatus;
126
+ };
127
+ whatIf?: boolean;
128
+ }) => Promise<{
129
+ id: string;
130
+ } | {
131
+ plan: {
132
+ id: string;
133
+ patch: Partial<CreateBriefInput> & {
134
+ status?: BriefStatus;
135
+ };
136
+ };
137
+ }>;
103
138
  /**
104
139
  * Delete a brief instance. Mirrors `runBriefTypeDelete` — honours
105
140
  * `whatIf` for a plan-only dry run. SDK `deleteBrief` is verified
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.runBriefCommentAdd = exports.runBriefDelete = exports.runBriefCommentsList = exports.runBriefTodosList = exports.runBriefTypeDelete = exports.runBriefTypeUpdate = exports.runBriefTypeCreate = exports.runBriefTypeGet = exports.runBriefSetStatus = exports.runBriefTypes = exports.runBriefShow = exports.runBriefList = void 0;
3
+ exports.runBriefCommentAdd = exports.runBriefDelete = exports.runBriefUpdate = exports.runBriefCreate = exports.runBriefCommentsList = exports.runBriefTodosList = exports.runBriefTypeDelete = exports.runBriefTypeUpdate = exports.runBriefTypeCreate = exports.runBriefTypeGet = exports.runBriefSetStatus = exports.runBriefTypes = exports.runBriefShow = exports.runBriefList = void 0;
4
4
  const logger_1 = require("../../shared/logger");
5
5
  const client_1 = require("../client");
6
6
  const briefs_1 = require("../api/briefs");
@@ -229,6 +229,67 @@ const runBriefCommentsList = async (options) => {
229
229
  return result;
230
230
  };
231
231
  exports.runBriefCommentsList = runBriefCommentsList;
232
+ /**
233
+ * Create a brief instance from a `CreateBriefInput`. Mirrors
234
+ * `runBriefTypeCreate` — honours `whatIf` for a plan-only dry run. The
235
+ * SDK `createBrief` is verified against the Agents tenant.
236
+ */
237
+ const runBriefCreate = async (options) => {
238
+ const { logger, client } = await prepareBriefClient(options);
239
+ if (options.whatIf) {
240
+ const plan = { plan: options.input };
241
+ if (logger.isJson()) {
242
+ writeJson(plan);
243
+ }
244
+ else {
245
+ logger.info(`Would create brief '${options.input.name}'.`, "yellow");
246
+ logger.info(` Brief type: ${options.input.briefTypeId}`);
247
+ if (options.input.locale)
248
+ logger.info(` Locale: ${options.input.locale}`);
249
+ const fieldCount = Object.keys(options.input.fields ?? {}).length;
250
+ logger.info(` Fields: ${fieldCount}`);
251
+ }
252
+ return plan;
253
+ }
254
+ const created = await (0, briefs_1.createBrief)(client, options.input);
255
+ if (logger.isJson()) {
256
+ writeJson(created);
257
+ return created;
258
+ }
259
+ logger.info(`Created brief ${created.id} (${created.name}).`, "green");
260
+ return created;
261
+ };
262
+ exports.runBriefCreate = runBriefCreate;
263
+ /**
264
+ * Update a brief instance by id with a partial patch (`PUT`). Accepts
265
+ * any subset of `CreateBriefInput` plus an optional `status`. Mirrors
266
+ * `runBriefTypeUpdate` — honours `whatIf` for a plan-only dry run. The
267
+ * status-only PUT path is verified (2026-05-15); other partial fields
268
+ * are wired the same way but not smoke-tested.
269
+ */
270
+ const runBriefUpdate = async (options) => {
271
+ const { logger, client } = await prepareBriefClient(options);
272
+ if (options.whatIf) {
273
+ const plan = { plan: { id: options.briefId, patch: options.patch } };
274
+ if (logger.isJson()) {
275
+ writeJson(plan);
276
+ }
277
+ else {
278
+ logger.info(`Would PUT-update brief ${options.briefId}.`, "yellow");
279
+ const keys = Object.keys(options.patch);
280
+ logger.info(` Patch keys: ${keys.length === 0 ? "(none)" : keys.join(", ")}`);
281
+ }
282
+ return plan;
283
+ }
284
+ await (0, briefs_1.updateBrief)(client, options.briefId, options.patch);
285
+ if (logger.isJson()) {
286
+ writeJson({ id: options.briefId });
287
+ return { id: options.briefId };
288
+ }
289
+ logger.info(`Updated brief ${options.briefId}.`, "green");
290
+ return { id: options.briefId };
291
+ };
292
+ exports.runBriefUpdate = runBriefUpdate;
232
293
  /**
233
294
  * Delete a brief instance. Mirrors `runBriefTypeDelete` — honours
234
295
  * `whatIf` for a plan-only dry run. SDK `deleteBrief` is verified
@@ -9,6 +9,21 @@
9
9
  * model reads it. See docs/recipe-sync-architecture.md.
10
10
  */
11
11
  import { z } from "zod";
12
+ /**
13
+ * Server enum values **confirmed by observation** in HAR captures of the
14
+ * Sitecore Content Operations UI driving the Orchestrate API. Other
15
+ * values likely exist in the server's enum — `recipe pull` from a tenant
16
+ * with a live `IN_PROGRESS` campaign must still round-trip cleanly — so
17
+ * the schema accepts the known set as a strong hint and falls back to
18
+ * `z.string()`. JSON Schema renders this as `anyOf: [{ enum: [...] }, { type:
19
+ * "string" }]`, which the model reads as "prefer one of these values; new
20
+ * uppercase enums are acceptable too".
21
+ *
22
+ * Update the lists when more values are observed; the trailing
23
+ * `z.string()` keeps the schema permissive in the meantime.
24
+ */
25
+ export declare const KNOWN_CAMPAIGN_STATUSES: readonly ["NOT_STARTED"];
26
+ export declare const KNOWN_CAMPAIGN_FUNNEL_STAGES: readonly ["TOP"];
12
27
  /**
13
28
  * A task — the leaf work item of a campaign. Owned by a deliverable.
14
29
  * Identified within its deliverable by `name`; server ids are dropped
@@ -16,7 +31,9 @@ import { z } from "zod";
16
31
  */
17
32
  export declare const CampaignTaskSchema: z.ZodObject<{
18
33
  name: z.ZodString;
19
- status: z.ZodOptional<z.ZodString>;
34
+ status: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
35
+ NOT_STARTED: "NOT_STARTED";
36
+ }>, z.ZodString]>>;
20
37
  dueDate: z.ZodOptional<z.ZodString>;
21
38
  priority: z.ZodOptional<z.ZodString>;
22
39
  description: z.ZodOptional<z.ZodString>;
@@ -29,14 +46,20 @@ export declare const CampaignTaskSchema: z.ZodObject<{
29
46
  */
30
47
  export declare const CampaignDeliverableSchema: z.ZodObject<{
31
48
  name: z.ZodString;
32
- status: z.ZodOptional<z.ZodString>;
49
+ status: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
50
+ NOT_STARTED: "NOT_STARTED";
51
+ }>, z.ZodString]>>;
33
52
  dueDate: z.ZodOptional<z.ZodString>;
34
- funnelStage: z.ZodOptional<z.ZodString>;
53
+ funnelStage: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
54
+ TOP: "TOP";
55
+ }>, z.ZodString]>>;
35
56
  funnelTactics: z.ZodDefault<z.ZodArray<z.ZodString>>;
36
57
  labels: z.ZodDefault<z.ZodArray<z.ZodString>>;
37
58
  tasks: z.ZodDefault<z.ZodArray<z.ZodObject<{
38
59
  name: z.ZodString;
39
- status: z.ZodOptional<z.ZodString>;
60
+ status: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
61
+ NOT_STARTED: "NOT_STARTED";
62
+ }>, z.ZodString]>>;
40
63
  dueDate: z.ZodOptional<z.ZodString>;
41
64
  priority: z.ZodOptional<z.ZodString>;
42
65
  description: z.ZodOptional<z.ZodString>;
@@ -48,21 +71,29 @@ export declare const CampaignDeliverableSchema: z.ZodObject<{
48
71
  export declare const CampaignRecipeSchema: z.ZodObject<{
49
72
  name: z.ZodString;
50
73
  description: z.ZodOptional<z.ZodString>;
51
- status: z.ZodOptional<z.ZodString>;
74
+ status: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
75
+ NOT_STARTED: "NOT_STARTED";
76
+ }>, z.ZodString]>>;
52
77
  startDate: z.ZodOptional<z.ZodString>;
53
78
  dueDate: z.ZodOptional<z.ZodString>;
54
79
  brandKitId: z.ZodOptional<z.ZodString>;
55
80
  labels: z.ZodDefault<z.ZodArray<z.ZodString>>;
56
81
  deliverables: z.ZodDefault<z.ZodArray<z.ZodObject<{
57
82
  name: z.ZodString;
58
- status: z.ZodOptional<z.ZodString>;
83
+ status: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
84
+ NOT_STARTED: "NOT_STARTED";
85
+ }>, z.ZodString]>>;
59
86
  dueDate: z.ZodOptional<z.ZodString>;
60
- funnelStage: z.ZodOptional<z.ZodString>;
87
+ funnelStage: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
88
+ TOP: "TOP";
89
+ }>, z.ZodString]>>;
61
90
  funnelTactics: z.ZodDefault<z.ZodArray<z.ZodString>>;
62
91
  labels: z.ZodDefault<z.ZodArray<z.ZodString>>;
63
92
  tasks: z.ZodDefault<z.ZodArray<z.ZodObject<{
64
93
  name: z.ZodString;
65
- status: z.ZodOptional<z.ZodString>;
94
+ status: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
95
+ NOT_STARTED: "NOT_STARTED";
96
+ }>, z.ZodString]>>;
66
97
  dueDate: z.ZodOptional<z.ZodString>;
67
98
  priority: z.ZodOptional<z.ZodString>;
68
99
  description: z.ZodOptional<z.ZodString>;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.CampaignRecipeSchema = exports.CampaignDeliverableSchema = exports.CampaignTaskSchema = void 0;
3
+ exports.CampaignRecipeSchema = exports.CampaignDeliverableSchema = exports.CampaignTaskSchema = exports.KNOWN_CAMPAIGN_FUNNEL_STAGES = exports.KNOWN_CAMPAIGN_STATUSES = void 0;
4
4
  /**
5
5
  * `CampaignRecipe` — the declarative definition of a Sitecore
6
6
  * Orchestrate campaign (a `project`, with its nested deliverables and
@@ -12,6 +12,33 @@ exports.CampaignRecipeSchema = exports.CampaignDeliverableSchema = exports.Campa
12
12
  * model reads it. See docs/recipe-sync-architecture.md.
13
13
  */
14
14
  const zod_1 = require("zod");
15
+ /**
16
+ * ISO-8601 date-or-datetime — accepts `2026-05-26`, `2026-05-26T15:00:00Z`,
17
+ * or `2026-05-26T15:00:00.123+02:00`. Less strict than `z.string().datetime()`
18
+ * because the campaign API returns date-only values for some fields.
19
+ */
20
+ const Iso8601 = zod_1.z
21
+ .string()
22
+ .regex(/^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:?\d{2}))?$/, {
23
+ message: "must be an ISO-8601 date or datetime (e.g. `2026-05-26` or `2026-05-26T15:00:00Z`)",
24
+ });
25
+ /**
26
+ * Server enum values **confirmed by observation** in HAR captures of the
27
+ * Sitecore Content Operations UI driving the Orchestrate API. Other
28
+ * values likely exist in the server's enum — `recipe pull` from a tenant
29
+ * with a live `IN_PROGRESS` campaign must still round-trip cleanly — so
30
+ * the schema accepts the known set as a strong hint and falls back to
31
+ * `z.string()`. JSON Schema renders this as `anyOf: [{ enum: [...] }, { type:
32
+ * "string" }]`, which the model reads as "prefer one of these values; new
33
+ * uppercase enums are acceptable too".
34
+ *
35
+ * Update the lists when more values are observed; the trailing
36
+ * `z.string()` keeps the schema permissive in the meantime.
37
+ */
38
+ exports.KNOWN_CAMPAIGN_STATUSES = ["NOT_STARTED"];
39
+ exports.KNOWN_CAMPAIGN_FUNNEL_STAGES = ["TOP"];
40
+ const CampaignStatusSchema = zod_1.z.union([zod_1.z.enum(exports.KNOWN_CAMPAIGN_STATUSES), zod_1.z.string()]);
41
+ const CampaignFunnelStageSchema = zod_1.z.union([zod_1.z.enum(exports.KNOWN_CAMPAIGN_FUNNEL_STAGES), zod_1.z.string()]);
15
42
  /**
16
43
  * A task — the leaf work item of a campaign. Owned by a deliverable.
17
44
  * Identified within its deliverable by `name`; server ids are dropped
@@ -19,9 +46,12 @@ const zod_1 = require("zod");
19
46
  */
20
47
  exports.CampaignTaskSchema = zod_1.z.object({
21
48
  name: zod_1.z.string().min(1).describe("Task name. Identifies the task within its deliverable."),
22
- status: zod_1.z.string().optional().describe('Task status — a server enum, e.g. "NOT_STARTED".'),
23
- dueDate: zod_1.z.string().optional().describe("Task due date (ISO-8601)."),
24
- priority: zod_1.z.string().optional().describe("Task priority — a server enum."),
49
+ status: CampaignStatusSchema.optional().describe('Task status — a server enum. Confirmed values: "NOT_STARTED". Other UPPER_SNAKE values may exist on the server; the schema accepts them but agents should prefer the confirmed set.'),
50
+ dueDate: Iso8601.optional().describe("Task due date (ISO-8601 date or datetime)."),
51
+ priority: zod_1.z
52
+ .string()
53
+ .optional()
54
+ .describe("Task priority — a server enum. No values have been observed yet; convention follows project-management norms (e.g. UPPERCASE strings). Schema is `string` until the enum set is captured."),
25
55
  description: zod_1.z.string().optional().describe("Task description. HTML."),
26
56
  assignee: zod_1.z.string().optional().describe('Assignee — an Auth0 user subject (e.g. "auth0|...").'),
27
57
  labels: zod_1.z.array(zod_1.z.string()).default([]).describe("Free-form labels on the task."),
@@ -35,9 +65,9 @@ exports.CampaignDeliverableSchema = zod_1.z.object({
35
65
  .string()
36
66
  .min(1)
37
67
  .describe("Deliverable name. Identifies the deliverable within its campaign."),
38
- status: zod_1.z.string().optional().describe('Deliverable status — a server enum, e.g. "NOT_STARTED".'),
39
- dueDate: zod_1.z.string().optional().describe("Deliverable due date (ISO-8601)."),
40
- funnelStage: zod_1.z.string().optional().describe('Funnel stage — a server enum, e.g. "TOP".'),
68
+ status: CampaignStatusSchema.optional().describe('Deliverable status — a server enum. Confirmed values: "NOT_STARTED". Other UPPER_SNAKE values may exist on the server; the schema accepts them but agents should prefer the confirmed set.'),
69
+ dueDate: Iso8601.optional().describe("Deliverable due date (ISO-8601 date or datetime)."),
70
+ funnelStage: CampaignFunnelStageSchema.optional().describe('Funnel stage — a server enum. Confirmed values: "TOP". Other values likely exist (e.g. middle / bottom of funnel); the schema accepts them but agents should prefer the confirmed set.'),
41
71
  funnelTactics: zod_1.z.array(zod_1.z.string()).default([]).describe("Funnel tactics for the deliverable."),
42
72
  labels: zod_1.z.array(zod_1.z.string()).default([]).describe("Free-form labels on the deliverable."),
43
73
  tasks: zod_1.z
@@ -52,9 +82,9 @@ exports.CampaignRecipeSchema = zod_1.z.object({
52
82
  .min(1)
53
83
  .describe("Display name of the campaign (project). Identifies the campaign when pushing."),
54
84
  description: zod_1.z.string().optional().describe("Human description of the campaign."),
55
- status: zod_1.z.string().optional().describe('Campaign status — a server enum, e.g. "NOT_STARTED".'),
56
- startDate: zod_1.z.string().optional().describe("Campaign start date (ISO-8601)."),
57
- dueDate: zod_1.z.string().optional().describe("Campaign due date (ISO-8601)."),
85
+ status: CampaignStatusSchema.optional().describe('Campaign status — a server enum. Confirmed values: "NOT_STARTED". Other UPPER_SNAKE values may exist on the server; the schema accepts them but agents should prefer the confirmed set.'),
86
+ startDate: Iso8601.optional().describe("Campaign start date (ISO-8601 date or datetime)."),
87
+ dueDate: Iso8601.optional().describe("Campaign due date (ISO-8601 date or datetime)."),
58
88
  brandKitId: zod_1.z
59
89
  .string()
60
90
  .optional()
@@ -91,7 +91,7 @@ const createDiffCommand = () => {
91
91
  const logger = (0, cli_tasks_1.toLogger)(options);
92
92
  const kind = resolveKind(options.kind);
93
93
  const ctx = buildContext(options, logger);
94
- const recipe = (0, sync_1.loadRecipe)(options.file ?? "", kind.schema);
94
+ const recipe = await (0, sync_1.loadRecipe)(options.file ?? "", kind.schema);
95
95
  const plan = await (0, sync_1.syncDiff)(kind, recipe, { kind: kind.name, id: recipeName(recipe) }, ctx);
96
96
  printPlan(logger, plan);
97
97
  });
@@ -110,7 +110,7 @@ const createPushCommand = () => {
110
110
  const logger = (0, cli_tasks_1.toLogger)(options);
111
111
  const kind = resolveKind(options.kind);
112
112
  const ctx = buildContext(options, logger);
113
- const recipe = (0, sync_1.loadRecipe)(options.file ?? "", kind.schema);
113
+ const recipe = await (0, sync_1.loadRecipe)(options.file ?? "", kind.schema);
114
114
  const mode = options.allowWrite ? "apply" : "what-if";
115
115
  const outcome = await (0, sync_1.syncPush)(kind, recipe, { kind: kind.name, id: recipeName(recipe) }, ctx, {
116
116
  mode,
@@ -70,7 +70,7 @@ const seedFromFile = async (options, logger) => {
70
70
  configPath,
71
71
  logger,
72
72
  };
73
- const recipe = (0, sync_1.loadRecipe)(options.file ?? "", recipe_1.brandKitKind.schema);
73
+ const recipe = await (0, sync_1.loadRecipe)(options.file ?? "", recipe_1.brandKitKind.schema);
74
74
  const outcome = await (0, sync_1.syncPush)(recipe_1.brandKitKind, recipe, { kind: recipe_1.brandKitKind.name, id: recipe.name }, ctx, { mode: "apply", prune: false });
75
75
  if (options.format === "json") {
76
76
  process.stdout.write(JSON.stringify({ recipe: recipe.name, plan: outcome.plan, result: outcome.result }, null, 2) +