@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
@@ -77,7 +77,7 @@ const createDiffCommand = () => {
77
77
  command.action(async (options) => {
78
78
  const logger = (0, cli_tasks_1.toLogger)(options);
79
79
  const ctx = buildContext(options, logger);
80
- const recipe = (0, sync_1.loadRecipe)(options.file ?? "", recipe_1.brandKitKind.schema);
80
+ const recipe = await (0, sync_1.loadRecipe)(options.file ?? "", recipe_1.brandKitKind.schema);
81
81
  const plan = await (0, sync_1.syncDiff)(recipe_1.brandKitKind, recipe, { kind: recipe_1.brandKitKind.name, id: recipe.name }, ctx);
82
82
  printPlan(logger, plan);
83
83
  if (hasPaidPipeline(plan)) {
@@ -98,7 +98,7 @@ const createPushCommand = () => {
98
98
  command.action(async (options) => {
99
99
  const logger = (0, cli_tasks_1.toLogger)(options);
100
100
  const ctx = buildContext(options, logger);
101
- const recipe = (0, sync_1.loadRecipe)(options.file ?? "", recipe_1.brandKitKind.schema);
101
+ const recipe = await (0, sync_1.loadRecipe)(options.file ?? "", recipe_1.brandKitKind.schema);
102
102
  const mode = options.allowWrite ? "apply" : "what-if";
103
103
  const outcome = await (0, sync_1.syncPush)(recipe_1.brandKitKind, recipe, { kind: recipe_1.brandKitKind.name, id: recipe.name }, ctx, { mode, prune: options.prune });
104
104
  printPlan(logger, outcome.plan);
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const createBriefCreateCommand: () => Command;
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createBriefCreateCommand = void 0;
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const commander_1 = require("commander");
9
+ const brief_1 = require("../../brief");
10
+ const tasks_1 = require("../../brief/tasks");
11
+ const cli_tasks_1 = require("../../shared/cli-tasks");
12
+ const shared_1 = require("../shared");
13
+ /**
14
+ * `scai ops brief create` — create a brief instance from a JSON
15
+ * document matching `CreateBriefInput`. Mirrors `brief types create`:
16
+ * imperative one-shot, dry-runs by default, --apply to write.
17
+ *
18
+ * The JSON body matches the raw API shape:
19
+ * {
20
+ * "name": "Q3 Launch Brief",
21
+ * "briefTypeId": "<uuid-from `brief types list`>",
22
+ * "locale": "en-us", // optional
23
+ * "fields": { ... }, // optional, per-field shapes vary by brief type
24
+ * "isTemplate": false // optional
25
+ * }
26
+ *
27
+ * For a declarative (codename-based, idempotent) push, use
28
+ * `scai ops brief sync push --kind brief --file <recipe>` instead.
29
+ */
30
+ const readJsonFile = (path) => {
31
+ try {
32
+ return JSON.parse(node_fs_1.default.readFileSync(path, "utf8"));
33
+ }
34
+ catch (error) {
35
+ throw (0, cli_tasks_1.inputError)(`Could not read JSON from ${path}: ${error instanceof Error ? error.message : String(error)}`, "Pass --file <path> pointing at a valid CreateBriefInput JSON document.");
36
+ }
37
+ };
38
+ const createBriefCreateCommand = () => {
39
+ const command = new commander_1.Command("create")
40
+ .description("Create a brief instance from a CreateBriefInput JSON document. For declarative + idempotent pushes, use `brief sync push --kind brief`.")
41
+ .addOption(new commander_1.Option("-f, --file <path>", "Path to a JSON file matching CreateBriefInput (name + briefTypeId, plus optional locale/fields/isTemplate).").makeOptionMandatory(true));
42
+ (0, shared_1.addOrgScopeOptions)(command);
43
+ (0, shared_1.addConfigOption)(command);
44
+ (0, shared_1.addVerbosityOptions)(command);
45
+ (0, shared_1.addApplyOption)(command);
46
+ (0, shared_1.addWhatIfOption)(command);
47
+ command.action((0, shared_1.withApplyGate)(async (options) => {
48
+ const input = (0, brief_1.assertCreateBriefInput)(readJsonFile(options.file));
49
+ await (0, tasks_1.runBriefCreate)({ ...options, input });
50
+ }));
51
+ command.addHelpText("after", "\nExamples:\n" +
52
+ " $ scai ops brief create -f ./brief.json -n agents --apply\n" +
53
+ " $ scai ops brief create -f ./brief.json -n agents # dry run\n");
54
+ return command;
55
+ };
56
+ exports.createBriefCreateCommand = createBriefCreateCommand;
@@ -6,10 +6,12 @@ import { Command } from "commander";
6
6
  * Surface:
7
7
  * - `scai ops brief list` — list briefs in the tenant
8
8
  * - `scai ops brief show <briefId>` — read one brief in detail
9
+ * - `scai ops brief create -f <file>` — create a brief from CreateBriefInput JSON
10
+ * - `scai ops brief update <briefId>` — partial-PUT update (file or --status)
9
11
  * - `scai ops brief set-status <briefId> <status>` — move a brief's workflow status
10
12
  * - `scai ops brief delete <briefId>` — delete a brief
11
13
  * - `scai ops brief types {list,get,create,update,delete}` — brief type CRUD
12
- * - `scai ops brief sync {pull,diff,push}` — brief type as a recipe
14
+ * - `scai ops brief sync {pull,diff,push} [--kind brief|brief-type]` recipe sync
13
15
  * - `scai ops brief todos [briefId]` — list to-dos
14
16
  * - `scai ops brief comments {list,add}` — list / post comments
15
17
  *
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createBriefCommand = void 0;
4
4
  const commander_1 = require("commander");
5
+ const create_1 = require("./create");
5
6
  const list_1 = require("./list");
6
7
  const show_1 = require("./show");
7
8
  const set_status_1 = require("./set-status");
@@ -10,6 +11,7 @@ const todos_1 = require("./todos");
10
11
  const comments_1 = require("./comments");
11
12
  const delete_1 = require("./delete");
12
13
  const sync_1 = require("./sync");
14
+ const update_1 = require("./update");
13
15
  /**
14
16
  * `scai ops brief …` command family — Sitecore Content Operations Brief
15
17
  * API (`co-brief-api-<region>.sitecorecloud.io`).
@@ -17,10 +19,12 @@ const sync_1 = require("./sync");
17
19
  * Surface:
18
20
  * - `scai ops brief list` — list briefs in the tenant
19
21
  * - `scai ops brief show <briefId>` — read one brief in detail
22
+ * - `scai ops brief create -f <file>` — create a brief from CreateBriefInput JSON
23
+ * - `scai ops brief update <briefId>` — partial-PUT update (file or --status)
20
24
  * - `scai ops brief set-status <briefId> <status>` — move a brief's workflow status
21
25
  * - `scai ops brief delete <briefId>` — delete a brief
22
26
  * - `scai ops brief types {list,get,create,update,delete}` — brief type CRUD
23
- * - `scai ops brief sync {pull,diff,push}` — brief type as a recipe
27
+ * - `scai ops brief sync {pull,diff,push} [--kind brief|brief-type]` recipe sync
24
28
  * - `scai ops brief todos [briefId]` — list to-dos
25
29
  * - `scai ops brief comments {list,add}` — list / post comments
26
30
  *
@@ -31,6 +35,8 @@ const createBriefCommand = () => {
31
35
  const command = new commander_1.Command("brief").description("Briefs, brief types, to-dos, and comments on the Sitecore Content Operations Brief API.");
32
36
  command.addCommand((0, list_1.createBriefListCommand)());
33
37
  command.addCommand((0, show_1.createBriefShowCommand)());
38
+ command.addCommand((0, create_1.createBriefCreateCommand)());
39
+ command.addCommand((0, update_1.createBriefUpdateCommand)());
34
40
  command.addCommand((0, set_status_1.createBriefSetStatusCommand)());
35
41
  command.addCommand((0, delete_1.createBriefDeleteCommand)());
36
42
  command.addCommand((0, types_1.createBriefTypesCommand)());
@@ -40,11 +46,15 @@ const createBriefCommand = () => {
40
46
  command.addHelpText("after", "\nExamples:\n" +
41
47
  " $ scai ops brief list -n agents # list briefs\n" +
42
48
  " $ scai ops brief show <briefId> -n agents # read one brief\n" +
49
+ " $ scai ops brief create -f b.json -n agents --apply # create a brief (raw API shape)\n" +
50
+ " $ scai ops brief update <id> --status Approved --apply # status-only patch\n" +
43
51
  " $ scai ops brief set-status <briefId> Approved --apply # move out of Draft\n" +
44
52
  " $ scai ops brief delete <briefId> --apply --force # delete a brief\n" +
45
53
  " $ scai ops brief types list -n agents # list brief schemas\n" +
46
54
  " $ scai ops brief types create -f t.json --apply # create a new schema\n" +
47
55
  " $ scai ops brief sync pull --name CreativeBrief # capture a type as a recipe\n" +
56
+ " $ scai ops brief sync pull --kind brief --name MyBrief # capture a brief as a recipe\n" +
57
+ " $ scai ops brief sync push --kind brief -f b.yaml --allow-write # converge a brief\n" +
48
58
  " $ scai ops brief todos <briefId> --assignees # to-dos on a brief, with assignees\n" +
49
59
  " $ scai ops brief comments list <briefId> # comments on a brief\n" +
50
60
  ' $ scai ops brief comments add <briefId> --text "…" --apply # post a comment\n');
@@ -1,12 +1,15 @@
1
1
  /**
2
- * `scai ops brief sync` — pull, diff, and push a brief type as a
3
- * declarative recipe. The recipe / sync model — see
4
- * docs/recipe-sync-architecture.md.
2
+ * `scai ops brief sync` — pull, diff, and push a brief type OR a brief
3
+ * instance as a declarative recipe. See docs/recipe-sync-architecture.md.
5
4
  *
6
- * pull capture a live brief type into a recipe file
7
- * diff show the plan to converge a brief type onto a recipe
5
+ * pull capture a live brief type or brief as a recipe file
6
+ * diff show the plan to converge a brief type or brief onto a recipe
8
7
  * push apply that plan (dry-run unless --allow-write)
8
+ *
9
+ * Both verbs default to `--kind brief-type` for back-compat with the
10
+ * pre-instance surface — the same flag distinguishes `briefTypeKind`
11
+ * (the schema template) from `briefInstanceKind` (a populated brief).
9
12
  */
10
13
  import { Command } from "commander";
11
- /** `scai ops brief sync` — the recipe pull / diff / push verbs for brief types. */
14
+ /** `scai ops brief sync` — the recipe pull / diff / push verbs. */
12
15
  export declare const createBriefSyncCommand: () => Command;
@@ -2,13 +2,16 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createBriefSyncCommand = void 0;
4
4
  /**
5
- * `scai ops brief sync` — pull, diff, and push a brief type as a
6
- * declarative recipe. The recipe / sync model — see
7
- * docs/recipe-sync-architecture.md.
5
+ * `scai ops brief sync` — pull, diff, and push a brief type OR a brief
6
+ * instance as a declarative recipe. See docs/recipe-sync-architecture.md.
8
7
  *
9
- * pull capture a live brief type into a recipe file
10
- * diff show the plan to converge a brief type onto a recipe
8
+ * pull capture a live brief type or brief as a recipe file
9
+ * diff show the plan to converge a brief type or brief onto a recipe
11
10
  * push apply that plan (dry-run unless --allow-write)
11
+ *
12
+ * Both verbs default to `--kind brief-type` for back-compat with the
13
+ * pre-instance surface — the same flag distinguishes `briefTypeKind`
14
+ * (the schema template) from `briefInstanceKind` (a populated brief).
12
15
  */
13
16
  const commander_1 = require("commander");
14
17
  const shared_1 = require("../shared");
@@ -16,11 +19,28 @@ const recipe_1 = require("../../brief/recipe");
16
19
  const root_config_1 = require("../../config/root-config");
17
20
  const cli_tasks_1 = require("../../shared/cli-tasks");
18
21
  const sync_1 = require("../../sync");
19
- /** Slugify a brief-type name for a default recipe filename. */
20
- const slug = (value) => value
22
+ const BRIEF_SYNC_KINDS = ["brief-type", "brief"];
23
+ /** Slugify a recipe name for a default filename. */
24
+ const slug = (value, fallback) => value
21
25
  .toLowerCase()
22
26
  .replace(/[^a-z0-9]+/g, "-")
23
- .replace(/^-+|-+$/g, "") || "brief-type";
27
+ .replace(/^-+|-+$/g, "") || fallback;
28
+ /** Per-kind metadata — wires the discriminator to the right kind + filename suffix. */
29
+ const kindFor = (kind) => {
30
+ const resolved = kind ?? "brief-type";
31
+ if (resolved === "brief") {
32
+ return {
33
+ recipeKind: recipe_1.briefInstanceKind,
34
+ suffix: "brief.yaml",
35
+ humanName: "brief",
36
+ };
37
+ }
38
+ return {
39
+ recipeKind: recipe_1.briefTypeKind,
40
+ suffix: "brieftype.yaml",
41
+ humanName: "brief type",
42
+ };
43
+ };
24
44
  /** Build the `SyncContext` for a brief sync command invocation. */
25
45
  const buildContext = (options, logger) => {
26
46
  const configPath = options.config ?? process.cwd();
@@ -44,23 +64,31 @@ const printPlan = (logger, plan) => {
44
64
  const tally = (0, sync_1.summarizePlan)(plan);
45
65
  logger.info(`Plan: ${tally.create} create, ${tally.update} update, ${tally.delete} delete, ${tally.noop} unchanged.`);
46
66
  };
67
+ /** `--kind` is shared across all three verbs. */
68
+ const addKindOption = (command) => command.addOption(new commander_1.Option("--kind <kind>", "Recipe kind to operate on. Defaults to brief-type for back-compat.")
69
+ .choices(BRIEF_SYNC_KINDS)
70
+ .default("brief-type"));
47
71
  const createPullCommand = () => {
48
72
  const command = new commander_1.Command("pull")
49
- .description("Capture a live brief type as a recipe file.")
50
- .requiredOption("--name <name>", "Brief type codename")
51
- .addOption(new commander_1.Option("--file <path>", "Output recipe file (default: <name>.brieftype.yaml)"));
73
+ .description("Capture a live brief type or brief instance as a recipe file.")
74
+ .requiredOption("--name <name>", "Identifier of the recipe. Brief-type codename (`CreativeBrief`) or brief display name (`Q3 Launch`).")
75
+ .addOption(new commander_1.Option("--file <path>", "Output recipe file (default: <name>.<kind>.yaml)"));
76
+ addKindOption(command);
52
77
  (0, shared_1.addEnvironmentOption)(command);
53
78
  (0, shared_1.addConfigOption)(command);
54
79
  (0, shared_1.addVerbosityOptions)(command);
55
80
  command.action(async (options) => {
56
81
  const logger = (0, cli_tasks_1.toLogger)(options);
57
82
  const ctx = buildContext(options, logger);
83
+ const { recipeKind, suffix, humanName } = kindFor(options.kind);
58
84
  const name = options.name ?? "";
59
- const recipe = await (0, sync_1.syncPull)(recipe_1.briefTypeKind, { kind: recipe_1.briefTypeKind.name, id: name }, ctx);
85
+ const recipe = await (0, sync_1.syncPull)(recipeKind, { kind: recipeKind.name, id: name }, ctx);
60
86
  if (!recipe) {
61
- throw (0, cli_tasks_1.inputError)(`Brief type "${name}" not found.`, "List brief types with `scai ops brief types list`.");
87
+ throw (0, cli_tasks_1.inputError)(`${humanName.charAt(0).toUpperCase()}${humanName.slice(1)} "${name}" not found.`, recipeKind === recipe_1.briefTypeKind
88
+ ? "List brief types with `scai ops brief types list`."
89
+ : "List briefs with `scai ops brief list`.");
62
90
  }
63
- const file = options.file ?? `${slug(name)}.brieftype.yaml`;
91
+ const file = options.file ?? `${slug(name, humanName.replace(/\s+/g, "-"))}.${suffix}`;
64
92
  (0, sync_1.writeRecipe)(file, recipe);
65
93
  logger.info(`Pulled "${name}" -> ${file}`, "green");
66
94
  });
@@ -68,35 +96,39 @@ const createPullCommand = () => {
68
96
  };
69
97
  const createDiffCommand = () => {
70
98
  const command = new commander_1.Command("diff")
71
- .description("Show the plan to converge a brief type onto a recipe file.")
99
+ .description("Show the plan to converge a brief type or brief onto a recipe file.")
72
100
  .requiredOption("--file <path>", "Recipe file (.yaml / .json)");
101
+ addKindOption(command);
73
102
  (0, shared_1.addEnvironmentOption)(command);
74
103
  (0, shared_1.addConfigOption)(command);
75
104
  (0, shared_1.addVerbosityOptions)(command);
76
105
  command.action(async (options) => {
77
106
  const logger = (0, cli_tasks_1.toLogger)(options);
78
107
  const ctx = buildContext(options, logger);
79
- const recipe = (0, sync_1.loadRecipe)(options.file ?? "", recipe_1.briefTypeKind.schema);
80
- const plan = await (0, sync_1.syncDiff)(recipe_1.briefTypeKind, recipe, { kind: recipe_1.briefTypeKind.name, id: recipe.name }, ctx);
108
+ const { recipeKind } = kindFor(options.kind);
109
+ const recipe = (await (0, sync_1.loadRecipe)(options.file ?? "", recipeKind.schema));
110
+ const plan = await (0, sync_1.syncDiff)(recipeKind, recipe, { kind: recipeKind.name, id: recipe.name }, ctx);
81
111
  printPlan(logger, plan);
82
112
  });
83
113
  return command;
84
114
  };
85
115
  const createPushCommand = () => {
86
116
  const command = new commander_1.Command("push")
87
- .description("Converge a brief type onto a recipe file. Dry-run unless --allow-write.")
117
+ .description("Converge a brief type or brief onto a recipe file. Dry-run unless --allow-write.")
88
118
  .requiredOption("--file <path>", "Recipe file (.yaml / .json)")
89
119
  .addOption(new commander_1.Option("--allow-write", "Apply the plan (default is a dry-run)"))
90
120
  .addOption(new commander_1.Option("--prune", "Include delete changes (off by default)"));
121
+ addKindOption(command);
91
122
  (0, shared_1.addEnvironmentOption)(command);
92
123
  (0, shared_1.addConfigOption)(command);
93
124
  (0, shared_1.addVerbosityOptions)(command);
94
125
  command.action(async (options) => {
95
126
  const logger = (0, cli_tasks_1.toLogger)(options);
96
127
  const ctx = buildContext(options, logger);
97
- const recipe = (0, sync_1.loadRecipe)(options.file ?? "", recipe_1.briefTypeKind.schema);
128
+ const { recipeKind } = kindFor(options.kind);
129
+ const recipe = (await (0, sync_1.loadRecipe)(options.file ?? "", recipeKind.schema));
98
130
  const mode = options.allowWrite ? "apply" : "what-if";
99
- const outcome = await (0, sync_1.syncPush)(recipe_1.briefTypeKind, recipe, { kind: recipe_1.briefTypeKind.name, id: recipe.name }, ctx, { mode, prune: options.prune });
131
+ const outcome = await (0, sync_1.syncPush)(recipeKind, recipe, { kind: recipeKind.name, id: recipe.name }, ctx, { mode, prune: options.prune });
100
132
  printPlan(logger, outcome.plan);
101
133
  if (outcome.result) {
102
134
  logger.info(`Applied ${outcome.result.applied.length} change(s); ${outcome.result.skipped.length} skipped.`, "green");
@@ -110,9 +142,9 @@ const createPushCommand = () => {
110
142
  });
111
143
  return command;
112
144
  };
113
- /** `scai ops brief sync` — the recipe pull / diff / push verbs for brief types. */
145
+ /** `scai ops brief sync` — the recipe pull / diff / push verbs. */
114
146
  const createBriefSyncCommand = () => {
115
- const command = new commander_1.Command("sync").description("Pull, diff, and push a brief type as a declarative recipe.");
147
+ const command = new commander_1.Command("sync").description("Pull, diff, and push a brief type or brief instance as a declarative recipe.");
116
148
  command.addCommand(createPullCommand());
117
149
  command.addCommand(createDiffCommand());
118
150
  command.addCommand(createPushCommand());
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const createBriefUpdateCommand: () => Command;
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createBriefUpdateCommand = void 0;
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const commander_1 = require("commander");
9
+ const tasks_1 = require("../../brief/tasks");
10
+ const cli_tasks_1 = require("../../shared/cli-tasks");
11
+ const errors_1 = require("../../shared/errors");
12
+ const shared_1 = require("../shared");
13
+ /**
14
+ * `scai ops brief update <briefId>` — partial-PUT update of a brief
15
+ * instance. The JSON body is `Partial<CreateBriefInput> & { status? }`:
16
+ * any subset of `name`, `locale`, `fields`, `isTemplate`, plus an
17
+ * optional `status` workflow move. Read first if you only want to
18
+ * change one field — the PUT is partial but applies whatever keys it
19
+ * receives.
20
+ *
21
+ * Use `scai ops brief set-status` for status-only moves (already wired)
22
+ * or this command's `--status <s>` flag as a shortcut that bypasses the
23
+ * `--file` requirement.
24
+ */
25
+ const KNOWN_STATUSES = [
26
+ "Draft",
27
+ "InReview",
28
+ "Approved",
29
+ "Canceled",
30
+ "Archived",
31
+ ];
32
+ const readJsonFile = (path) => {
33
+ try {
34
+ return JSON.parse(node_fs_1.default.readFileSync(path, "utf8"));
35
+ }
36
+ catch (error) {
37
+ throw (0, cli_tasks_1.inputError)(`Could not read JSON from ${path}: ${error instanceof Error ? error.message : String(error)}`, "Pass --file <path> pointing at a valid brief-update JSON document.");
38
+ }
39
+ };
40
+ const assertUpdateBody = (value) => {
41
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
42
+ throw (0, errors_1.createScaiError)("Brief update body must be a JSON object.", "INPUT_INVALID");
43
+ }
44
+ const obj = value;
45
+ if (obj.status !== undefined && !KNOWN_STATUSES.includes(obj.status)) {
46
+ throw (0, errors_1.createScaiError)(`Invalid 'status': ${JSON.stringify(obj.status)}.`, "INPUT_INVALID", {
47
+ hint: `Must be one of: ${KNOWN_STATUSES.join(", ")}.`,
48
+ });
49
+ }
50
+ if (obj.fields !== undefined && (typeof obj.fields !== "object" || Array.isArray(obj.fields))) {
51
+ throw (0, errors_1.createScaiError)("'fields' must be an object keyed by field name.", "INPUT_INVALID");
52
+ }
53
+ return obj;
54
+ };
55
+ const createBriefUpdateCommand = () => {
56
+ const command = new commander_1.Command("update")
57
+ .description("Update a brief instance with a partial-PUT body. Provide --file for arbitrary patches, or --status as a shortcut for a status-only move.")
58
+ .argument("<briefId>", "Brief UUID")
59
+ .addOption(new commander_1.Option("-f, --file <path>", "Path to a JSON file with the partial patch."))
60
+ .addOption(new commander_1.Option("--status <status>", "Shortcut: status-only patch. Equivalent to `scai ops brief set-status`.").choices(KNOWN_STATUSES));
61
+ (0, shared_1.addOrgScopeOptions)(command);
62
+ (0, shared_1.addConfigOption)(command);
63
+ (0, shared_1.addVerbosityOptions)(command);
64
+ (0, shared_1.addApplyOption)(command);
65
+ (0, shared_1.addWhatIfOption)(command);
66
+ command.action(async (briefId, options) => {
67
+ await (0, shared_1.withApplyGate)(async (opts) => {
68
+ if (!opts.file && !opts.status) {
69
+ throw (0, cli_tasks_1.inputError)("Pass --file <path> or --status <status>.", "The PUT requires at least one field; pass a JSON file with the patch or use --status for the common case.");
70
+ }
71
+ const fromFile = opts.file ? assertUpdateBody(readJsonFile(opts.file)) : {};
72
+ const patch = {
73
+ ...fromFile,
74
+ ...(opts.status ? { status: opts.status } : {}),
75
+ };
76
+ await (0, tasks_1.runBriefUpdate)({ ...opts, briefId, patch });
77
+ })(options);
78
+ });
79
+ command.addHelpText("after", "\nExamples:\n" +
80
+ " $ scai ops brief update <id> --status Approved -n agents --apply\n" +
81
+ " $ scai ops brief update <id> -f ./patch.json -n agents --apply\n");
82
+ return command;
83
+ };
84
+ exports.createBriefUpdateCommand = createBriefUpdateCommand;
@@ -76,7 +76,7 @@ const createDiffCommand = () => {
76
76
  command.action(async (options) => {
77
77
  const logger = (0, cli_tasks_1.toLogger)(options);
78
78
  const ctx = buildContext(options, logger);
79
- const recipe = (0, sync_1.loadRecipe)(options.file ?? "", recipe_1.campaignKind.schema);
79
+ const recipe = await (0, sync_1.loadRecipe)(options.file ?? "", recipe_1.campaignKind.schema);
80
80
  const plan = await (0, sync_1.syncDiff)(recipe_1.campaignKind, recipe, { kind: recipe_1.campaignKind.name, id: recipe.name }, ctx);
81
81
  printPlan(logger, plan);
82
82
  });
@@ -94,7 +94,7 @@ const createPushCommand = () => {
94
94
  command.action(async (options) => {
95
95
  const logger = (0, cli_tasks_1.toLogger)(options);
96
96
  const ctx = buildContext(options, logger);
97
- const recipe = (0, sync_1.loadRecipe)(options.file ?? "", recipe_1.campaignKind.schema);
97
+ const recipe = await (0, sync_1.loadRecipe)(options.file ?? "", recipe_1.campaignKind.schema);
98
98
  const mode = options.allowWrite ? "apply" : "what-if";
99
99
  const outcome = await (0, sync_1.syncPush)(recipe_1.campaignKind, recipe, { kind: recipe_1.campaignKind.name, id: recipe.name }, ctx, { mode, prune: options.prune });
100
100
  printPlan(logger, outcome.plan);
@@ -72,7 +72,7 @@ exports.TOOL_DESCRIPTIONS = {
72
72
  publish_lifecycle: "Mutating publishing operations. v1 exposes only `cancel` — the safety-improving op that stops a running publish. Submission verbs (`submit_item` / `submit_all` / `unpublish`) are intentionally CLI-only because publishing pushes content to Experience Edge and the consent model requires a token minted from a human-driven dry-run. Use `publish_inspect verb='list-running'` to find a jobId to cancel; cancellation is recoverable via resubmission.",
73
73
  // Brief (Content Operations)
74
74
  brief_inspect: "[unstable] Read-side Content Operations Brief surface over a discriminated { verb } input — `list` (briefs in tenant), `show` (one brief by id, with nested to-dos/comments/references), `types` (brief schema templates with field definitions and aiIntent hints), `todos` (across briefs or filtered by briefId), or `comments` (across briefs or filtered by briefId). 'Todo' is the Content Operations UI label for the Brief API's wire `tasks` resource. No writes; safe to call as part of plan assembly before any future brief_manage operation.",
75
- brief_manage: "[unstable] Mutating Content Operations Brief surface over a discriminated { resource, verb } input. `resource: 'brief-type'` supports `create` (POST a new type from a full body), `update` (PUT-replace by id; no PATCH — read first if preserving fields), and `delete` (irreversible). `resource: 'brief'` supports `set-status` — move a brief to Draft | InReview | Approved | Canceled | Archived (a brief must leave Draft before it can be linked to a campaign) — and `delete` (irreversible; SDK `deleteBrief`). `resource: 'comment'` supports `create` — post a comment to a brief via `commentText` (UNVERIFIED: the write body is a best guess; smoke-test before relying on it). Requires allowWrite: true. Brief-type `name` must match /^[A-Za-z][A-Za-z0-9_]*$/. Brief `create` remains SDK/CLI-only.",
75
+ brief_manage: "[unstable] Mutating Content Operations Brief surface over a discriminated { resource, verb } input. `resource: 'brief-type'` supports `create` (POST a new type from a full body), `update` (PUT-replace by id; no PATCH — read first if preserving fields), and `delete` (irreversible). `resource: 'brief'` supports `create` (POST a new brief — needs `briefTypeId` plus a `brief` body of name/locale/fields/isTemplate), `update` (partial PUT by id — any subset of name/locale/fields/isTemplate plus an optional `status`), `set-status` — move a brief to Draft | InReview | Approved | Canceled | Archived (a brief must leave Draft before it can be linked to a campaign) — and `delete` (irreversible; SDK `deleteBrief`). `resource: 'comment'` supports `create` — post a comment to a brief via `commentText` (UNVERIFIED: the write body is a best guess; smoke-test before relying on it). Requires allowWrite: true. Brief-type `name` must match /^[A-Za-z][A-Za-z0-9_]*$/. For declarative, idempotent brief writes prefer brief_recipe_push.",
76
76
  // Campaign (Orchestrate)
77
77
  campaign_inspect: "[unstable] Read-side Orchestrate campaign surface over a discriminated { verb } input — `list` (campaigns in tenant), `show` (one campaign by id, with deliverables and tasks inline), `tasks` (tasks under a deliverable), `task` (one task by id), or `users` (the member directory that resolves the Auth0 subjects on members and assignees). A campaign is an Orchestrate `project`; projects own deliverables, deliverables own tasks. No writes.",
78
78
  campaign_manage: "[unstable] Mutating Orchestrate campaign surface over a discriminated { resource, verb } input — `resource: 'campaign'|'deliverable'` support verbs `create` and `delete`; `resource: 'task'` supports `create`, `update` (PUT full-replacement, no PATCH), and `delete`. Requires allowWrite: true. Deliverable/task writes need `campaignId` (and `deliverableId` for tasks); update/task-delete need `taskId`. The `delete` verb is irreversible and hits Orchestrate DELETE endpoints that were never captured during reverse-engineering — they are wired optimistically per REST conventions and remain UNVERIFIED; smoke-test before relying on them. Pass whatIf: true for a plan-only dry run.",
@@ -90,8 +90,8 @@ exports.TOOL_DESCRIPTIONS = {
90
90
  brand_review: "[unstable] Score content against a brand kit using AI. Returns an overall 1–5 score plus per-section + per-field breakdowns with explanations and improvement suggestions. The kit must already have populated sections — call brand_manage with action=seed (or pre-existing brand_inspect verb=list-sections on a populated kit) first. Headline agent-loop op for evaluating marketing copy, page content, or PR drafts; pair with the file path in `label` so SARIF / CI aggregators can attribute findings to source files.",
91
91
  brand_recipe_inspect: "[unstable] Read-side of the brand-kit recipe surface over a discriminated { verb } input. verb=pull captures a live brand kit as a declarative recipe (a clean, schema'd description — kit metadata plus section/field values, no server UUIDs). verb=diff compares a recipe against the live kit and returns the plan (create / update / noop changes). Both are read-only; neither writes. Use pull to snapshot a kit, diff to preview what a push would do.",
92
92
  brand_recipe_push: "[unstable] Converge a brand kit onto a declarative recipe. Computes the plan, then — unless whatIf — applies it: full orchestration via seedBrandKit (create → upload → publish → ingest → enrich) when the kit is absent, then per-field value convergence. Requires allowWrite: true to mutate; pass whatIf: true for a dry-run that returns the plan without writing. When the recipe carries documents for a not-yet-created kit, push triggers paid AI pipeline runs (~5–15 min).",
93
- brief_recipe_inspect: "[unstable] Pull or diff a Sitecore Content Operations brief type as a declarative recipe. verb='pull' captures the live brief type named `name` as a clean recipe; verb='diff' compares a given recipe against the live type and returns the convergence plan. Read-only — neither verb writes.",
94
- brief_recipe_push: "[unstable] Push a brief-type recipe — converge a Sitecore Content Operations brief type onto the given declarative recipe. Creates the brief type when absent or PUT-replaces it when present. Write tool: gated by `allowWrite`; pass `whatIf: true` for a dry-run that returns the plan without writing.",
93
+ brief_recipe_inspect: "[unstable] Pull or diff a Sitecore Content Operations brief type OR brief instance as a declarative recipe. `kind` discriminates: 'brief-type' (default — the schema template, identified by codename) or 'brief' (a populated brief instance, identified by display name; references its type by codename via `briefTypeName`). verb='pull' captures the live resource named `name` as a clean recipe; verb='diff' compares a given recipe against the live resource and returns the convergence plan. Read-only — neither verb writes.",
94
+ brief_recipe_push: "[unstable] Push a brief-type OR brief-instance recipe — converge the named Sitecore Content Operations resource onto the recipe. `kind: 'brief-type'` (default) creates the brief type when absent or PUT-replaces it when present. `kind: 'brief'` creates the brief when absent (resolving `briefTypeName` to its server id), or partial-PUT-updates it when present (refuses to repoint at a different brief type — the API has no verified path for that). Write tool: gated by `allowWrite`; pass `whatIf: true` for a dry-run that returns the plan without writing.",
95
95
  campaign_recipe_inspect: "[unstable] Pull or diff a Sitecore Orchestrate campaign as a declarative recipe. verb='pull' captures the live campaign named `campaignName` (its project, deliverables, and tasks) as a clean recipe with server ids dropped. verb='diff' compares a given recipe against the live campaign and returns the convergence plan. Neither verb writes.",
96
96
  campaign_recipe_push: "[unstable] Push a campaign recipe — converge a Sitecore Orchestrate campaign onto the recipe. Creates the campaign when absent, creates missing deliverables and tasks, and updates existing tasks. Additive: a recipe omitting a deliverable or task never removes it. Gated by `allowWrite`; `whatIf` returns the plan without writing.",
97
97
  };
@@ -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),