@introspection-ai/pi-recipes 0.1.0-beta.0
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/README.md +116 -0
- package/dist/child-agent.d.ts +40 -0
- package/dist/child-agent.js +235 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +479 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/pi-extension.d.ts +26 -0
- package/dist/pi-extension.js +809 -0
- package/dist/recipe-agent.d.ts +51 -0
- package/dist/recipe-agent.js +359 -0
- package/dist/recipe-dev.d.ts +30 -0
- package/dist/recipe-dev.js +180 -0
- package/dist/recipe-package.d.ts +38 -0
- package/dist/recipe-package.js +231 -0
- package/dist/recipe-publish.d.ts +30 -0
- package/dist/recipe-publish.js +192 -0
- package/dist/recipe-store.d.ts +66 -0
- package/dist/recipe-store.js +691 -0
- package/docs/pi-extension.md +413 -0
- package/docs/recipe-cli.md +529 -0
- package/docs/recipe-flow.md +148 -0
- package/package.json +92 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export interface RecipeSystemInstructions {
|
|
2
|
+
mode: "append" | "replace";
|
|
3
|
+
content: string;
|
|
4
|
+
}
|
|
5
|
+
export interface RecipeAgentExtensions {
|
|
6
|
+
include?: string[];
|
|
7
|
+
exclude?: string[];
|
|
8
|
+
}
|
|
9
|
+
export interface RecipeAgentDefinition {
|
|
10
|
+
name: string;
|
|
11
|
+
from?: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
model?: {
|
|
14
|
+
name?: string;
|
|
15
|
+
thinkingLevel?: string;
|
|
16
|
+
};
|
|
17
|
+
tools: string[];
|
|
18
|
+
skills: string[];
|
|
19
|
+
subagents: string[];
|
|
20
|
+
subagentsDeclared?: boolean;
|
|
21
|
+
extensions?: RecipeAgentExtensions;
|
|
22
|
+
systemInstructions?: RecipeSystemInstructions;
|
|
23
|
+
}
|
|
24
|
+
export type RequiredResolvedRecipeAgentField = "model.name" | "model.thinkingLevel" | "tools" | "skills" | "subagents" | "systemInstructions";
|
|
25
|
+
export declare const REQUIRED_RECIPE_AGENT_FIELDS: RequiredResolvedRecipeAgentField[];
|
|
26
|
+
export interface RecipeAgentValidationFinding {
|
|
27
|
+
agentName: string;
|
|
28
|
+
field: "name" | "from" | RequiredResolvedRecipeAgentField;
|
|
29
|
+
message: string;
|
|
30
|
+
}
|
|
31
|
+
export declare function loadRecipeAgentDefinitions(recipeDir: string): Map<string, RecipeAgentDefinition>;
|
|
32
|
+
export declare function validateResolvedRecipeAgentDefinition(opts: {
|
|
33
|
+
recipeDir: string;
|
|
34
|
+
agentName: string;
|
|
35
|
+
requireExplicitName?: boolean;
|
|
36
|
+
requiredFields?: RequiredResolvedRecipeAgentField[];
|
|
37
|
+
}): RecipeAgentValidationFinding[];
|
|
38
|
+
export declare function validateRecipeAgentDefinitions(recipeDir: string): RecipeAgentValidationFinding[];
|
|
39
|
+
export declare function resolveRecipeAgentName(opts: {
|
|
40
|
+
recipeDir: string;
|
|
41
|
+
agentName?: string;
|
|
42
|
+
}): string;
|
|
43
|
+
export declare function resolveRecipeAgentDefinition(opts: {
|
|
44
|
+
recipeDir: string;
|
|
45
|
+
agentName?: string;
|
|
46
|
+
}): {
|
|
47
|
+
agentName: string;
|
|
48
|
+
agent: RecipeAgentDefinition | null;
|
|
49
|
+
};
|
|
50
|
+
export declare function loadRecipeSystemPrompt(recipeDir: string): string | undefined;
|
|
51
|
+
//# sourceMappingURL=recipe-agent.d.ts.map
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { basename, join } from "node:path";
|
|
3
|
+
import { parse } from "yaml";
|
|
4
|
+
import { packageResourcePaths, readPiPackageManifest, RecipePackageError, } from "./recipe-package.js";
|
|
5
|
+
export const REQUIRED_RECIPE_AGENT_FIELDS = [
|
|
6
|
+
"model.name",
|
|
7
|
+
"model.thinkingLevel",
|
|
8
|
+
"tools",
|
|
9
|
+
"skills",
|
|
10
|
+
"subagents",
|
|
11
|
+
"systemInstructions",
|
|
12
|
+
];
|
|
13
|
+
function asRecord(value) {
|
|
14
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
15
|
+
? value
|
|
16
|
+
: {};
|
|
17
|
+
}
|
|
18
|
+
function stringArray(value) {
|
|
19
|
+
return Array.isArray(value)
|
|
20
|
+
? value.filter((item) => typeof item === "string")
|
|
21
|
+
: [];
|
|
22
|
+
}
|
|
23
|
+
function parseModel(data) {
|
|
24
|
+
const raw = asRecord(data.model);
|
|
25
|
+
const name = typeof raw.name === "string" ? raw.name.trim() : "";
|
|
26
|
+
const thinkingLevel = typeof raw.thinking_level === "string"
|
|
27
|
+
? raw.thinking_level.trim()
|
|
28
|
+
: typeof raw.thinkingLevel === "string"
|
|
29
|
+
? raw.thinkingLevel.trim()
|
|
30
|
+
: "";
|
|
31
|
+
if (!name && !thinkingLevel)
|
|
32
|
+
return undefined;
|
|
33
|
+
return {
|
|
34
|
+
...(name ? { name } : {}),
|
|
35
|
+
...(thinkingLevel ? { thinkingLevel } : {}),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function parseSystemInstructions(data) {
|
|
39
|
+
const raw = asRecord(data.system_instructions ?? data.systemInstructions);
|
|
40
|
+
const content = typeof raw.content === "string" ? raw.content.trim() : "";
|
|
41
|
+
if (!content) {
|
|
42
|
+
const prompt = typeof data.prompt === "string" ? data.prompt.trim() : "";
|
|
43
|
+
return prompt ? { mode: "append", content: prompt } : undefined;
|
|
44
|
+
}
|
|
45
|
+
const mode = raw.mode === "replace" ? "replace" : "append";
|
|
46
|
+
return { mode, content };
|
|
47
|
+
}
|
|
48
|
+
function parseExtensions(data) {
|
|
49
|
+
const raw = asRecord(data.extensions);
|
|
50
|
+
const extensions = {};
|
|
51
|
+
if (Object.hasOwn(raw, "include"))
|
|
52
|
+
extensions.include = stringArray(raw.include);
|
|
53
|
+
if (Object.hasOwn(raw, "exclude"))
|
|
54
|
+
extensions.exclude = stringArray(raw.exclude);
|
|
55
|
+
return extensions.include || extensions.exclude ? extensions : undefined;
|
|
56
|
+
}
|
|
57
|
+
function readYaml(path) {
|
|
58
|
+
return asRecord(parse(readFileSync(path, "utf8")));
|
|
59
|
+
}
|
|
60
|
+
function recipeManifest(recipeDir) {
|
|
61
|
+
try {
|
|
62
|
+
return readPiPackageManifest(recipeDir);
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
if (err instanceof RecipePackageError)
|
|
66
|
+
return null;
|
|
67
|
+
throw err;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function yamlFilesFromPaths(paths) {
|
|
71
|
+
const files = [];
|
|
72
|
+
for (const path of paths) {
|
|
73
|
+
if (!existsSync(path))
|
|
74
|
+
continue;
|
|
75
|
+
const stats = statSync(path);
|
|
76
|
+
if (stats.isFile() && /\.ya?ml$/i.test(path)) {
|
|
77
|
+
files.push(path);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (!stats.isDirectory())
|
|
81
|
+
continue;
|
|
82
|
+
for (const entry of readdirSync(path, { withFileTypes: true })) {
|
|
83
|
+
if (!entry.isFile() || !/\.ya?ml$/i.test(entry.name))
|
|
84
|
+
continue;
|
|
85
|
+
files.push(join(path, entry.name));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return files.sort();
|
|
89
|
+
}
|
|
90
|
+
function recipeAgentFiles(recipeDir) {
|
|
91
|
+
const manifest = recipeManifest(recipeDir);
|
|
92
|
+
if (manifest)
|
|
93
|
+
return yamlFilesFromPaths(packageResourcePaths(manifest, "agents"));
|
|
94
|
+
return yamlFilesFromPaths([join(recipeDir, "agents")]);
|
|
95
|
+
}
|
|
96
|
+
function readRecipeAgentSources(recipeDir) {
|
|
97
|
+
const sources = [];
|
|
98
|
+
for (const path of recipeAgentFiles(recipeDir)) {
|
|
99
|
+
const data = readYaml(path);
|
|
100
|
+
const fallbackName = basename(path).replace(/\.ya?ml$/i, "");
|
|
101
|
+
const explicitName = typeof data.name === "string" && Boolean(data.name.trim());
|
|
102
|
+
const name = explicitName ? data.name.trim() : fallbackName;
|
|
103
|
+
sources.push({
|
|
104
|
+
fallbackName,
|
|
105
|
+
explicitName,
|
|
106
|
+
definition: {
|
|
107
|
+
name,
|
|
108
|
+
from: typeof data.from === "string" && data.from.trim() ? data.from.trim() : undefined,
|
|
109
|
+
description: typeof data.description === "string" ? data.description : undefined,
|
|
110
|
+
model: parseModel(data),
|
|
111
|
+
tools: Object.hasOwn(data, "tools") ? stringArray(data.tools) : undefined,
|
|
112
|
+
skills: Object.hasOwn(data, "skills") ? stringArray(data.skills) : undefined,
|
|
113
|
+
subagents: Object.hasOwn(data, "subagents") ? stringArray(data.subagents) : undefined,
|
|
114
|
+
extensions: parseExtensions(data),
|
|
115
|
+
systemInstructions: parseSystemInstructions(data),
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
return sources;
|
|
120
|
+
}
|
|
121
|
+
export function loadRecipeAgentDefinitions(recipeDir) {
|
|
122
|
+
const rawDefinitions = new Map();
|
|
123
|
+
const aliases = new Map();
|
|
124
|
+
const resolvedDefinitions = new Map();
|
|
125
|
+
const definitions = new Map();
|
|
126
|
+
for (const source of readRecipeAgentSources(recipeDir)) {
|
|
127
|
+
rawDefinitions.set(source.definition.name, source.definition);
|
|
128
|
+
aliases.set(source.fallbackName, source.definition.name);
|
|
129
|
+
}
|
|
130
|
+
function resolveName(name) {
|
|
131
|
+
return rawDefinitions.has(name) ? name : aliases.get(name) ?? name;
|
|
132
|
+
}
|
|
133
|
+
function mergeModel(base, child) {
|
|
134
|
+
return base || child ? { ...base, ...child } : undefined;
|
|
135
|
+
}
|
|
136
|
+
function mergeExtensions(base, child) {
|
|
137
|
+
if (!base)
|
|
138
|
+
return child;
|
|
139
|
+
if (!child)
|
|
140
|
+
return base;
|
|
141
|
+
return {
|
|
142
|
+
...(base.include ? { include: base.include } : {}),
|
|
143
|
+
...(base.exclude ? { exclude: base.exclude } : {}),
|
|
144
|
+
...(child.include ? { include: child.include } : {}),
|
|
145
|
+
...(child.exclude ? { exclude: child.exclude } : {}),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function resolveDefinition(name, stack = []) {
|
|
149
|
+
const resolvedName = resolveName(name);
|
|
150
|
+
if (resolvedDefinitions.has(resolvedName))
|
|
151
|
+
return resolvedDefinitions.get(resolvedName);
|
|
152
|
+
if (stack.includes(resolvedName))
|
|
153
|
+
return undefined;
|
|
154
|
+
const raw = rawDefinitions.get(resolvedName);
|
|
155
|
+
if (!raw)
|
|
156
|
+
return undefined;
|
|
157
|
+
const base = raw.from
|
|
158
|
+
? resolveDefinition(raw.from, [...stack, resolvedName])
|
|
159
|
+
: undefined;
|
|
160
|
+
if (raw.from && !base)
|
|
161
|
+
return undefined;
|
|
162
|
+
const definition = {
|
|
163
|
+
name: raw.name,
|
|
164
|
+
...(raw.from ? { from: raw.from } : {}),
|
|
165
|
+
description: raw.description ?? base?.description,
|
|
166
|
+
model: mergeModel(base?.model, raw.model),
|
|
167
|
+
tools: raw.tools ?? base?.tools ?? [],
|
|
168
|
+
skills: raw.skills ?? base?.skills ?? [],
|
|
169
|
+
subagents: raw.subagents ?? base?.subagents ?? [],
|
|
170
|
+
subagentsDeclared: raw.subagents !== undefined || base?.subagentsDeclared === true,
|
|
171
|
+
extensions: mergeExtensions(base?.extensions, raw.extensions),
|
|
172
|
+
systemInstructions: raw.systemInstructions ?? base?.systemInstructions,
|
|
173
|
+
};
|
|
174
|
+
resolvedDefinitions.set(resolvedName, definition);
|
|
175
|
+
return definition;
|
|
176
|
+
}
|
|
177
|
+
for (const name of rawDefinitions.keys()) {
|
|
178
|
+
const definition = resolveDefinition(name);
|
|
179
|
+
if (!definition)
|
|
180
|
+
continue;
|
|
181
|
+
definitions.set(name, definition);
|
|
182
|
+
}
|
|
183
|
+
for (const [alias, name] of aliases) {
|
|
184
|
+
if (definitions.has(alias))
|
|
185
|
+
continue;
|
|
186
|
+
const definition = definitions.get(name);
|
|
187
|
+
if (definition)
|
|
188
|
+
definitions.set(alias, definition);
|
|
189
|
+
}
|
|
190
|
+
return definitions;
|
|
191
|
+
}
|
|
192
|
+
function recipeAgentFieldProvided(definition, field) {
|
|
193
|
+
if (field === "model.name")
|
|
194
|
+
return Boolean(definition.model?.name);
|
|
195
|
+
if (field === "model.thinkingLevel")
|
|
196
|
+
return Boolean(definition.model?.thinkingLevel);
|
|
197
|
+
if (field === "tools")
|
|
198
|
+
return definition.tools !== undefined;
|
|
199
|
+
if (field === "skills")
|
|
200
|
+
return definition.skills !== undefined;
|
|
201
|
+
if (field === "subagents")
|
|
202
|
+
return definition.subagents !== undefined;
|
|
203
|
+
return definition.systemInstructions !== undefined;
|
|
204
|
+
}
|
|
205
|
+
export function validateResolvedRecipeAgentDefinition(opts) {
|
|
206
|
+
const rawDefinitions = new Map();
|
|
207
|
+
const aliases = new Map();
|
|
208
|
+
const explicitNames = new Map();
|
|
209
|
+
for (const source of readRecipeAgentSources(opts.recipeDir)) {
|
|
210
|
+
rawDefinitions.set(source.definition.name, source.definition);
|
|
211
|
+
aliases.set(source.fallbackName, source.definition.name);
|
|
212
|
+
explicitNames.set(source.definition.name, source.explicitName);
|
|
213
|
+
}
|
|
214
|
+
function resolveName(name) {
|
|
215
|
+
return rawDefinitions.has(name) ? name : aliases.get(name) ?? name;
|
|
216
|
+
}
|
|
217
|
+
function inheritanceFinding(name, stack = []) {
|
|
218
|
+
const resolvedName = resolveName(name);
|
|
219
|
+
const definition = rawDefinitions.get(resolvedName);
|
|
220
|
+
if (!definition) {
|
|
221
|
+
return {
|
|
222
|
+
agentName: resolvedName,
|
|
223
|
+
field: "from",
|
|
224
|
+
message: `Recipe agent "${resolvedName}" was not found`,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
if (!definition.from)
|
|
228
|
+
return undefined;
|
|
229
|
+
const resolvedFrom = resolveName(definition.from);
|
|
230
|
+
if (stack.includes(resolvedFrom)) {
|
|
231
|
+
return {
|
|
232
|
+
agentName: resolvedName,
|
|
233
|
+
field: "from",
|
|
234
|
+
message: `Recipe agent "${resolvedName}" has cyclic from chain: ${[
|
|
235
|
+
...stack,
|
|
236
|
+
resolvedName,
|
|
237
|
+
resolvedFrom,
|
|
238
|
+
].join(" -> ")}`,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
if (!rawDefinitions.has(resolvedFrom)) {
|
|
242
|
+
return {
|
|
243
|
+
agentName: resolvedName,
|
|
244
|
+
field: "from",
|
|
245
|
+
message: `Recipe agent "${resolvedName}" inherits from missing agent "${definition.from}"`,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
return inheritanceFinding(definition.from, [...stack, resolvedName]);
|
|
249
|
+
}
|
|
250
|
+
function resolvedFieldProvided(name, field, stack = []) {
|
|
251
|
+
const resolvedName = resolveName(name);
|
|
252
|
+
if (stack.includes(resolvedName))
|
|
253
|
+
return false;
|
|
254
|
+
const definition = rawDefinitions.get(resolvedName);
|
|
255
|
+
if (!definition)
|
|
256
|
+
return false;
|
|
257
|
+
if (recipeAgentFieldProvided(definition, field))
|
|
258
|
+
return true;
|
|
259
|
+
return definition.from
|
|
260
|
+
? resolvedFieldProvided(definition.from, field, [...stack, resolvedName])
|
|
261
|
+
: false;
|
|
262
|
+
}
|
|
263
|
+
const agentName = resolveName(opts.agentName);
|
|
264
|
+
const findings = [];
|
|
265
|
+
if (opts.requireExplicitName && explicitNames.get(agentName) !== true) {
|
|
266
|
+
findings.push({
|
|
267
|
+
agentName,
|
|
268
|
+
field: "name",
|
|
269
|
+
message: `Recipe agent "${agentName}" must declare name`,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
const inheritance = inheritanceFinding(agentName);
|
|
273
|
+
if (inheritance)
|
|
274
|
+
findings.push(inheritance);
|
|
275
|
+
for (const field of opts.requiredFields ?? []) {
|
|
276
|
+
if (resolvedFieldProvided(agentName, field))
|
|
277
|
+
continue;
|
|
278
|
+
findings.push({
|
|
279
|
+
agentName,
|
|
280
|
+
field,
|
|
281
|
+
message: `Recipe agent "${agentName}" must declare ${field} directly or inherit it with from`,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
return findings;
|
|
285
|
+
}
|
|
286
|
+
function validateRecipeAgentNames(sources) {
|
|
287
|
+
const findings = [];
|
|
288
|
+
const explicitNameCounts = new Map();
|
|
289
|
+
const explicitNames = new Set();
|
|
290
|
+
for (const source of sources) {
|
|
291
|
+
if (!source.explicitName)
|
|
292
|
+
continue;
|
|
293
|
+
explicitNames.add(source.definition.name);
|
|
294
|
+
explicitNameCounts.set(source.definition.name, (explicitNameCounts.get(source.definition.name) ?? 0) + 1);
|
|
295
|
+
}
|
|
296
|
+
for (const [name, count] of explicitNameCounts) {
|
|
297
|
+
if (count <= 1)
|
|
298
|
+
continue;
|
|
299
|
+
findings.push({
|
|
300
|
+
agentName: name,
|
|
301
|
+
field: "name",
|
|
302
|
+
message: `Recipe agent name "${name}" is declared by multiple files`,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
for (const source of sources) {
|
|
306
|
+
if (source.fallbackName === source.definition.name ||
|
|
307
|
+
!explicitNames.has(source.fallbackName)) {
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
findings.push({
|
|
311
|
+
agentName: source.definition.name,
|
|
312
|
+
field: "name",
|
|
313
|
+
message: `Recipe agent file alias "${source.fallbackName}" conflicts with an explicit agent name`,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
return findings;
|
|
317
|
+
}
|
|
318
|
+
export function validateRecipeAgentDefinitions(recipeDir) {
|
|
319
|
+
const sources = readRecipeAgentSources(recipeDir);
|
|
320
|
+
const agentNames = [...new Set(sources.map((source) => source.definition.name))].sort();
|
|
321
|
+
return [
|
|
322
|
+
...validateRecipeAgentNames(sources),
|
|
323
|
+
...agentNames.flatMap((agentName) => validateResolvedRecipeAgentDefinition({
|
|
324
|
+
recipeDir,
|
|
325
|
+
agentName,
|
|
326
|
+
requireExplicitName: true,
|
|
327
|
+
requiredFields: REQUIRED_RECIPE_AGENT_FIELDS,
|
|
328
|
+
})),
|
|
329
|
+
];
|
|
330
|
+
}
|
|
331
|
+
export function resolveRecipeAgentName(opts) {
|
|
332
|
+
if (opts.agentName?.trim())
|
|
333
|
+
return opts.agentName.trim();
|
|
334
|
+
const definitions = loadRecipeAgentDefinitions(opts.recipeDir);
|
|
335
|
+
const defaultAgent = definitions.get("agent");
|
|
336
|
+
if (defaultAgent)
|
|
337
|
+
return defaultAgent.name;
|
|
338
|
+
const uniqueNames = [
|
|
339
|
+
...new Set([...definitions.values()].map((definition) => definition.name)),
|
|
340
|
+
];
|
|
341
|
+
if (uniqueNames.length === 1)
|
|
342
|
+
return uniqueNames[0];
|
|
343
|
+
if (uniqueNames.length === 0)
|
|
344
|
+
return "agent";
|
|
345
|
+
throw new Error("Recipe has multiple agents and no default entrypoint; add agents/agent.yaml or set PI_AGENT_NAME");
|
|
346
|
+
}
|
|
347
|
+
export function resolveRecipeAgentDefinition(opts) {
|
|
348
|
+
const agentName = resolveRecipeAgentName(opts);
|
|
349
|
+
const agent = loadRecipeAgentDefinitions(opts.recipeDir).get(agentName) ?? null;
|
|
350
|
+
return { agentName, agent };
|
|
351
|
+
}
|
|
352
|
+
export function loadRecipeSystemPrompt(recipeDir) {
|
|
353
|
+
const path = join(recipeDir, "SYSTEM.md");
|
|
354
|
+
if (!existsSync(path))
|
|
355
|
+
return undefined;
|
|
356
|
+
const content = readFileSync(path, "utf8").trim();
|
|
357
|
+
return content || undefined;
|
|
358
|
+
}
|
|
359
|
+
//# sourceMappingURL=recipe-agent.js.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type RecipePackageManifest, type RecipePackageResources, type RecipeValidationFinding } from "./recipe-package.js";
|
|
2
|
+
export interface RecipeScaffoldFile {
|
|
3
|
+
path: string;
|
|
4
|
+
action: "created" | "overwritten" | "existing";
|
|
5
|
+
}
|
|
6
|
+
export interface RecipeScaffoldResult {
|
|
7
|
+
recipeDir: string;
|
|
8
|
+
name: string;
|
|
9
|
+
files: RecipeScaffoldFile[];
|
|
10
|
+
}
|
|
11
|
+
export interface RecipeDevelopmentReport {
|
|
12
|
+
valid: boolean;
|
|
13
|
+
manifest: RecipePackageManifest;
|
|
14
|
+
findings: RecipeValidationFinding[];
|
|
15
|
+
resources: Partial<Record<keyof RecipePackageResources, string[]>>;
|
|
16
|
+
}
|
|
17
|
+
export interface RecipePublishGuide {
|
|
18
|
+
manifest: RecipePackageManifest;
|
|
19
|
+
report: RecipeDevelopmentReport;
|
|
20
|
+
sourceExamples: string[];
|
|
21
|
+
checklist: string[];
|
|
22
|
+
}
|
|
23
|
+
export declare function createRecipeScaffold(target: string, opts?: {
|
|
24
|
+
cwd?: string;
|
|
25
|
+
name?: string;
|
|
26
|
+
force?: boolean;
|
|
27
|
+
}): RecipeScaffoldResult;
|
|
28
|
+
export declare function validateRecipeDirectory(recipeDir: string): RecipeDevelopmentReport;
|
|
29
|
+
export declare function createRecipePublishGuide(recipeDir: string): RecipePublishGuide;
|
|
30
|
+
//# sourceMappingURL=recipe-dev.d.ts.map
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, statSync, writeFileSync, } from "node:fs";
|
|
2
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
3
|
+
import { loadRecipeAgentDefinitions, resolveRecipeAgentDefinition, validateRecipeAgentDefinitions, } from "./recipe-agent.js";
|
|
4
|
+
import { packageResourcePaths, readPiPackageManifest, validatePiPackageManifest, } from "./recipe-package.js";
|
|
5
|
+
const RESOURCE_KEYS = [
|
|
6
|
+
"agents",
|
|
7
|
+
"extensions",
|
|
8
|
+
"skills",
|
|
9
|
+
"prompts",
|
|
10
|
+
"themes",
|
|
11
|
+
];
|
|
12
|
+
function recipeNameFromTarget(target) {
|
|
13
|
+
const name = basename(resolve(target)).replace(/[^a-zA-Z0-9._-]+/g, "-");
|
|
14
|
+
return name && name !== "." && name !== ".." ? name : "my-recipe";
|
|
15
|
+
}
|
|
16
|
+
function finding(severity, code, message, packageName) {
|
|
17
|
+
return {
|
|
18
|
+
severity,
|
|
19
|
+
code,
|
|
20
|
+
message,
|
|
21
|
+
packageName,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function writeScaffoldFile(path, content, opts) {
|
|
25
|
+
const exists = existsSync(path);
|
|
26
|
+
if (exists && !opts.force)
|
|
27
|
+
return { path, action: "existing" };
|
|
28
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
29
|
+
writeFileSync(path, content);
|
|
30
|
+
return { path, action: exists ? "overwritten" : "created" };
|
|
31
|
+
}
|
|
32
|
+
function assertScaffoldTargetWritable(files, opts) {
|
|
33
|
+
if (opts.force)
|
|
34
|
+
return;
|
|
35
|
+
const existing = files.filter((path) => existsSync(path));
|
|
36
|
+
if (existing.length === 0)
|
|
37
|
+
return;
|
|
38
|
+
throw new Error([
|
|
39
|
+
"Recipe scaffold would overwrite existing files:",
|
|
40
|
+
...existing.map((path) => ` - ${path}`),
|
|
41
|
+
"Re-run with --force to overwrite them.",
|
|
42
|
+
].join("\n"));
|
|
43
|
+
}
|
|
44
|
+
export function createRecipeScaffold(target, opts = {}) {
|
|
45
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
46
|
+
const recipeDir = resolve(cwd, target);
|
|
47
|
+
const name = (opts.name?.trim() || recipeNameFromTarget(recipeDir)).replace(/[^a-zA-Z0-9._-]+/g, "-");
|
|
48
|
+
const files = [
|
|
49
|
+
join(recipeDir, "package.json"),
|
|
50
|
+
join(recipeDir, "SYSTEM.md"),
|
|
51
|
+
join(recipeDir, "agents", "agent.yaml"),
|
|
52
|
+
join(recipeDir, "README.md"),
|
|
53
|
+
];
|
|
54
|
+
if (existsSync(recipeDir) && !statSync(recipeDir).isDirectory()) {
|
|
55
|
+
throw new Error(`Recipe target exists and is not a directory: ${recipeDir}`);
|
|
56
|
+
}
|
|
57
|
+
assertScaffoldTargetWritable(files, opts);
|
|
58
|
+
mkdirSync(join(recipeDir, "agents"), { recursive: true });
|
|
59
|
+
const written = [
|
|
60
|
+
writeScaffoldFile(files[0], `${JSON.stringify({
|
|
61
|
+
name,
|
|
62
|
+
version: "0.1.0",
|
|
63
|
+
description: "Describe what this recipe helps users do.",
|
|
64
|
+
type: "module",
|
|
65
|
+
pi: {
|
|
66
|
+
agents: ["agents/*.yaml"],
|
|
67
|
+
},
|
|
68
|
+
}, null, 2)}\n`, opts),
|
|
69
|
+
writeScaffoldFile(files[1], [
|
|
70
|
+
"# Recipe Workflow",
|
|
71
|
+
"",
|
|
72
|
+
"Describe the durable workflow guidance this recipe should add to the session.",
|
|
73
|
+
"",
|
|
74
|
+
].join("\n"), opts),
|
|
75
|
+
writeScaffoldFile(files[2], [
|
|
76
|
+
"name: agent",
|
|
77
|
+
"description: Main recipe agent.",
|
|
78
|
+
"model:",
|
|
79
|
+
" name: openai/gpt-5.4",
|
|
80
|
+
" thinking_level: medium",
|
|
81
|
+
"tools:",
|
|
82
|
+
" - read",
|
|
83
|
+
" - bash",
|
|
84
|
+
"skills: []",
|
|
85
|
+
"subagents: []",
|
|
86
|
+
"system_instructions:",
|
|
87
|
+
" mode: append",
|
|
88
|
+
" content: |",
|
|
89
|
+
" Follow the recipe workflow.",
|
|
90
|
+
"",
|
|
91
|
+
].join("\n"), opts),
|
|
92
|
+
writeScaffoldFile(files[3], [
|
|
93
|
+
`# ${name}`,
|
|
94
|
+
"",
|
|
95
|
+
"## Develop",
|
|
96
|
+
"",
|
|
97
|
+
"```bash",
|
|
98
|
+
"recipes doctor .",
|
|
99
|
+
"recipes install .",
|
|
100
|
+
`pi --recipe ${name}`,
|
|
101
|
+
"```",
|
|
102
|
+
"",
|
|
103
|
+
"## Publish",
|
|
104
|
+
"",
|
|
105
|
+
"Commit this directory to Git and share the repository locator:",
|
|
106
|
+
"",
|
|
107
|
+
"```bash",
|
|
108
|
+
`recipes install github:owner/${name}`,
|
|
109
|
+
"```",
|
|
110
|
+
"",
|
|
111
|
+
].join("\n"), opts),
|
|
112
|
+
];
|
|
113
|
+
return { recipeDir, name, files: written };
|
|
114
|
+
}
|
|
115
|
+
export function validateRecipeDirectory(recipeDir) {
|
|
116
|
+
const manifest = readPiPackageManifest(recipeDir);
|
|
117
|
+
const baseReport = validatePiPackageManifest(manifest);
|
|
118
|
+
const findings = [...baseReport.findings];
|
|
119
|
+
const resources = {};
|
|
120
|
+
for (const key of RESOURCE_KEYS) {
|
|
121
|
+
try {
|
|
122
|
+
resources[key] = packageResourcePaths(manifest, key);
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
findings.push(finding("error", `package.${key}_invalid`, err instanceof Error ? err.message : String(err), manifest.name));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
const agents = loadRecipeAgentDefinitions(recipeDir);
|
|
130
|
+
const uniqueAgents = new Set([...agents.values()].map((agent) => agent.name));
|
|
131
|
+
if (uniqueAgents.size === 0) {
|
|
132
|
+
findings.push(finding("warning", "agent.none_loaded", "No recipe agent definitions were loaded", manifest.name));
|
|
133
|
+
}
|
|
134
|
+
for (const agentFinding of validateRecipeAgentDefinitions(recipeDir)) {
|
|
135
|
+
findings.push(finding("error", `agent.${agentFinding.field}_missing`, agentFinding.message, manifest.name));
|
|
136
|
+
}
|
|
137
|
+
if (uniqueAgents.size > 0) {
|
|
138
|
+
try {
|
|
139
|
+
resolveRecipeAgentDefinition({ recipeDir });
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
findings.push(finding("warning", "agent.default_missing", err instanceof Error ? err.message : String(err), manifest.name));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
findings.push(finding("error", "agent.invalid", err instanceof Error ? err.message : String(err), manifest.name));
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
valid: findings.every((item) => item.severity !== "error"),
|
|
151
|
+
manifest,
|
|
152
|
+
findings,
|
|
153
|
+
resources,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
export function createRecipePublishGuide(recipeDir) {
|
|
157
|
+
const report = validateRecipeDirectory(recipeDir);
|
|
158
|
+
const name = report.manifest.name;
|
|
159
|
+
const repositoryName = name.startsWith("@")
|
|
160
|
+
? name.slice(1).split("/").at(-1) ?? name.slice(1)
|
|
161
|
+
: name;
|
|
162
|
+
return {
|
|
163
|
+
manifest: report.manifest,
|
|
164
|
+
report,
|
|
165
|
+
checklist: [
|
|
166
|
+
"Run `recipes doctor .` and fix any errors.",
|
|
167
|
+
`Run \`recipes publish . --github owner/${repositoryName} --visibility private\` to create, commit, and push a GitHub repository.`,
|
|
168
|
+
"Commit package.json, agents, prompts, skills, themes, extensions, and SYSTEM.md.",
|
|
169
|
+
"If extensions have runtime dependencies, commit the package lockfile.",
|
|
170
|
+
"Push the recipe to a Git repository.",
|
|
171
|
+
"Tag releases when users should install a stable version.",
|
|
172
|
+
],
|
|
173
|
+
sourceExamples: [
|
|
174
|
+
`recipes install github:owner/${repositoryName}`,
|
|
175
|
+
`recipes install github:owner/${repositoryName}#v${report.manifest.version}`,
|
|
176
|
+
`recipes install git@github.com:owner/${repositoryName}.git`,
|
|
177
|
+
],
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
//# sourceMappingURL=recipe-dev.js.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export interface RecipePackageResources {
|
|
2
|
+
agents: string[];
|
|
3
|
+
extensions: string[];
|
|
4
|
+
skills: string[];
|
|
5
|
+
prompts: string[];
|
|
6
|
+
themes: string[];
|
|
7
|
+
}
|
|
8
|
+
export interface RecipePackageManifest {
|
|
9
|
+
name: string;
|
|
10
|
+
version: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
path: string;
|
|
13
|
+
resources: RecipePackageResources;
|
|
14
|
+
}
|
|
15
|
+
export type PiPackageResources = RecipePackageResources;
|
|
16
|
+
export type PiPackageManifest = RecipePackageManifest;
|
|
17
|
+
export type RecipeValidationSeverity = "error" | "warning";
|
|
18
|
+
export interface RecipeValidationFinding {
|
|
19
|
+
severity: RecipeValidationSeverity;
|
|
20
|
+
code: string;
|
|
21
|
+
message: string;
|
|
22
|
+
packageName?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface RecipeValidationReport {
|
|
25
|
+
valid: boolean;
|
|
26
|
+
findings: RecipeValidationFinding[];
|
|
27
|
+
}
|
|
28
|
+
export declare class RecipePackageError extends Error {
|
|
29
|
+
constructor(message: string);
|
|
30
|
+
}
|
|
31
|
+
export declare function readPiPackageManifest(packageDir: string): RecipePackageManifest;
|
|
32
|
+
export declare function resolvePiPackageResourcePaths(pkg: RecipePackageManifest, key: keyof RecipePackageResources, opts?: {
|
|
33
|
+
allowEmptyGlobMatches?: boolean;
|
|
34
|
+
}): string[];
|
|
35
|
+
export declare function defaultPiPackageResourcePaths(pkg: RecipePackageManifest, key: keyof RecipePackageResources): string[];
|
|
36
|
+
export declare function packageResourcePaths(pkg: RecipePackageManifest, key: keyof RecipePackageResources): string[];
|
|
37
|
+
export declare function validatePiPackageManifest(pkg: RecipePackageManifest): RecipeValidationReport;
|
|
38
|
+
//# sourceMappingURL=recipe-package.d.ts.map
|