@sitecoreai-labs/sitecoreai-cli 0.2.0 → 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.
@@ -2,19 +2,23 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.registerBriefRecipeTools = void 0;
4
4
  /**
5
- * Brief-type recipe surface — the MCP projection of the `brief-type`
6
- * recipe kind. Two workflow-shaped tools:
5
+ * Brief recipe surface — the MCP projection of both brief recipe kinds.
6
+ * Two workflow-shaped tools, each discriminating on `kind`:
7
7
  *
8
8
  * - `brief_recipe_inspect` — read. `verb=pull` captures a live brief
9
- * type as a declarative recipe; `verb=diff` plans the convergence of
10
- * a brief type onto a given recipe. Neither writes.
9
+ * type OR brief instance as a declarative recipe; `verb=diff` plans
10
+ * the convergence of the named resource onto a given recipe.
11
11
  *
12
- * - `brief_recipe_push` — write. Converges a brief type onto a recipe,
13
- * gated by `allowWrite`; `whatIf` returns the plan without writing.
12
+ * - `brief_recipe_push` — write. Converges a brief type or brief
13
+ * instance onto a recipe, gated by `allowWrite`; `whatIf` returns
14
+ * the plan without writing.
14
15
  *
15
- * The `recipe` input field IS `BriefTypeRecipeSchema` the single
16
- * source of truth feeds the agent-facing tool surface for free. See
17
- * docs/recipe-sync-architecture.md.
16
+ * `kind: 'brief-type'` is the legacy default (kept for back-compat with
17
+ * the pre-instance surface). `kind: 'brief'` operates on populated
18
+ * brief instances. The matching recipe schema becomes the input shape
19
+ * for `recipe` automatically — the schema is the single source of truth.
20
+ *
21
+ * See docs/recipe-sync-architecture.md.
18
22
  */
19
23
  const zod_1 = require("zod");
20
24
  const recipe_1 = require("../../brief/recipe");
@@ -31,53 +35,81 @@ const planSummaryText = (plan) => {
31
35
  const tally = (0, sync_1.summarizePlan)(plan);
32
36
  return `Plan: ${tally.create} create, ${tally.update} update, ${tally.delete} delete, ${tally.noop} unchanged.`;
33
37
  };
