@happyvertical/smrt-cli 0.55.9 → 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.
- package/AGENTS.md +11 -19
- package/README.md +4 -0
- package/agents/cookbook-apply.md +65 -0
- package/agents/kitchen.md +111 -0
- package/agents/process-fixture-isolation.md +29 -0
- package/cookbooks/bakery.cookbook.json +187 -0
- package/cookbooks/mechanic.cookbook.json +191 -0
- package/cookbooks/welder.cookbook.json +177 -0
- package/cookbooks/yoga-studio.cookbook.json +173 -0
- package/dist/{AuditLogCollection-WDHL-_AF-HQQINXuT.js → AuditLogCollection-WDHL-_AF-ClQNu_VR.js} +1 -1
- package/dist/{OidcIdentity-BUblgQLs-B3dRU6nj.js → OidcIdentity-BUblgQLs-br8RyLkA.js} +2 -2
- package/dist/{OidcIdentityCollection-ClOyaHeF-C7-jxx9o.js → OidcIdentityCollection-ClOyaHeF-BpQXB3mz.js} +1 -1
- package/dist/{ProfileAssetCollection-CRMcjLJ--CQ4QjtjK.js → ProfileAssetCollection-CRMcjLJ--C5ytBKfF.js} +3 -3
- package/dist/{ProfileCollection-C4INKh7Y-C22q_JqL.js → ProfileCollection-C4INKh7Y-CfkyJilm.js} +17 -17
- package/dist/{ProfileLinkCollection-DFfZRsgv-BsRRI9lh.js → ProfileLinkCollection-DFfZRsgv-MQ2zhD9z.js} +2 -2
- package/dist/{ProfileMetadataCollection-BIYKy4Y3-C6hpFkOa.js → ProfileMetadataCollection-BIYKy4Y3-DXu5L2yx.js} +1 -1
- package/dist/{ProfileMetafieldCollection-Buz7gzag-BLXQjqBG.js → ProfileMetafieldCollection-Buz7gzag-C2tZc1PF.js} +1 -1
- package/dist/{ProfileRelationshipCollection-C4g_QLI4-DNywujKX.js → ProfileRelationshipCollection-C4g_QLI4-vTnkAIAh.js} +1 -1
- package/dist/{ProfileTypeCollection-B7kwi1mU-DPXH67Rh.js → ProfileTypeCollection-B7kwi1mU-KwPGcUkI.js} +2 -2
- package/dist/apply-BNDIdixP.js +947 -0
- package/dist/cli-generator.d.ts.map +1 -1
- package/dist/commands/cookbook/apply.d.ts +102 -0
- package/dist/commands/cookbook/apply.d.ts.map +1 -0
- package/dist/commands/cookbook/recipe-index.d.ts +108 -0
- package/dist/commands/cookbook/recipe-index.d.ts.map +1 -0
- package/dist/commands/cookbook/registry.d.ts +69 -0
- package/dist/commands/cookbook/registry.d.ts.map +1 -0
- package/dist/commands/cookbook/shared.d.ts +23 -0
- package/dist/commands/cookbook/shared.d.ts.map +1 -0
- package/dist/commands/cookbook/validate.d.ts +25 -0
- package/dist/commands/cookbook/validate.d.ts.map +1 -0
- package/dist/commands/cookbook.d.ts +6 -0
- package/dist/commands/cookbook.d.ts.map +1 -0
- package/dist/commands/index.d.ts +2 -0
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/kitchen/ai.d.ts +34 -0
- package/dist/commands/kitchen/ai.d.ts.map +1 -0
- package/dist/commands/kitchen/apply.d.ts +52 -0
- package/dist/commands/kitchen/apply.d.ts.map +1 -0
- package/dist/commands/kitchen/mcp/catalog.d.ts +39 -0
- package/dist/commands/kitchen/mcp/catalog.d.ts.map +1 -0
- package/dist/commands/kitchen/mcp/server.d.ts +15 -0
- package/dist/commands/kitchen/mcp/server.d.ts.map +1 -0
- package/dist/commands/kitchen/mcp/tools.d.ts +68 -0
- package/dist/commands/kitchen/mcp/tools.d.ts.map +1 -0
- package/dist/commands/kitchen/planner.d.ts +71 -0
- package/dist/commands/kitchen/planner.d.ts.map +1 -0
- package/dist/commands/kitchen/run.d.ts +40 -0
- package/dist/commands/kitchen/run.d.ts.map +1 -0
- package/dist/commands/kitchen/server.d.ts +39 -0
- package/dist/commands/kitchen/server.d.ts.map +1 -0
- package/dist/commands/kitchen.d.ts +7 -0
- package/dist/commands/kitchen.d.ts.map +1 -0
- package/dist/{commands-NBDTnB3h.js → commands-pbRVQvix.js} +759 -9
- package/dist/dist-BsRSQ_hE.js +4117 -0
- package/dist/{dist-DuVZ0o9F.js → dist-CYVIKmvy.js} +4 -4
- package/dist/dist-D_WqjwkC.js +21 -0
- package/dist/{dist-FZLsgj__.js → dist-DxkSjKQQ.js} +2 -2
- package/dist/dist-ffh0LeC4.js +1788 -0
- package/dist/index.js +44 -16
- package/dist/{oidcProvisioningPrimitives-DNP9EZlU-CbUV0zfC.js → oidcProvisioningPrimitives-DNP9EZlU-CBYwksp0.js} +3 -3
- package/dist/{resolveIdentity-CkyqvctR-qw-nqxJ3.js → resolveIdentity-CkyqvctR-B5btFsQd.js} +14 -14
- package/dist/server-Dp096tZj.js +898 -0
- package/package.json +13 -10
- package/dist/dist-B-kbXFDM.js +0 -4113
- package/dist/dist-BKuPI8_N.js +0 -1788
- package/dist/dist-DqmJkgkG.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 };
|