@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
package/README.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# @introspection-ai/pi-recipes
|
|
2
|
+
|
|
3
|
+
Experimental Pi recipe tooling with a `recipes` CLI and Pi extension
|
|
4
|
+
support.
|
|
5
|
+
|
|
6
|
+
Recipes are Pi packages: folders with a `package.json` manifest containing a
|
|
7
|
+
`pi` block, plus agent YAML files, prompts, skills, and optional TypeScript
|
|
8
|
+
runtime extensions. The `recipes` CLI installs or registers recipes in a
|
|
9
|
+
local store and ensures the Pi extension is installed before recipes are run.
|
|
10
|
+
The Pi extension resolves an installed recipe into a local directory and wires
|
|
11
|
+
those recipe files into the live Pi session at launch time.
|
|
12
|
+
|
|
13
|
+
`package.json` owns both recipe identity and Node dependency metadata. The
|
|
14
|
+
top-level `name`, `version`, and `description` identify the recipe, while the
|
|
15
|
+
`pi` block declares recipe resources such as agents, extensions, skills,
|
|
16
|
+
prompts, and themes.
|
|
17
|
+
|
|
18
|
+
## Documentation
|
|
19
|
+
|
|
20
|
+
- [Recipe Flow](docs/recipe-flow.md): quick user-facing guide to installing, customizing, creating, and publishing recipes.
|
|
21
|
+
- [Recipe CLI](docs/recipe-cli.md): creating, installing, resolving, publishing, and removing recipes.
|
|
22
|
+
- [Pi Recipe Extension](docs/pi-extension.md): installing the Pi extension, launching recipes, agent selection, resources, subagents, and recipe extension loading.
|
|
23
|
+
|
|
24
|
+
## Package Exports
|
|
25
|
+
|
|
26
|
+
- `@introspection-ai/pi-recipes`: extension factory and recipe-loading helpers.
|
|
27
|
+
- `@introspection-ai/pi-recipes/pi-extension`: Pi extension entrypoint.
|
|
28
|
+
- `@introspection-ai/pi-recipes/recipe-store`: recipe install and resolution helpers.
|
|
29
|
+
|
|
30
|
+
## Quick Start
|
|
31
|
+
|
|
32
|
+
Install the recipe tooling:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm install -g @introspection-ai/pi-recipes
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The first `recipes install ...` run automatically installs the companion Pi
|
|
39
|
+
extension with `pi install npm:@introspection-ai/pi-recipes`.
|
|
40
|
+
|
|
41
|
+
Create a local recipe:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
recipes create ./my-recipe
|
|
45
|
+
recipes doctor ./my-recipe
|
|
46
|
+
recipes install ./my-recipe
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Launch it with Pi:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pi --recipe my-recipe
|
|
53
|
+
pi --recipe my-recipe --agent agent
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`recipes create` writes a starter `package.json`, `SYSTEM.md`, `agents/agent.yaml`,
|
|
57
|
+
and recipe README. Edit those files as the recipe grows. Named variants are
|
|
58
|
+
agents too:
|
|
59
|
+
|
|
60
|
+
```yaml
|
|
61
|
+
name: agent-opus
|
|
62
|
+
from: agent
|
|
63
|
+
model:
|
|
64
|
+
name: openrouter/anthropic/claude-opus-4.8
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Install Recipes
|
|
68
|
+
|
|
69
|
+
Install public GitHub recipes:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
recipes install github:owner/repo
|
|
73
|
+
recipes install github:owner/repo/path/to/recipe
|
|
74
|
+
recipes install github:owner/repo#v1.0.0
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Install private recipes with normal Git authentication:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
recipes install git@github.com:owner/private-recipe.git
|
|
81
|
+
GITHUB_TOKEN=... recipes install github:owner/private-recipe
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
No recipe registry is required. Publishing a recipe creates or updates a GitHub
|
|
85
|
+
repository and pushes the local recipe:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
recipes publish ./my-recipe --github owner/my-recipe --visibility private
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Customize an installed recipe into an editable local copy:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
recipes customize pi-codex
|
|
95
|
+
recipes doctor pi-codex
|
|
96
|
+
pi --recipe pi-codex
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Development
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
pnpm install
|
|
103
|
+
pnpm typecheck
|
|
104
|
+
pnpm test
|
|
105
|
+
pnpm build
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Install from a local clone into Pi:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
pnpm build
|
|
112
|
+
pi install "$(pwd)"
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Pi records the local package path in `~/.pi/agent/settings.json`. Re-run
|
|
116
|
+
`pnpm build` after changing extension source.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { AuthStorage, type ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
export interface CreateRecipeChildAgentRunnerOptions {
|
|
3
|
+
recipeDir: string;
|
|
4
|
+
workspaceDir: string;
|
|
5
|
+
agentName: string;
|
|
6
|
+
env?: NodeJS.ProcessEnv;
|
|
7
|
+
authStorage?: AuthStorage;
|
|
8
|
+
modelRegistry?: ModelRegistry;
|
|
9
|
+
onAssistantMessage?: (text: string, stream: "delta" | "final") => void;
|
|
10
|
+
onToolEvent?: (event: RecipeChildToolEvent) => void;
|
|
11
|
+
}
|
|
12
|
+
export interface RecipeChildAgentRunner {
|
|
13
|
+
start(): Promise<void>;
|
|
14
|
+
prompt(task: string): Promise<string>;
|
|
15
|
+
cancel(): Promise<void>;
|
|
16
|
+
shutdown(): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
export type CreateRecipeChildAgentRunner = (opts: CreateRecipeChildAgentRunnerOptions) => RecipeChildAgentRunner;
|
|
19
|
+
export type RecipeChildToolEvent = {
|
|
20
|
+
type: "start";
|
|
21
|
+
id: string;
|
|
22
|
+
name: string;
|
|
23
|
+
args: unknown;
|
|
24
|
+
} | {
|
|
25
|
+
type: "update";
|
|
26
|
+
id: string;
|
|
27
|
+
name: string;
|
|
28
|
+
args: unknown;
|
|
29
|
+
partialResult: unknown;
|
|
30
|
+
} | {
|
|
31
|
+
type: "end";
|
|
32
|
+
id: string;
|
|
33
|
+
name: string;
|
|
34
|
+
args: unknown;
|
|
35
|
+
result: unknown;
|
|
36
|
+
isError: boolean;
|
|
37
|
+
};
|
|
38
|
+
export declare function promptResultText(result: unknown): string;
|
|
39
|
+
export declare function createRecipeChildAgentRunner(opts: CreateRecipeChildAgentRunnerOptions): RecipeChildAgentRunner;
|
|
40
|
+
//# sourceMappingURL=child-agent.d.ts.map
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { getEnvApiKey, getModel } from "@earendil-works/pi-ai";
|
|
2
|
+
import { AuthStorage, createAgentSessionFromServices, createAgentSessionServices, SessionManager, SettingsManager, } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { loadRecipeSystemPrompt, REQUIRED_RECIPE_AGENT_FIELDS, resolveRecipeAgentDefinition, validateResolvedRecipeAgentDefinition, } from "./recipe-agent.js";
|
|
4
|
+
function parseModelSpec(spec) {
|
|
5
|
+
const slash = spec.indexOf("/");
|
|
6
|
+
if (slash < 0) {
|
|
7
|
+
throw new Error(`Invalid recipe model "${spec}" - expected "<provider>/<model_id>"`);
|
|
8
|
+
}
|
|
9
|
+
const provider = spec.slice(0, slash);
|
|
10
|
+
const modelId = spec.slice(slash + 1);
|
|
11
|
+
const lookupProvider = provider === "gemini" ? "google" : provider;
|
|
12
|
+
return { provider, modelId, lookupProvider };
|
|
13
|
+
}
|
|
14
|
+
function modelFromSpec(spec, modelRegistry) {
|
|
15
|
+
const { modelId, lookupProvider } = parseModelSpec(spec);
|
|
16
|
+
return (modelRegistry?.find(lookupProvider, modelId) ??
|
|
17
|
+
getModel(lookupProvider, modelId));
|
|
18
|
+
}
|
|
19
|
+
function authStorageForChildAgent(model, opts) {
|
|
20
|
+
if (opts.authStorage)
|
|
21
|
+
return opts.authStorage;
|
|
22
|
+
if (opts.modelRegistry)
|
|
23
|
+
return opts.modelRegistry.authStorage;
|
|
24
|
+
const env = opts.env ?? process.env;
|
|
25
|
+
const apiKey = getEnvApiKey(model.provider) ?? env[`${model.provider.toUpperCase()}_API_KEY`];
|
|
26
|
+
if (!apiKey) {
|
|
27
|
+
throw new Error(`${model.provider.toUpperCase()}_API_KEY is required when the recipe child agent is not running inside Pi`);
|
|
28
|
+
}
|
|
29
|
+
const authStorage = AuthStorage.inMemory();
|
|
30
|
+
authStorage.setRuntimeApiKey(model.provider, apiKey);
|
|
31
|
+
return authStorage;
|
|
32
|
+
}
|
|
33
|
+
function applySystemInstructions(base, instructions) {
|
|
34
|
+
if (!instructions)
|
|
35
|
+
return base;
|
|
36
|
+
if (instructions.mode === "replace")
|
|
37
|
+
return instructions.content;
|
|
38
|
+
return [base, instructions.content].filter(Boolean).join("\n\n");
|
|
39
|
+
}
|
|
40
|
+
function runtimeContextPrompt(workspaceDir, recipeDir) {
|
|
41
|
+
return [
|
|
42
|
+
"## Recipe Runtime Context",
|
|
43
|
+
"- Current workspace: " + workspaceDir,
|
|
44
|
+
"- Recipe directory: " + recipeDir,
|
|
45
|
+
].join("\n");
|
|
46
|
+
}
|
|
47
|
+
function asRecord(value) {
|
|
48
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
49
|
+
? value
|
|
50
|
+
: null;
|
|
51
|
+
}
|
|
52
|
+
function contentText(content) {
|
|
53
|
+
if (typeof content === "string")
|
|
54
|
+
return content;
|
|
55
|
+
if (!Array.isArray(content))
|
|
56
|
+
return "";
|
|
57
|
+
return content
|
|
58
|
+
.map((part) => {
|
|
59
|
+
const record = asRecord(part);
|
|
60
|
+
return record?.type === "text" && typeof record.text === "string"
|
|
61
|
+
? record.text
|
|
62
|
+
: "";
|
|
63
|
+
})
|
|
64
|
+
.filter(Boolean)
|
|
65
|
+
.join("\n");
|
|
66
|
+
}
|
|
67
|
+
export function promptResultText(result) {
|
|
68
|
+
if (typeof result === "string")
|
|
69
|
+
return result;
|
|
70
|
+
if (!result || typeof result !== "object")
|
|
71
|
+
return "";
|
|
72
|
+
const record = result;
|
|
73
|
+
if (typeof record.output === "string")
|
|
74
|
+
return record.output;
|
|
75
|
+
const messages = Array.isArray(record.messages) ? record.messages : [];
|
|
76
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
77
|
+
const message = asRecord(messages[index]);
|
|
78
|
+
if (!message)
|
|
79
|
+
continue;
|
|
80
|
+
if (message.role && message.role !== "assistant")
|
|
81
|
+
continue;
|
|
82
|
+
const text = contentText(message.content).trim();
|
|
83
|
+
if (text)
|
|
84
|
+
return text;
|
|
85
|
+
}
|
|
86
|
+
return "";
|
|
87
|
+
}
|
|
88
|
+
function messageFromEvent(event) {
|
|
89
|
+
const record = asRecord(event);
|
|
90
|
+
const direct = asRecord(record?.message);
|
|
91
|
+
if (direct)
|
|
92
|
+
return direct;
|
|
93
|
+
const assistantEvent = asRecord(record?.assistantMessageEvent);
|
|
94
|
+
return asRecord(assistantEvent?.partial);
|
|
95
|
+
}
|
|
96
|
+
class RecipeChildAgentSessionRunner {
|
|
97
|
+
opts;
|
|
98
|
+
session = null;
|
|
99
|
+
unsubscribe = null;
|
|
100
|
+
assistantStreamedText = false;
|
|
101
|
+
constructor(opts) {
|
|
102
|
+
this.opts = opts;
|
|
103
|
+
}
|
|
104
|
+
emitAssistantText(text, stream) {
|
|
105
|
+
const trimmed = text.trim();
|
|
106
|
+
if (!trimmed)
|
|
107
|
+
return;
|
|
108
|
+
this.opts.onAssistantMessage?.(trimmed, stream);
|
|
109
|
+
if (stream === "delta")
|
|
110
|
+
this.assistantStreamedText = true;
|
|
111
|
+
}
|
|
112
|
+
handleSessionEvent(event) {
|
|
113
|
+
const record = asRecord(event);
|
|
114
|
+
const message = messageFromEvent(event);
|
|
115
|
+
const role = typeof message?.role === "string" ? message.role : undefined;
|
|
116
|
+
if (record?.type === "tool_execution_start") {
|
|
117
|
+
this.opts.onToolEvent?.({
|
|
118
|
+
type: "start",
|
|
119
|
+
id: String(record.toolCallId ?? ""),
|
|
120
|
+
name: String(record.toolName ?? ""),
|
|
121
|
+
args: record.args,
|
|
122
|
+
});
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (record?.type === "tool_execution_update") {
|
|
126
|
+
this.opts.onToolEvent?.({
|
|
127
|
+
type: "update",
|
|
128
|
+
id: String(record.toolCallId ?? ""),
|
|
129
|
+
name: String(record.toolName ?? ""),
|
|
130
|
+
args: record.args,
|
|
131
|
+
partialResult: record.partialResult,
|
|
132
|
+
});
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (record?.type === "tool_execution_end") {
|
|
136
|
+
this.opts.onToolEvent?.({
|
|
137
|
+
type: "end",
|
|
138
|
+
id: String(record.toolCallId ?? ""),
|
|
139
|
+
name: String(record.toolName ?? ""),
|
|
140
|
+
args: record.args,
|
|
141
|
+
result: record.result,
|
|
142
|
+
isError: record.isError === true,
|
|
143
|
+
});
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (record?.type === "message_start" && role === "assistant") {
|
|
147
|
+
this.assistantStreamedText = false;
|
|
148
|
+
}
|
|
149
|
+
const assistantEvent = asRecord(record?.assistantMessageEvent);
|
|
150
|
+
if (role === "assistant" &&
|
|
151
|
+
record?.type === "message_update" &&
|
|
152
|
+
assistantEvent?.type === "text_delta" &&
|
|
153
|
+
typeof assistantEvent.delta === "string") {
|
|
154
|
+
this.emitAssistantText(assistantEvent.delta, "delta");
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (role === "assistant" &&
|
|
158
|
+
record?.type === "message_end" &&
|
|
159
|
+
!this.assistantStreamedText) {
|
|
160
|
+
this.emitAssistantText(contentText(message?.content), "final");
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
async start() {
|
|
164
|
+
if (this.session)
|
|
165
|
+
return;
|
|
166
|
+
const { agentName, agent } = resolveRecipeAgentDefinition({
|
|
167
|
+
recipeDir: this.opts.recipeDir,
|
|
168
|
+
agentName: this.opts.agentName,
|
|
169
|
+
});
|
|
170
|
+
if (!agent) {
|
|
171
|
+
throw new Error(`Recipe agent not found: ${agentName}`);
|
|
172
|
+
}
|
|
173
|
+
const validationFindings = validateResolvedRecipeAgentDefinition({
|
|
174
|
+
recipeDir: this.opts.recipeDir,
|
|
175
|
+
agentName,
|
|
176
|
+
requireExplicitName: true,
|
|
177
|
+
requiredFields: REQUIRED_RECIPE_AGENT_FIELDS,
|
|
178
|
+
});
|
|
179
|
+
if (validationFindings.length > 0) {
|
|
180
|
+
throw new Error(validationFindings.map((finding) => finding.message).join("\n"));
|
|
181
|
+
}
|
|
182
|
+
const modelSpec = agent.model?.name;
|
|
183
|
+
if (!modelSpec) {
|
|
184
|
+
throw new Error(`Recipe agent "${agentName}" must declare model.name`);
|
|
185
|
+
}
|
|
186
|
+
const model = modelFromSpec(modelSpec, this.opts.modelRegistry);
|
|
187
|
+
const authStorage = authStorageForChildAgent(model, this.opts);
|
|
188
|
+
const services = await createAgentSessionServices({
|
|
189
|
+
cwd: this.opts.workspaceDir,
|
|
190
|
+
agentDir: this.opts.recipeDir,
|
|
191
|
+
authStorage,
|
|
192
|
+
modelRegistry: this.opts.modelRegistry,
|
|
193
|
+
settingsManager: SettingsManager.create(this.opts.workspaceDir, this.opts.recipeDir),
|
|
194
|
+
resourceLoaderOptions: {
|
|
195
|
+
systemPromptOverride: (base) => applySystemInstructions(loadRecipeSystemPrompt(this.opts.recipeDir) ?? base, agent.systemInstructions),
|
|
196
|
+
appendSystemPromptOverride: (base) => [
|
|
197
|
+
...base,
|
|
198
|
+
runtimeContextPrompt(this.opts.workspaceDir, this.opts.recipeDir),
|
|
199
|
+
],
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
const created = await createAgentSessionFromServices({
|
|
203
|
+
services,
|
|
204
|
+
sessionManager: SessionManager.inMemory(this.opts.workspaceDir),
|
|
205
|
+
model,
|
|
206
|
+
thinkingLevel: (agent.model?.thinkingLevel ?? "low"),
|
|
207
|
+
tools: agent.tools.length > 0 ? agent.tools : undefined,
|
|
208
|
+
});
|
|
209
|
+
this.session = created.session;
|
|
210
|
+
await this.session.bindExtensions({});
|
|
211
|
+
this.unsubscribe = this.session.subscribe((event) => {
|
|
212
|
+
this.handleSessionEvent(event);
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
async prompt(task) {
|
|
216
|
+
await this.start();
|
|
217
|
+
if (!this.session)
|
|
218
|
+
throw new Error("Recipe child agent did not start");
|
|
219
|
+
await this.session.prompt(task);
|
|
220
|
+
return promptResultText({ messages: [...this.session.messages] });
|
|
221
|
+
}
|
|
222
|
+
async cancel() {
|
|
223
|
+
await this.session?.abort();
|
|
224
|
+
}
|
|
225
|
+
async shutdown() {
|
|
226
|
+
this.unsubscribe?.();
|
|
227
|
+
this.unsubscribe = null;
|
|
228
|
+
this.session?.dispose();
|
|
229
|
+
this.session = null;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
export function createRecipeChildAgentRunner(opts) {
|
|
233
|
+
return new RecipeChildAgentSessionRunner(opts);
|
|
234
|
+
}
|
|
235
|
+
//# sourceMappingURL=child-agent.js.map
|
package/dist/cli.d.ts
ADDED