38
+ /** Map the MCP `kind` discriminator to the underlying `RecipeKind`. */
39
+ const recipeKindFor = (kind) => {
40
+ const resolved = kind ?? "brief-type";
41
+ return resolved === "brief"
42
+ ? recipe_1.briefInstanceKind
43
+ : recipe_1.briefTypeKind;
44
+ };
34
45
  const registerBriefRecipeTools = (registry) => {
35
46
  registry.registerTool({
36
47
  name: "brief_recipe_inspect",
37
48
  description: descriptions_1.TOOL_DESCRIPTIONS.brief_recipe_inspect,
38
49
  auth: "read",
39
50
  annotations: {
40
- title: "Pull or diff a brief type as a declarative recipe",
51
+ title: "Pull or diff a brief type or brief instance as a declarative recipe",
41
52
  readOnlyHint: true,
42
53
  destructiveHint: false,
43
54
  openWorldHint: true,
44
55
  },
45
56
  inputSchema: {
57
+ kind: zod_1.z
58
+ .enum(["brief-type", "brief"])
59
+ .default("brief-type")
60
+ .describe("Which recipe kind to operate on. 'brief-type' (default — the schema template) or 'brief' (a populated brief instance)."),
46
61
  verb: zod_1.z
47
62
  .enum(["pull", "diff"])
48
- .describe("pull: capture the live brief type named `name` as a recipe. diff: compare `recipe` against the live brief type and return the plan."),
49
- name: zod_1.z.string().optional().describe("Brief type codename. Required for verb='pull'."),
50
- recipe: recipe_1.briefTypeKind.schema
63
+ .describe("pull: capture the live resource named `name` as a recipe. diff: compare `recipe` against the live resource and return the plan."),
64
+ name: zod_1.z
65
+ .string()
66
+ .optional()
67
+ .describe("Brief-type codename or brief display name. Required for verb='pull'."),
68
+ recipe: zod_1.z
69
+ .union([recipe_1.briefTypeKind.schema, recipe_1.briefInstanceKind.schema])
51
70
  .optional()
52
- .describe("A brief-type recipe. Required for verb='diff'."),
71
+ .describe("A brief-type recipe or brief-instance recipe (matching `kind`). Required for verb='diff'."),
53
72
  ...common_1.environmentBindingShape,
54
73
  },
55
74
  handler: async (input, context) => {
56
75
  const ctx = syncContextFrom(context, input.environmentName);
76
+ const kind = recipeKindFor(input.kind);
77
+ const humanKind = input.kind === "brief" ? "brief" : "brief type";
57
78
  if (input.verb === "pull") {
58
79
  if (!input.name) {
59
80
  throw (0, errors_1.createScaiError)("verb='pull' requires `name`.", "INPUT_INVALID");
60
81
  }
61
- const recipe = await (0, sync_1.syncPull)(recipe_1.briefTypeKind, { kind: recipe_1.briefTypeKind.name, id: input.name }, ctx);
82
+ const recipe = await (0, sync_1.syncPull)(kind, { kind: kind.name, id: input.name }, ctx);
62
83
  return {
63
84
  content: [
64
85
  {
65
86
  type: "text",
66
87
  text: recipe
67
- ? `Captured "${input.name}" as a recipe.`
68
- : `No brief type named "${input.name}".`,
88
+ ? `Captured ${humanKind} "${input.name}" as a recipe.`
89
+ : `No ${humanKind} named "${input.name}".`,
69
90
  },
70
91
  ],
71
- structuredContent: { verb: input.verb, found: recipe !== null, recipe },
92
+ structuredContent: {
93
+ kind: input.kind ?? "brief-type",
94
+ verb: input.verb,
95
+ found: recipe !== null,
96
+ recipe,
97
+ },
72
98
  };
73
99
  }
74
100
  if (!input.recipe) {
75
101
  throw (0, errors_1.createScaiError)("verb='diff' requires `recipe`.", "INPUT_INVALID");
76
102
  }
77
- const plan = await (0, sync_1.syncDiff)(recipe_1.briefTypeKind, input.recipe, { kind: recipe_1.briefTypeKind.name, id: input.recipe.name }, ctx);
103
+ const recipe = input.recipe;
104
+ const plan = await (0, sync_1.syncDiff)(kind, recipe, { kind: kind.name, id: recipe.name }, ctx);
78
105
  return {
79
106
  content: [{ type: "text", text: planSummaryText(plan) }],
80
- structuredContent: { verb: input.verb, plan, summary: (0, sync_1.summarizePlan)(plan) },
107
+ structuredContent: {
108
+ kind: input.kind ?? "brief-type",
109
+ verb: input.verb,
110
+ plan,
111
+ summary: (0, sync_1.summarizePlan)(plan),
112
+ },
81
113
  };
82
114
  },
83
115
  });
@@ -86,13 +118,19 @@ const registerBriefRecipeTools = (registry) => {
86
118
  description: descriptions_1.TOOL_DESCRIPTIONS.brief_recipe_push,
87
119
  auth: "write",
88
120
  annotations: {
89
- title: "Push a brief-type recipe — converge the type onto the recipe",
121
+ title: "Push a brief-type or brief recipe — converge the resource onto the recipe",
90
122
  readOnlyHint: false,
91
123
  destructiveHint: true,
92
124
  openWorldHint: true,
93
125
  },
94
126
  inputSchema: {
95
- recipe: recipe_1.briefTypeKind.schema.describe("The brief-type recipe to converge onto. The brief type is identified by `recipe.name`."),
127
+ kind: zod_1.z
128
+ .enum(["brief-type", "brief"])
129
+ .default("brief-type")
130
+ .describe("Which recipe kind to push. 'brief-type' (default — the schema template) or 'brief' (a populated brief instance)."),
131
+ recipe: zod_1.z
132
+ .union([recipe_1.briefTypeKind.schema, recipe_1.briefInstanceKind.schema])
133
+ .describe("The recipe to converge onto. Schema must match `kind`. The resource is identified by `recipe.name`."),
96
134
  prune: zod_1.z
97
135
  .boolean()
98
136
  .default(false)
@@ -103,14 +141,20 @@ const registerBriefRecipeTools = (registry) => {
103
141
  },
104
142
  handler: async (input, context, extra) => {
105
143
  const ctx = syncContextFrom(context, input.environmentName, extra.signal);
144
+ const kind = recipeKindFor(input.kind);
145
+ const recipe = input.recipe;
106
146
  const mode = input.whatIf ? "what-if" : "apply";
107
- const outcome = await (0, sync_1.syncPush)(recipe_1.briefTypeKind, input.recipe, { kind: recipe_1.briefTypeKind.name, id: input.recipe.name }, ctx, { mode, prune: input.prune });
147
+ const outcome = await (0, sync_1.syncPush)(kind, recipe, { kind: kind.name, id: recipe.name }, ctx, {
148
+ mode,
149
+ prune: input.prune,
150
+ });
108
151
  const text = outcome.result
109
152
  ? `Applied ${outcome.result.applied.length} change(s); ${outcome.result.skipped.length} skipped.`
110
153
  : planSummaryText(outcome.plan);
111
154
  return {
112
155
  content: [{ type: "text", text }],
113
156
  structuredContent: {
157
+ kind: input.kind ?? "brief-type",
114
158
  mode,
115
159
  plan: outcome.plan,
116
160
  summary: (0, sync_1.summarizePlan)(outcome.plan),
@@ -186,21 +186,21 @@ const registerBriefTools = (registry) => {
186
186
  .describe("Which Brief resource the verb targets. 'brief-type' supports create/update/delete; 'brief' supports set-status/delete; 'comment' supports create."),
187
187
  verb: zod_1.z
188
188
  .enum(["create", "update", "delete", "set-status"])
189
- .describe("Mutation verb. brief-type: create (POST), update (PUT-replace), delete (irreversible). brief: set-status, delete. comment: create."),
189
+ .describe("Mutation verb. brief-type: create (POST), update (PUT-replace), delete (irreversible). brief: create (POST), update (partial PUT), set-status, delete. comment: create."),
190
190
  briefTypeId: zod_1.z
191
191
  .string()
192
192
  .uuid()
193
193
  .optional()
194
- .describe("Brief type UUID. Required for brief-type verb='update' and verb='delete'."),
194
+ .describe("Brief type UUID. Required for brief-type verb='update' and verb='delete', and for brief verb='create' (the type to build the brief against)."),
195
195
  briefId: zod_1.z
196
196
  .string()
197
197
  .uuid()
198
198
  .optional()
199
- .describe("Brief UUID. Required for resource='brief' verb='set-status'."),
199
+ .describe("Brief UUID. Required for resource='brief' verbs 'update', 'set-status', and 'delete'."),
200
200
  status: zod_1.z
201
201
  .enum(["Draft", "InReview", "Approved", "Canceled", "Archived"])
202
202
  .optional()
203
- .describe("Target brief status for verb='set-status'. Wire form — 'InReview' is the 'In Review' UI label. A brief must leave 'Draft' before it can be linked to a campaign."),
203
+ .describe("Target brief status. Required for verb='set-status'; optional on brief 'create'/'update' (defaults to server 'Draft' on create). Wire form — 'InReview' is the 'In Review' UI label. A brief must leave 'Draft' before it can be linked to a campaign."),
204
204
  commentText: zod_1.z
205
205
  .string()
206
206
  .optional()
@@ -221,7 +221,19 @@ const registerBriefTools = (registry) => {
221
221
  .describe("Field definitions (RichText | DateTime | Timeline | Budget). Use brief_inspect verb='type' on an existing type for a worked example."),
222
222
  })
223
223
  .optional()
224
- .describe("Full BriefType body for verb='create' or verb='update'. Required on writes; ignored on delete."),
224
+ .describe("Full BriefType body for resource='brief-type' verb='create' or verb='update'. Required on brief-type writes; ignored on delete."),
225
+ brief: zod_1.z
226
+ .object({
227
+ name: zod_1.z.string().min(1).describe("Brief display name."),
228
+ locale: zod_1.z.string().optional().describe("BCP-47-ish locale, e.g. 'en-us'."),
229
+ fields: zod_1.z
230
+ .record(zod_1.z.string(), zod_1.z.unknown())
231
+ .optional()
232
+ .describe("Field values keyed by BriefField.name; the per-field shape follows the brief type's field definitions."),
233
+ isTemplate: zod_1.z.boolean().optional().describe("Whether the brief is a template."),
234
+ })
235
+ .optional()
236
+ .describe("Brief body. Required for resource='brief' verb='create' (with `briefTypeId`); optional for verb='update' (any subset of name/locale/fields/isTemplate plus the top-level `status`)."),
225
237
  ...common_1.environmentBindingShape,
226
238
  ...common_1.allowWriteShape,
227
239
  ...common_1.whatIfShape,
@@ -230,6 +242,71 @@ const registerBriefTools = (registry) => {
230
242
  const taskOpts = baseTaskOptions(context.configPath, input.environmentName ?? context.envName);
231
243
  const whatIf = input.whatIf;
232
244
  if (input.resource === "brief") {
245
+ if (input.verb === "create") {
246
+ if (!input.brief) {
247
+ throw (0, errors_1.createScaiError)("verb='create' on resource='brief' requires `brief`.", "INPUT_INVALID");
248
+ }
249
+ if (!input.briefTypeId) {
250
+ throw (0, errors_1.createScaiError)("verb='create' on resource='brief' requires `briefTypeId`.", "INPUT_INVALID");
251
+ }
252
+ const createInput = {
253
+ name: input.brief.name,
254
+ briefTypeId: input.briefTypeId,
255
+ ...(input.brief.locale !== undefined && { locale: input.brief.locale }),
256
+ ...(input.brief.fields !== undefined && { fields: input.brief.fields }),
257
+ ...(input.brief.isTemplate !== undefined && { isTemplate: input.brief.isTemplate }),
258
+ };
259
+ const result = await (0, tasks_1.runBriefCreate)({
260
+ ...taskOpts,
261
+ input: createInput,
262
+ whatIf,
263
+ });
264
+ const isPlan = "plan" in result;
265
+ return {
266
+ content: [
267
+ {
268
+ type: "text",
269
+ text: isPlan
270
+ ? `Plan: create brief '${input.brief.name}'.`
271
+ : `Created brief '${input.brief.name}'.`,
272
+ },
273
+ ],
274
+ structuredContent: { resource: input.resource, verb: input.verb, result },
275
+ };
276
+ }
277
+ if (input.verb === "update") {
278
+ if (!input.briefId) {
279
+ throw (0, errors_1.createScaiError)("verb='update' on resource='brief' requires `briefId`.", "INPUT_INVALID");
280
+ }
281
+ const patch = {
282
+ ...(input.brief?.name !== undefined && { name: input.brief.name }),
283
+ ...(input.brief?.locale !== undefined && { locale: input.brief.locale }),
284
+ ...(input.brief?.fields !== undefined && { fields: input.brief.fields }),
285
+ ...(input.brief?.isTemplate !== undefined && { isTemplate: input.brief.isTemplate }),
286
+ ...(input.status !== undefined && { status: input.status }),
287
+ };
288
+ if (Object.keys(patch).length === 0) {
289
+ throw (0, errors_1.createScaiError)("verb='update' on resource='brief' requires at least one field in `brief` or a top-level `status`.", "INPUT_INVALID");
290
+ }
291
+ const result = await (0, tasks_1.runBriefUpdate)({
292
+ ...taskOpts,
293
+ briefId: input.briefId,
294
+ patch,
295
+ whatIf,
296
+ });
297
+ const isPlan = "plan" in result;
298
+ return {
299
+ content: [
300
+ {
301
+ type: "text",
302
+ text: isPlan
303
+ ? `Plan: update brief ${input.briefId} (${Object.keys(patch).join(", ")}).`
304
+ : `Updated brief ${input.briefId}.`,
305
+ },
306
+ ],
307
+ structuredContent: { resource: input.resource, verb: input.verb, result },
308
+ };
309
+ }
233
310
  if (input.verb === "set-status") {
234
311
  if (!input.briefId) {
235
312
  throw (0, errors_1.createScaiError)("verb='set-status' requires `briefId`.", "INPUT_INVALID");
@@ -277,7 +354,7 @@ const registerBriefTools = (registry) => {
277
354
  structuredContent: { resource: input.resource, verb: input.verb, result },
278
355
  };
279
356
  }
280
- throw (0, errors_1.createScaiError)("resource='brief' supports verb='set-status' and verb='delete'.", "INPUT_INVALID");
357
+ throw (0, errors_1.createScaiError)("resource='brief' supports verbs 'create', 'update', 'set-status', and 'delete'.", "INPUT_INVALID");
281
358
  }
282
359
  if (input.resource === "comment") {
283
360
  if (input.verb !== "create") {
@@ -29,4 +29,5 @@ const recipe_2 = require("../brief/recipe");
29
29
  exports.ENUMERABLE_RECIPE_KINDS = [
30
30
  recipe_1.brandKitKind,
31
31
  recipe_2.briefTypeKind,
32
+ recipe_2.briefInstanceKind,
32
33
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sitecoreai-labs/sitecoreai-cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "SitecoreAI developer toolkit — a native TypeScript CLI, SDK, and MCP server for deploy, serialization, recipes, publishing, and content operations.",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",