@happyvertical/smrt-cli 0.55.10 → 0.55.11

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/AGENTS.md +11 -19
  2. package/README.md +4 -0
  3. package/agents/cookbook-apply.md +65 -0
  4. package/agents/kitchen.md +111 -0
  5. package/agents/process-fixture-isolation.md +29 -0
  6. package/cookbooks/bakery.cookbook.json +187 -0
  7. package/cookbooks/mechanic.cookbook.json +191 -0
  8. package/cookbooks/welder.cookbook.json +177 -0
  9. package/cookbooks/yoga-studio.cookbook.json +173 -0
  10. package/dist/{AuditLogCollection-WDHL-_AF-9sLdjeZF.js → AuditLogCollection-WDHL-_AF-ClQNu_VR.js} +1 -1
  11. package/dist/{OidcIdentity-BUblgQLs-QX0f44sA.js → OidcIdentity-BUblgQLs-br8RyLkA.js} +2 -2
  12. package/dist/{OidcIdentityCollection-ClOyaHeF-BfIHh4aA.js → OidcIdentityCollection-ClOyaHeF-BpQXB3mz.js} +1 -1
  13. package/dist/{ProfileAssetCollection-CRMcjLJ--Dobq-AgM.js → ProfileAssetCollection-CRMcjLJ--C5ytBKfF.js} +3 -3
  14. package/dist/{ProfileCollection-C4INKh7Y-DtWNBhg3.js → ProfileCollection-C4INKh7Y-CfkyJilm.js} +17 -17
  15. package/dist/{ProfileLinkCollection-DFfZRsgv-DNiBQtmL.js → ProfileLinkCollection-DFfZRsgv-MQ2zhD9z.js} +2 -2
  16. package/dist/{ProfileMetadataCollection-BIYKy4Y3-CeQxYDL4.js → ProfileMetadataCollection-BIYKy4Y3-DXu5L2yx.js} +1 -1
  17. package/dist/{ProfileMetafieldCollection-Buz7gzag-DP2ZYYv7.js → ProfileMetafieldCollection-Buz7gzag-C2tZc1PF.js} +1 -1
  18. package/dist/{ProfileRelationshipCollection-C4g_QLI4-VbnpFG2C.js → ProfileRelationshipCollection-C4g_QLI4-vTnkAIAh.js} +1 -1
  19. package/dist/{ProfileTypeCollection-B7kwi1mU-DWalSOyP.js → ProfileTypeCollection-B7kwi1mU-KwPGcUkI.js} +2 -2
  20. package/dist/apply-BNDIdixP.js +947 -0
  21. package/dist/cli-generator.d.ts.map +1 -1
  22. package/dist/commands/cookbook/apply.d.ts +102 -0
  23. package/dist/commands/cookbook/apply.d.ts.map +1 -0
  24. package/dist/commands/cookbook/recipe-index.d.ts +108 -0
  25. package/dist/commands/cookbook/recipe-index.d.ts.map +1 -0
  26. package/dist/commands/cookbook/registry.d.ts +69 -0
  27. package/dist/commands/cookbook/registry.d.ts.map +1 -0
  28. package/dist/commands/cookbook/shared.d.ts +23 -0
  29. package/dist/commands/cookbook/shared.d.ts.map +1 -0
  30. package/dist/commands/cookbook/validate.d.ts +25 -0
  31. package/dist/commands/cookbook/validate.d.ts.map +1 -0
  32. package/dist/commands/cookbook.d.ts +6 -0
  33. package/dist/commands/cookbook.d.ts.map +1 -0
  34. package/dist/commands/index.d.ts +2 -0
  35. package/dist/commands/index.d.ts.map +1 -1
  36. package/dist/commands/kitchen/ai.d.ts +34 -0
  37. package/dist/commands/kitchen/ai.d.ts.map +1 -0
  38. package/dist/commands/kitchen/apply.d.ts +52 -0
  39. package/dist/commands/kitchen/apply.d.ts.map +1 -0
  40. package/dist/commands/kitchen/mcp/catalog.d.ts +39 -0
  41. package/dist/commands/kitchen/mcp/catalog.d.ts.map +1 -0
  42. package/dist/commands/kitchen/mcp/server.d.ts +15 -0
  43. package/dist/commands/kitchen/mcp/server.d.ts.map +1 -0
  44. package/dist/commands/kitchen/mcp/tools.d.ts +68 -0
  45. package/dist/commands/kitchen/mcp/tools.d.ts.map +1 -0
  46. package/dist/commands/kitchen/planner.d.ts +71 -0
  47. package/dist/commands/kitchen/planner.d.ts.map +1 -0
  48. package/dist/commands/kitchen/run.d.ts +40 -0
  49. package/dist/commands/kitchen/run.d.ts.map +1 -0
  50. package/dist/commands/kitchen/server.d.ts +39 -0
  51. package/dist/commands/kitchen/server.d.ts.map +1 -0
  52. package/dist/commands/kitchen.d.ts +7 -0
  53. package/dist/commands/kitchen.d.ts.map +1 -0
  54. package/dist/{commands-n_7ZXru_.js → commands-pbRVQvix.js} +759 -9
  55. package/dist/{dist-BsMQ4Itc.js → dist-BsRSQ_hE.js} +16 -16
  56. package/dist/{dist-DscvT_q_.js → dist-CYVIKmvy.js} +4 -4
  57. package/dist/dist-D_WqjwkC.js +21 -0
  58. package/dist/{dist-gZP7Tw10.js → dist-DxkSjKQQ.js} +2 -2
  59. package/dist/{dist-Bw1oCty_.js → dist-ffh0LeC4.js} +3 -3
  60. package/dist/index.js +44 -16
  61. package/dist/{oidcProvisioningPrimitives-DNP9EZlU-w2kQ8oqV.js → oidcProvisioningPrimitives-DNP9EZlU-CBYwksp0.js} +3 -3
  62. package/dist/{resolveIdentity-CkyqvctR-Cm591gNa.js → resolveIdentity-CkyqvctR-B5btFsQd.js} +14 -14
  63. package/dist/server-Dp096tZj.js +898 -0
  64. package/package.json +13 -10
  65. package/dist/dist-DsZbjZ9J.js +0 -21
