@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 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
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map