@@ -0,0 +1,898 @@
1
+ import { c as SMRT_SCOPE, l as loadRegistryManifest, r as cliVersion, s as validateCookbookText, t as createCookbookApplier, u as resolveRecipeIndex } from "./apply-BNDIdixP.js";
2
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
3
+ import { dirname, isAbsolute, join, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { createHash, randomUUID } from "node:crypto";
6
+ import { validateCookbook } from "@happyvertical/smrt-core/cookbook";
7
+ import { Server } from "@modelcontextprotocol/server";
8
+ import { serveStdio } from "@modelcontextprotocol/server/stdio";
9
+ import { buildBatchTool, checkSchema, createCookbookEngine } from "@happyvertical/smrt-core/cookbook/engine";
10
+ //#region src/commands/kitchen/mcp/catalog.ts
11
+ /**
12
+ * The engine catalog (#3752) built from real recipe manifests: the recipes
13
+ * each package declares, the models its objects declare, and the library of
14
+ * ready-made cookbooks. Nothing here is invented; a package that cannot be
15
+ * found contributes nothing and is reported.
16
+ */
17
+ var FILE_SUFFIX = ".cookbook.json";
18
+ /** Fields every object has, or that policies cannot target. */
19
+ var SYSTEM_FIELDS = [
20
+ "id",
21
+ "slug",
22
+ "context",
23
+ "createdAt",
24
+ "updatedAt",
25
+ "created_at",
26
+ "updated_at"
27
+ ];
28
+ /** `@happyvertical/smrt-commerce` -> `commerce`, as item ids spell it. */
29
+ function packageIdOf(packageName) {
30
+ return packageName.replace(/^@[^/]+\//, "").replace(/^smrt-/, "");
31
+ }
32
+ var isExposed = (object) => {
33
+ const api = object.decoratorConfig?.api;
34
+ if (api === false) return false;
35
+ const include = api?.include;
36
+ return !(Array.isArray(include) && include.length === 0);
37
+ };
38
+ function modelsOf(packageName, manifest) {
39
+ const packageId = packageIdOf(packageName);
40
+ const models = [];
41
+ for (const [id, object] of Object.entries(manifest.objects ?? {})) {
42
+ const fields = object.fields ?? {};
43
+ const name = object.className ?? id.slice(id.lastIndexOf(":") + 1);
44
+ if (name.endsWith("Collection") && Object.keys(fields).length === 0) continue;
45
+ const listed = Object.entries(fields).map(([field, def]) => ({
46
+ name: field,
47
+ ...def?._meta?.__tenancy ? { system: true } : {}
48
+ }));
49
+ for (const field of SYSTEM_FIELDS) if (!listed.some((f) => f.name === field)) listed.push({
50
+ name: field,
51
+ system: true
52
+ });
53
+ models.push({
54
+ id,
55
+ name,
56
+ packageId,
57
+ exposed: isExposed(object),
58
+ fields: listed
59
+ });
60
+ }
61
+ return models;
62
+ }
63
+ /** Recipes the engine can use: they need an id, a label and a nav list. */
64
+ function recipesOf(manifest) {
65
+ return (manifest.recipes ?? []).flatMap((recipe) => recipe.id && recipe.label ? [{
66
+ ...recipe,
67
+ id: recipe.id,
68
+ label: recipe.label,
69
+ models: recipe.models ?? [],
70
+ nav: recipe.nav ?? [],
71
+ requires: recipe.requires ?? []
72
+ }] : []);
73
+ }
74
+ /** Build the engine catalog from every manifest in the index. */
75
+ function catalogFromIndex(index, cookbooks = []) {
76
+ const recipes = [];
77
+ const models = [];
78
+ for (const entry of [...index.packages.values()].sort((a, b) => a.packageName.localeCompare(b.packageName))) {
79
+ const manifest = entry.manifest;
80
+ recipes.push(...recipesOf(manifest));
81
+ models.push(...modelsOf(entry.packageName, manifest));
82
+ }
83
+ return {
84
+ recipes,
85
+ models,
86
+ cookbooks
87
+ };
88
+ }
89
+ /** The package that ships this CLI's own files (`cookbooks/`). */
90
+ function packageRoot() {
91
+ let current = dirname(fileURLToPath(import.meta.url));
92
+ while (true) {
93
+ if (existsSync(join(current, "cookbooks"))) {
94
+ if (existsSync(join(current, "package.json"))) return current;
95
+ }
96
+ const parent = dirname(current);
97
+ if (parent === current) return dirname(fileURLToPath(import.meta.url));
98
+ current = parent;
99
+ }
100
+ }
101
+ /** `yoga-studio` -> `Yoga Studio`. */
102
+ var titleOf = (id) => id.split(/[-_.]/).filter(Boolean).map((part) => part[0]?.toUpperCase() + part.slice(1)).join(" ");
103
+ /**
104
+ * Ready-made cookbooks from `<dir>/<id>.cookbook.json` files. A file that is
105
+ * not a valid cookbook is skipped and named in `warnings`.
106
+ */
107
+ function readLibraryDir(dir, warnings) {
108
+ if (!existsSync(dir)) {
109
+ warnings.push(`Cookbook library directory not found: ${dir}`);
110
+ return [];
111
+ }
112
+ const library = [];
113
+ for (const file of readdirSync(dir).sort()) {
114
+ if (!file.endsWith(FILE_SUFFIX)) continue;
115
+ const id = file.slice(0, -14);
116
+ try {
117
+ const checked = validateCookbook(JSON.parse(readFileSync(join(dir, file), "utf-8")));
118
+ if (!checked.ok) {
119
+ warnings.push(`${file}: ${checked.errors.join("; ")}`);
120
+ continue;
121
+ }
122
+ library.push({
123
+ id,
124
+ name: checked.cookbook.name ?? titleOf(id),
125
+ document: checked.cookbook
126
+ });
127
+ } catch (error) {
128
+ warnings.push(`${file}: ${error instanceof Error ? error.message : String(error)}`);
129
+ }
130
+ }
131
+ return library;
132
+ }
133
+ /** The library this CLI ships plus any extra directories (later ids win). */
134
+ function loadLibrary(extraDirs, warnings) {
135
+ const byId = /* @__PURE__ */ new Map();
136
+ for (const dir of [join(packageRoot(), "cookbooks"), ...extraDirs.map((d) => resolve(d))]) for (const entry of readLibraryDir(dir, warnings)) byId.set(entry.id, entry);
137
+ return [...byId.values()];
138
+ }
139
+ /**
140
+ * Resolve the manifests behind the library's recipes (local sources first,
141
+ * then the scope registry, as `cookbook validate` does) and build the catalog.
142
+ */
143
+ async function loadCatalog(options) {
144
+ const warnings = [];
145
+ const library = loadLibrary(options.library ?? [], warnings);
146
+ const index = await resolveRecipeIndex({
147
+ recipes: [...new Set(library.flatMap((c) => c.document.recipes))],
148
+ features: [],
149
+ policies: []
150
+ }, {
151
+ dir: options.dir,
152
+ manifests: options.manifests ?? [],
153
+ registry: options.registry !== false,
154
+ warn: (message) => warnings.push(message)
155
+ });
156
+ for (const name of options.packages ?? []) {
157
+ const full = name.startsWith("@") ? name : `${SMRT_SCOPE}/smrt-${name}`;
158
+ if (index.packages.has(full) || options.registry === false) continue;
159
+ if (!await loadRegistryManifest(index, full, { dir: options.dir })) warnings.push(`Could not find a manifest for ${full}.`);
160
+ }
161
+ for (const name of index.missing) warnings.push(`No manifest found for ${name}; its recipes are unavailable.`);
162
+ const catalog = catalogFromIndex(index, library);
163
+ if (catalog.recipes.length === 0) warnings.push(index.packages.size > 0 ? `Manifests were found for ${index.packages.size} package(s) but none declares recipes; use package versions that ship recipes, or pass --manifests.` : "No recipe manifests were found, so no recipes are available. Run from a SMRT workspace or project, pass --manifests, or allow registry access.");
164
+ return {
165
+ catalog,
166
+ index,
167
+ warnings
168
+ };
169
+ }
170
+ var SCHEMA = "https://json-schema.org/draft/2020-12/schema";
171
+ var planId = {
172
+ type: "string",
173
+ description: "The plan id start_plan returned.",
174
+ minLength: 1,
175
+ maxLength: 100
176
+ };
177
+ var errorSchema = {
178
+ type: "object",
179
+ description: "Present when ok is false.",
180
+ properties: {
181
+ code: { type: "string" },
182
+ message: { type: "string" },
183
+ path: { type: "string" },
184
+ details: {
185
+ type: "object",
186
+ additionalProperties: true
187
+ }
188
+ },
189
+ required: ["code", "message"],
190
+ additionalProperties: true
191
+ };
192
+ var snapshotSchema = {
193
+ type: "object",
194
+ description: "The plan: revision, app name, recipes, features, settings, theme, menu sections and undo ids.",
195
+ properties: {
196
+ version: { type: "integer" },
197
+ revision: { type: "integer" }
198
+ },
199
+ required: ["revision"],
200
+ additionalProperties: true
201
+ };
202
+ /** An output schema: `ok` always, `error` when it is false, plus the extras. */
203
+ var output = (properties, required = []) => ({
204
+ $schema: SCHEMA,
205
+ type: "object",
206
+ properties: {
207
+ ok: { type: "boolean" },
208
+ error: errorSchema,
209
+ warnings: {
210
+ type: "array",
211
+ items: { type: "string" }
212
+ },
213
+ ...properties
214
+ },
215
+ required: ["ok", ...required],
216
+ additionalProperties: true
217
+ });
218
+ var input = (properties, required = []) => ({
219
+ $schema: SCHEMA,
220
+ type: "object",
221
+ properties,
222
+ ...required.length ? { required } : {},
223
+ additionalProperties: false
224
+ });
225
+ var annotations = (title, hints) => ({
226
+ title,
227
+ readOnlyHint: false,
228
+ destructiveHint: false,
229
+ idempotentHint: false,
230
+ openWorldHint: false,
231
+ ...hints
232
+ });
233
+ var READ_ONLY = {
234
+ readOnlyHint: true,
235
+ idempotentHint: true
236
+ };
237
+ /** The batch tool's schema without catalog enums: ids come from search_recipes. */
238
+ var batchBase = buildBatchTool({
239
+ recipes: [],
240
+ models: []
241
+ }).inputSchema;
242
+ var KITCHEN_TOOLS = [
243
+ {
244
+ name: "list_cookbooks",
245
+ title: "List cookbooks",
246
+ description: "List the ready-made cookbooks (starting points such as a bakery or a yoga studio) with the recipes each one switches on. Start here to offer the user a head start.",
247
+ inputSchema: input({}),
248
+ outputSchema: output({ cookbooks: {
249
+ type: "array",
250
+ items: {
251
+ type: "object",
252
+ properties: {
253
+ id: { type: "string" },
254
+ name: { type: "string" },
255
+ recipes: {
256
+ type: "array",
257
+ items: { type: "string" }
258
+ },
259
+ features: {
260
+ type: "array",
261
+ items: { type: "string" }
262
+ }
263
+ },
264
+ required: [
265
+ "id",
266
+ "name",
267
+ "recipes"
268
+ ]
269
+ }
270
+ } }, ["cookbooks"]),
271
+ annotations: annotations("List cookbooks", READ_ONLY)
272
+ },
273
+ {
274
+ name: "search_recipes",
275
+ title: "Search recipes",
276
+ description: "Find recipes (units of app function such as Sales or Customers) by words in their id, name, summary or synonyms. With no query, lists them all. Use the returned ids with add_recipes.",
277
+ inputSchema: input({
278
+ query: {
279
+ type: "string",
280
+ description: "Words to look for; every word must match.",
281
+ maxLength: 200
282
+ },
283
+ limit: {
284
+ type: "integer",
285
+ description: "Most results to return (default 20).",
286
+ minimum: 1,
287
+ maximum: 100
288
+ }
289
+ }),
290
+ outputSchema: output({
291
+ total: { type: "integer" },
292
+ recipes: {
293
+ type: "array",
294
+ items: {
295
+ type: "object",
296
+ properties: {
297
+ id: { type: "string" },
298
+ label: { type: "string" },
299
+ summary: { type: "string" },
300
+ requires: {
301
+ type: "array",
302
+ items: { type: "string" }
303
+ },
304
+ models: {
305
+ type: "array",
306
+ items: { type: "string" }
307
+ }
308
+ },
309
+ required: ["id", "label"]
310
+ }
311
+ }
312
+ }, ["total", "recipes"]),
313
+ annotations: annotations("Search recipes", READ_ONLY)
314
+ },
315
+ {
316
+ name: "start_plan",
317
+ title: "Start a plan",
318
+ description: "Start a new plan for an app, empty or from a ready-made cookbook (see list_cookbooks). Returns the plan id every other tool takes. The plan lives in this server process until apply_plan writes it to a project.",
319
+ inputSchema: input({
320
+ cookbook: {
321
+ type: "string",
322
+ description: "A ready-made cookbook id from list_cookbooks.",
323
+ minLength: 1,
324
+ maxLength: 200
325
+ },
326
+ name: {
327
+ type: "string",
328
+ description: "A name for the app.",
329
+ minLength: 1,
330
+ maxLength: 200
331
+ }
332
+ }),
333
+ outputSchema: output({
334
+ planId: { type: "string" },
335
+ snapshot: snapshotSchema
336
+ }, []),
337
+ annotations: annotations("Start a plan", {})
338
+ },
339
+ {
340
+ name: "run_commands",
341
+ title: "Change the plan",
342
+ description: "Apply plan commands (add_recipes, set_settings, set_theme, rename_item, hide, ...) all-or-nothing. Send expectedRevision from the last snapshot so a stale change is refused, and a batch id so a retry is not applied twice. A single undo command is also accepted. Returns the new snapshot and one receipt per command.",
343
+ inputSchema: {
344
+ ...batchBase,
345
+ $schema: SCHEMA,
346
+ properties: {
347
+ planId,
348
+ ...batchBase.properties
349
+ },
350
+ required: ["planId", "commands"]
351
+ },
352
+ outputSchema: output({
353
+ planId: { type: "string" },
354
+ replayed: { type: "boolean" },
355
+ receipt: {
356
+ type: "object",
357
+ additionalProperties: true
358
+ },
359
+ results: {
360
+ type: "array",
361
+ items: { type: "object" }
362
+ },
363
+ snapshot: snapshotSchema
364
+ }),
365
+ annotations: annotations("Change the plan", { idempotentHint: true })
366
+ },
367
+ {
368
+ name: "get_plan",
369
+ title: "Get the plan",
370
+ description: "The current plan as a compact snapshot: revision, recipes, settings, theme and the menu the app would show.",
371
+ inputSchema: input({ planId }, ["planId"]),
372
+ outputSchema: output({
373
+ planId: { type: "string" },
374
+ snapshot: snapshotSchema
375
+ }),
376
+ annotations: annotations("Get the plan", READ_ONLY)
377
+ },
378
+ {
379
+ name: "validate_plan",
380
+ title: "Validate the plan",
381
+ description: "Check the plan as a cookbook: the document, that every recipe exists with what it requires, and that policies name real fields. Run before apply_plan. Changes nothing.",
382
+ inputSchema: input({ planId }, ["planId"]),
383
+ outputSchema: output({
384
+ planId: { type: "string" },
385
+ valid: { type: "boolean" },
386
+ errors: {
387
+ type: "array",
388
+ items: { type: "string" }
389
+ },
390
+ diagnostics: {
391
+ type: "array",
392
+ items: { type: "object" }
393
+ },
394
+ packages: {
395
+ type: "array",
396
+ items: { type: "string" }
397
+ }
398
+ }),
399
+ annotations: annotations("Validate the plan", READ_ONLY)
400
+ },
401
+ {
402
+ name: "export_plan",
403
+ title: "Export the plan",
404
+ description: "The plan as a cookbook document (version 1) and its `<name>.cookbook.json` text, to show, save or send elsewhere. Changes nothing.",
405
+ inputSchema: input({
406
+ planId,
407
+ name: {
408
+ type: "string",
409
+ description: "File name stem; default the app name.",
410
+ maxLength: 200
411
+ }
412
+ }, ["planId"]),
413
+ outputSchema: output({
414
+ planId: { type: "string" },
415
+ fileName: { type: "string" },
416
+ text: { type: "string" },
417
+ cookbook: {
418
+ type: "object",
419
+ additionalProperties: true
420
+ }
421
+ }),
422
+ annotations: annotations("Export the plan", READ_ONLY)
423
+ },
424
+ {
425
+ name: "apply_plan",
426
+ title: "Create the project",
427
+ description: "Create the project from the plan in an absolute directory: copy the template, add the packages the recipes need to package.json, and write smrt.cookbook.json. Writes files, so ask the user to confirm the directory first; without confirm: true and the confirmCode from the preview nothing is written and the plan is previewed. The template is set by the server (--template), not chosen per call. Re-applying to a project that already has smrt.cookbook.json updates it. Does not install packages unless install is true.",
428
+ inputSchema: input({
429
+ planId,
430
+ dir: {
431
+ type: "string",
432
+ description: "Absolute path of the project directory: absent or empty for a new project, or an existing project that holds smrt.cookbook.json.",
433
+ minLength: 1,
434
+ maxLength: 1e3
435
+ },
436
+ confirm: {
437
+ type: "boolean",
438
+ description: "true once the user agreed to write into dir. Needs confirmCode from the preview of this same plan and dir; otherwise only a preview is returned."
439
+ },
440
+ confirmCode: {
441
+ type: "string",
442
+ description: "The confirmCode the preview returned for this exact plan state and dir. A code from another dir or an edited plan is refused.",
443
+ maxLength: 200
444
+ },
445
+ dryRun: {
446
+ type: "boolean",
447
+ description: "Preview only, even with confirm."
448
+ },
449
+ install: {
450
+ type: "boolean",
451
+ description: "Run the package manager install afterwards (slow; default false)."
452
+ }
453
+ }, ["planId", "dir"]),
454
+ outputSchema: output({
455
+ planId: { type: "string" },
456
+ applied: { type: "boolean" },
457
+ confirmCode: { type: "string" },
458
+ dir: { type: "string" },
459
+ mode: {
460
+ type: "string",
461
+ enum: ["new", "update"]
462
+ },
463
+ installed: { type: "boolean" },
464
+ packagesAdded: {
465
+ type: "array",
466
+ items: { type: "string" }
467
+ },
468
+ config: { type: "string" },
469
+ nextSteps: {
470
+ type: "array",
471
+ items: { type: "string" }
472
+ },
473
+ errors: {
474
+ type: "array",
475
+ items: { type: "string" }
476
+ }
477
+ }),
478
+ annotations: annotations("Create the project", {
479
+ destructiveHint: true,
480
+ idempotentHint: true,
481
+ openWorldHint: true
482
+ })
483
+ }
484
+ ];
485
+ var fail = (code, message, extra = {}) => ({
486
+ data: {
487
+ ok: false,
488
+ error: {
489
+ code,
490
+ message,
491
+ ...extra
492
+ }
493
+ },
494
+ text: `Error ${code}: ${message}`
495
+ });
496
+ var failFrom = (error) => fail(error.code, error.message, {
497
+ ...error.path ? { path: error.path } : {},
498
+ ...error.details ? { details: error.details } : {}
499
+ });
500
+ /** One line of a snapshot, for the model's text. */
501
+ function describeSnapshot(snapshot) {
502
+ const recipes = snapshot.recipes.map((r) => r.id).join(", ") || "none";
503
+ const menu = snapshot.sections.filter((s) => !s.hidden).map((s) => `${s.label} (${s.items.filter((i) => !i.hidden).map((i) => i.label).join(", ")})`).join("; ");
504
+ return [
505
+ `"${snapshot.app.name}" revision ${snapshot.revision}`,
506
+ `recipes: ${recipes}`,
507
+ ...snapshot.features.length ? [`features: ${snapshot.features.join(", ")}`] : [],
508
+ ...snapshot.unavailable.length ? [`unavailable: ${snapshot.unavailable.join(", ")}`] : [],
509
+ `settings: ${snapshot.settings.currency}, tax ${snapshot.settings.taxRate}%, ${snapshot.settings.paymentTerms}`,
510
+ `theme: ${snapshot.theme.text}`,
511
+ `menu: ${menu || "empty"}`
512
+ ].join("\n");
513
+ }
514
+ /** The first `max` lines, then a count of the rest. */
515
+ var firstLines = (lines, max) => [...lines.slice(0, max), ...lines.length > max ? [`... and ${lines.length - max} more`] : []].join("\n");
516
+ /** Tools that say what the catalog lacks; the rest stay quiet. */
517
+ var WARNS = /* @__PURE__ */ new Set([
518
+ "list_cookbooks",
519
+ "search_recipes",
520
+ "start_plan",
521
+ "validate_plan"
522
+ ]);
523
+ var words = (text) => text.toLowerCase().split(/\s+/).filter(Boolean);
524
+ /** The server's state: the lazily loaded catalog and the plans. */
525
+ var KitchenSession = class KitchenSession {
526
+ options;
527
+ loaded;
528
+ plans = /* @__PURE__ */ new Map();
529
+ constructor(options) {
530
+ this.options = options;
531
+ }
532
+ /** Load the catalog once; a failed load is retried on the next call. */
533
+ catalog() {
534
+ if (!this.loaded) {
535
+ const load = this.options.catalogLoader ?? (() => loadCatalog(this.options));
536
+ this.loaded = load().catch((error) => {
537
+ this.loaded = void 0;
538
+ throw error;
539
+ });
540
+ }
541
+ return this.loaded;
542
+ }
543
+ plan(id) {
544
+ return (typeof id === "string" ? this.plans.get(id) : void 0) ?? fail("not_found", `No plan ${JSON.stringify(id)} in this server; call start_plan (a restart forgets plans).`, { path: "planId" });
545
+ }
546
+ /** Validate arguments against a tool's published schema. */
547
+ static check(tool, args) {
548
+ const problem = checkSchema(tool.inputSchema, args, "arguments");
549
+ return problem ? fail("invalid_input", problem.message, { path: problem.path }) : null;
550
+ }
551
+ async call(name, args = {}) {
552
+ const tool = KITCHEN_TOOLS.find((t) => t.name === name);
553
+ if (!tool) return fail("unknown_tool", `Unknown tool ${JSON.stringify(name)}.`);
554
+ const invalid = KitchenSession.check(tool, args);
555
+ if (invalid) return invalid;
556
+ try {
557
+ const result = await this.dispatch(name, args);
558
+ if (!result.data.ok || !WARNS.has(name)) return result;
559
+ const { warnings } = await this.catalog().catch(() => ({ warnings: [] }));
560
+ if (!warnings.length || result.data.warnings) return result;
561
+ return {
562
+ data: {
563
+ ...result.data,
564
+ warnings
565
+ },
566
+ text: `${result.text}\nWarnings:\n${firstLines(warnings, 5)}`
567
+ };
568
+ } catch (error) {
569
+ return fail("failed", error instanceof Error ? error.message : String(error));
570
+ }
571
+ }
572
+ async dispatch(name, args) {
573
+ switch (name) {
574
+ case "list_cookbooks": return this.listCookbooks();
575
+ case "search_recipes": return this.searchRecipes(args);
576
+ case "start_plan": return this.startPlan(args);
577
+ case "run_commands": return this.runCommands(args);
578
+ case "get_plan": {
579
+ const plan = this.plan(args.planId);
580
+ if (!("engine" in plan)) return plan;
581
+ const snapshot = plan.engine.snapshot();
582
+ return {
583
+ data: {
584
+ ok: true,
585
+ planId: args.planId,
586
+ snapshot
587
+ },
588
+ text: describeSnapshot(snapshot)
589
+ };
590
+ }
591
+ case "validate_plan": return this.validatePlan(args);
592
+ case "export_plan": return this.exportPlan(args);
593
+ case "apply_plan": return this.applyPlan(args);
594
+ default: return fail("unknown_tool", `Unknown tool ${JSON.stringify(name)}.`);
595
+ }
596
+ }
597
+ async listCookbooks() {
598
+ const { catalog } = await this.catalog();
599
+ const cookbooks = (catalog.cookbooks ?? []).map((c) => ({
600
+ id: c.id,
601
+ name: c.name,
602
+ recipes: c.document.recipes,
603
+ features: c.document.features
604
+ }));
605
+ return {
606
+ data: {
607
+ ok: true,
608
+ cookbooks
609
+ },
610
+ text: cookbooks.length ? cookbooks.map((c) => `${c.id}: ${c.name} (${c.recipes.join(", ")})`).join("\n") : "No ready-made cookbooks are available; start an empty plan."
611
+ };
612
+ }
613
+ async searchRecipes(args) {
614
+ const { catalog } = await this.catalog();
615
+ const wanted = words(typeof args.query === "string" ? args.query : "");
616
+ const limit = typeof args.limit === "number" ? args.limit : 20;
617
+ const matches = catalog.recipes.filter((recipe) => {
618
+ const hay = `${recipe.id} ${recipe.label} ${recipe.summary ?? ""} ${(recipe.synonyms ?? []).join(" ")}`.toLowerCase();
619
+ return wanted.every((word) => hay.includes(word));
620
+ });
621
+ const recipes = matches.slice(0, limit).map((recipe) => ({
622
+ id: recipe.id,
623
+ label: recipe.label,
624
+ summary: recipe.summary ?? "",
625
+ requires: recipe.requires,
626
+ models: recipe.models
627
+ }));
628
+ return {
629
+ data: {
630
+ ok: true,
631
+ total: matches.length,
632
+ recipes
633
+ },
634
+ text: recipes.length ? `${matches.length} recipe(s)${matches.length > recipes.length ? `, first ${recipes.length}` : ""}:\n${recipes.map((r) => `${r.id}: ${r.label}${r.summary ? ` - ${r.summary}` : ""}`).join("\n")}` : "No recipes match."
635
+ };
636
+ }
637
+ async startPlan(args) {
638
+ const { catalog } = await this.catalog();
639
+ const engine = createCookbookEngine({ catalog });
640
+ const steps = [];
641
+ if (typeof args.cookbook === "string") steps.push({
642
+ name: "apply_cookbook",
643
+ input: { id: args.cookbook }
644
+ });
645
+ if (typeof args.name === "string") steps.push({
646
+ name: "set_name",
647
+ input: { name: args.name }
648
+ });
649
+ for (const step of steps) {
650
+ const result = engine.run(step);
651
+ if (!result.ok) return failFrom(result.error);
652
+ }
653
+ const id = `plan-${randomUUID().slice(0, 8)}`;
654
+ this.plans.set(id, { engine });
655
+ while (this.plans.size > 20) {
656
+ const oldest = this.plans.keys().next().value;
657
+ if (oldest === void 0) break;
658
+ this.plans.delete(oldest);
659
+ }
660
+ const snapshot = engine.snapshot();
661
+ return {
662
+ data: {
663
+ ok: true,
664
+ planId: id,
665
+ snapshot
666
+ },
667
+ text: `Started ${id}.\n${describeSnapshot(snapshot)}`
668
+ };
669
+ }
670
+ runCommands(args) {
671
+ const plan = this.plan(args.planId);
672
+ if (!("engine" in plan)) return plan;
673
+ const { planId: _planId, ...batch } = args;
674
+ const commands = batch.commands;
675
+ const lone = commands.length === 1 && commands[0]?.name === "undo" ? plan.engine.run({
676
+ ...commands[0],
677
+ ...batch.id !== void 0 ? { id: batch.id } : {},
678
+ ...batch.expectedRevision !== void 0 ? { expectedRevision: batch.expectedRevision } : {}
679
+ }) : null;
680
+ if (lone) {
681
+ if (!lone.ok) return failFrom(lone.error);
682
+ return {
683
+ data: {
684
+ ok: true,
685
+ planId: args.planId,
686
+ ...lone.replayed ? { replayed: true } : {},
687
+ receipt: lone.receipt,
688
+ results: [lone.receipt],
689
+ snapshot: lone.snapshot
690
+ },
691
+ text: `${lone.receipt.summary}\n${describeSnapshot(lone.snapshot)}`
692
+ };
693
+ }
694
+ const result = plan.engine.batch(batch);
695
+ if (!result.ok) {
696
+ const current = result.snapshot;
697
+ return {
698
+ data: {
699
+ ok: false,
700
+ planId: args.planId,
701
+ error: result.error,
702
+ snapshot: current
703
+ },
704
+ text: `Error ${result.error.code}: ${result.error.message}\nNothing was changed (revision ${current.revision}).`
705
+ };
706
+ }
707
+ return {
708
+ data: {
709
+ ok: true,
710
+ planId: args.planId,
711
+ ...result.replayed ? { replayed: true } : {},
712
+ receipt: result.receipt,
713
+ results: result.results.map((r) => r.receipt),
714
+ snapshot: result.snapshot
715
+ },
716
+ text: `${result.replayed ? "Already applied (same id): " : ""}${result.results.map((r) => r.receipt.summary).join("\n")}\n${describeSnapshot(result.snapshot)}`
717
+ };
718
+ }
719
+ async validatePlan(args) {
720
+ const plan = this.plan(args.planId);
721
+ if (!("engine" in plan)) return plan;
722
+ const { index } = await this.catalog();
723
+ const checked = plan.engine.run({ name: "validate" });
724
+ if (!checked.ok) return failFrom(checked.error);
725
+ const diagnostics = checked.data.diagnostics;
726
+ const exported = plan.engine.run({ name: "export_cookbook" });
727
+ if (!exported.ok) return failFrom(exported.error);
728
+ const report = validateCookbookText(exported.data.text, index);
729
+ const valid = checked.data.ok && report.ok;
730
+ const errors = report.errors;
731
+ return {
732
+ data: {
733
+ ok: true,
734
+ planId: args.planId,
735
+ valid,
736
+ errors,
737
+ diagnostics,
738
+ packages: report.packages,
739
+ ...report.warnings.length ? { warnings: report.warnings } : {}
740
+ },
741
+ text: valid ? `Valid. Packages needed: ${report.packages.join(", ") || "none"}.` : `Not valid:\n${firstLines(errors.length ? errors : diagnostics.map((d) => `${d.path}: ${d.message}`), 12)}`
742
+ };
743
+ }
744
+ exportPlan(args) {
745
+ const plan = this.plan(args.planId);
746
+ if (!("engine" in plan)) return plan;
747
+ const result = plan.engine.run({
748
+ name: "export_cookbook",
749
+ input: typeof args.name === "string" ? { name: args.name } : {}
750
+ });
751
+ if (!result.ok) return failFrom(result.error);
752
+ const exported = result.data;
753
+ return {
754
+ data: {
755
+ ok: true,
756
+ planId: args.planId,
757
+ fileName: exported.fileName,
758
+ text: exported.text,
759
+ cookbook: exported.cookbook
760
+ },
761
+ text: `${exported.fileName}\n${exported.text}`
762
+ };
763
+ }
764
+ async applyPlan(args) {
765
+ const plan = this.plan(args.planId);
766
+ if (!("engine" in plan)) return plan;
767
+ const dir = args.dir;
768
+ if (!isAbsolute(dir)) return fail("invalid_input", "dir must be an absolute path.", { path: "dir" });
769
+ const exported = plan.engine.run({ name: "export_cookbook" });
770
+ if (!exported.ok) return failFrom(exported.error);
771
+ const confirmCode = createHash("sha256").update([
772
+ dir,
773
+ this.options.template ?? "",
774
+ exported.data.text
775
+ ].join("\0")).digest("hex").slice(0, 32);
776
+ const confirmed = args.confirm === true && args.dryRun !== true && args.confirmCode === confirmCode;
777
+ const { index } = await this.catalog();
778
+ const outcome = await createCookbookApplier({
779
+ dir,
780
+ index,
781
+ template: this.options.template,
782
+ install: confirmed && args.install === true,
783
+ installOutput: "stderr",
784
+ dryRun: !confirmed,
785
+ manifests: this.options.manifests,
786
+ registry: this.options.registry !== false
787
+ })(exported.data.text);
788
+ if (!outcome.ok) return {
789
+ data: {
790
+ ok: false,
791
+ planId: args.planId,
792
+ error: {
793
+ code: outcome.status === 422 ? "invalid_cookbook" : "apply_refused",
794
+ message: outcome.errors.join("; ")
795
+ },
796
+ errors: outcome.errors
797
+ },
798
+ text: `Not applied:\n${firstLines(outcome.errors, 12)}`
799
+ };
800
+ const { plan: applied } = outcome.result;
801
+ const added = Object.keys(applied.addDependencies);
802
+ const summary = `${applied.mode === "new" ? "new project" : "update"} in ${applied.targetDir}; packages added: ${added.join(", ") || "none"}; smrt.cookbook.json ${applied.config}`;
803
+ const common = {
804
+ planId: args.planId,
805
+ dir: applied.targetDir,
806
+ mode: applied.mode,
807
+ packagesAdded: added,
808
+ config: applied.config,
809
+ nextSteps: outcome.body.nextSteps
810
+ };
811
+ if (!confirmed) return {
812
+ data: {
813
+ ok: false,
814
+ error: {
815
+ code: "confirmation_required",
816
+ message: "Nothing was written. Show the user this preview and the directory, then call apply_plan again with confirm: true and this confirmCode once they agree.",
817
+ details: { preview: summary }
818
+ },
819
+ confirmCode,
820
+ applied: false,
821
+ installed: false,
822
+ ...common
823
+ },
824
+ text: `Preview only (nothing written): ${summary}\nAsk the user to confirm, then call apply_plan with confirm: true and confirmCode: ${confirmCode}.`
825
+ };
826
+ return {
827
+ data: {
828
+ ok: true,
829
+ applied: true,
830
+ installed: outcome.body.installed,
831
+ ...common
832
+ },
833
+ text: `Applied: ${summary}\nNext steps:\n${outcome.body.nextSteps.join("\n")}`
834
+ };
835
+ }
836
+ };
837
+ //#endregion
838
+ //#region src/commands/kitchen/mcp/server.ts
839
+ /**
840
+ * `smrt kitchen mcp` (#3752): the Kitchen tools served over stdio. stdout
841
+ * carries the protocol only; everything else goes to stderr.
842
+ */
843
+ var SERVER_NAME = "smrt-kitchen";
844
+ /** The tool catalog changes only with a release. */
845
+ var CATALOG_CACHE_HINT = {
846
+ ttlMs: 864e5,
847
+ cacheScope: "private"
848
+ };
849
+ /** Build the MCP server; one {@link KitchenSession} (plans, catalog) per server. */
850
+ function createKitchenMcpServer(options) {
851
+ const session = new KitchenSession(options);
852
+ const server = new Server({
853
+ name: SERVER_NAME,
854
+ version: cliVersion()
855
+ }, {
856
+ capabilities: { tools: {} },
857
+ cacheHints: { "tools/list": CATALOG_CACHE_HINT }
858
+ });
859
+ server.setRequestHandler("tools/list", async () => ({ tools: KITCHEN_TOOLS.map((tool) => ({
860
+ name: tool.name,
861
+ title: tool.title,
862
+ description: tool.description,
863
+ inputSchema: tool.inputSchema,
864
+ outputSchema: tool.outputSchema,
865
+ annotations: tool.annotations
866
+ })) }));
867
+ server.setRequestHandler("tools/call", async (request) => {
868
+ const { name, arguments: args } = request.params;
869
+ const result = await session.call(name, args ?? {});
870
+ return {
871
+ content: [{
872
+ type: "text",
873
+ text: result.text
874
+ }],
875
+ structuredContent: result.data,
876
+ ...result.data.ok ? {} : { isError: true }
877
+ };
878
+ });
879
+ return {
880
+ server,
881
+ session
882
+ };
883
+ }
884
+ /** Serve on this process's stdio until the host closes it or the process is stopped. */
885
+ async function runKitchenMcp(options) {
886
+ const handle = serveStdio(() => createKitchenMcpServer(options).server, { onerror: (error) => console.error(`[${SERVER_NAME}] ${error.message}`) });
887
+ await new Promise((resolve) => {
888
+ const done = () => {
889
+ handle.close().finally(resolve);
890
+ };
891
+ process.once("SIGINT", done);
892
+ process.once("SIGTERM", done);
893
+ process.stdin.once("end", done);
894
+ process.stdin.once("close", done);
895
+ });
896
+ }
897
+ //#endregion
898
+ export { runKitchenMcp };