@johnnygreco/pizza-pi 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/LICENSE +191 -0
  2. package/README.md +82 -0
  3. package/extensions/context.ts +578 -0
  4. package/extensions/control.ts +1782 -0
  5. package/extensions/loop.ts +454 -0
  6. package/extensions/pizza-ui.ts +93 -0
  7. package/extensions/todos.ts +2066 -0
  8. package/node_modules/pi-interactive-subagents/.pi/settings.json +13 -0
  9. package/node_modules/pi-interactive-subagents/.pi/skills/release/SKILL.md +133 -0
  10. package/node_modules/pi-interactive-subagents/LICENSE +21 -0
  11. package/node_modules/pi-interactive-subagents/README.md +362 -0
  12. package/node_modules/pi-interactive-subagents/agents/planner.md +270 -0
  13. package/node_modules/pi-interactive-subagents/agents/reviewer.md +153 -0
  14. package/node_modules/pi-interactive-subagents/agents/scout.md +103 -0
  15. package/node_modules/pi-interactive-subagents/agents/spec.md +339 -0
  16. package/node_modules/pi-interactive-subagents/agents/visual-tester.md +202 -0
  17. package/node_modules/pi-interactive-subagents/agents/worker.md +104 -0
  18. package/node_modules/pi-interactive-subagents/package.json +34 -0
  19. package/node_modules/pi-interactive-subagents/pi-extension/session-artifacts/index.ts +252 -0
  20. package/node_modules/pi-interactive-subagents/pi-extension/subagents/cmux.ts +647 -0
  21. package/node_modules/pi-interactive-subagents/pi-extension/subagents/index.ts +1343 -0
  22. package/node_modules/pi-interactive-subagents/pi-extension/subagents/plan-skill.md +225 -0
  23. package/node_modules/pi-interactive-subagents/pi-extension/subagents/session.ts +124 -0
  24. package/node_modules/pi-interactive-subagents/pi-extension/subagents/subagent-done.ts +166 -0
  25. package/package.json +62 -0
  26. package/prompts/.gitkeep +0 -0
  27. package/skills/.gitkeep +0 -0
@@ -0,0 +1,252 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ import { highlightCode, getLanguageFromPath, keyHint } from "@mariozechner/pi-coding-agent";
3
+ import { Text } from "@mariozechner/pi-tui";
4
+ import { Type } from "@sinclair/typebox";
5
+ import { mkdirSync, writeFileSync, readFileSync, existsSync, readdirSync, statSync } from "node:fs";
6
+ import { dirname, join, resolve } from "node:path";
7
+
8
+ const PREVIEW_LINES = 10;
9
+
10
+ export default function (pi: ExtensionAPI) {
11
+ pi.registerTool({
12
+ name: "write_artifact",
13
+ label: "Write Artifact",
14
+ description:
15
+ "Write a session-scoped artifact file (plan, context, research, notes, etc.). " +
16
+ "Files are stored under <sessionDir>/artifacts/<session-id>/. " +
17
+ "Use this instead of writing pi working files directly.",
18
+ promptSnippet:
19
+ "Write a session-scoped artifact file (plan, context, research, notes, etc.). " +
20
+ "Files are stored under <sessionDir>/artifacts/<session-id>/. " +
21
+ "Use this instead of writing pi working files directly.",
22
+ promptGuidelines: [
23
+ "Use write_artifact for any pi working file: plans, scout context, research notes, reviews, or other session artifacts.",
24
+ "The name param can include subdirectories (e.g. 'context/auth-flow.md').",
25
+ ],
26
+ parameters: Type.Object({
27
+ name: Type.String({ description: "Filename, e.g. 'plan.md' or 'context/auth-flow.md'" }),
28
+ content: Type.String({ description: "File content" }),
29
+ }),
30
+
31
+ renderCall(args, theme) {
32
+ const name = args.name ?? "...";
33
+ const content = args.content ?? "";
34
+
35
+ let text =
36
+ theme.fg("toolTitle", theme.bold("write_artifact")) + " " + theme.fg("accent", name);
37
+
38
+ if (content) {
39
+ const lang = getLanguageFromPath(name);
40
+ const lines = lang ? highlightCode(content, lang) : content.split("\n");
41
+ const totalLines = lines.length;
42
+ // During streaming, show preview
43
+ const displayLines = lines.slice(0, PREVIEW_LINES);
44
+ const remaining = totalLines - PREVIEW_LINES;
45
+
46
+ text +=
47
+ "\n\n" +
48
+ displayLines
49
+ .map((line: string) => (lang ? line : theme.fg("toolOutput", line)))
50
+ .join("\n");
51
+
52
+ if (remaining > 0) {
53
+ text += theme.fg("muted", `\n... (${remaining} more lines, ${totalLines} total)`);
54
+ }
55
+ }
56
+
57
+ return new Text(text, 0, 0);
58
+ },
59
+
60
+ renderResult(result, _opts, theme) {
61
+ const details = result.details as { path?: string; name?: string } | undefined;
62
+ const text =
63
+ theme.fg("success", "✓") +
64
+ " " +
65
+ theme.fg("accent", details?.path ?? details?.name ?? "artifact");
66
+ return new Text(text, 0, 0);
67
+ },
68
+
69
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
70
+ const sessionDir = ctx.sessionManager.getSessionDir();
71
+ const sessionId = ctx.sessionManager.getSessionId();
72
+ const artifactDir = join(sessionDir, "artifacts", sessionId);
73
+ const filePath = resolve(artifactDir, params.name);
74
+
75
+ // Safety: ensure we're not escaping the artifact directory
76
+ if (!filePath.startsWith(artifactDir)) {
77
+ throw new Error(`Path escapes artifact directory: ${params.name}`);
78
+ }
79
+
80
+ mkdirSync(dirname(filePath), { recursive: true });
81
+ writeFileSync(filePath, params.content, "utf-8");
82
+
83
+ return {
84
+ content: [{ type: "text", text: `Artifact written to: ${filePath}` }],
85
+ details: { path: filePath, name: params.name, sessionId },
86
+ };
87
+ },
88
+ });
89
+
90
+ /**
91
+ * Find an artifact by name across all session artifact directories for the current project.
92
+ * Searches current session first, then other sessions (most recently modified first).
93
+ */
94
+ function findArtifact(
95
+ projectArtifactsDir: string,
96
+ currentSessionId: string,
97
+ name: string,
98
+ ): string | null {
99
+ // 1. Check current session first
100
+ const currentPath = resolve(join(projectArtifactsDir, currentSessionId), name);
101
+ if (existsSync(currentPath)) return currentPath;
102
+
103
+ // 2. Search other session directories, sorted by mtime (newest first)
104
+ if (!existsSync(projectArtifactsDir)) return null;
105
+
106
+ const sessionDirs = readdirSync(projectArtifactsDir)
107
+ .filter((d) => d !== currentSessionId)
108
+ .map((d) => {
109
+ const fullPath = join(projectArtifactsDir, d);
110
+ try {
111
+ const stat = statSync(fullPath);
112
+ return stat.isDirectory() ? { dir: d, mtime: stat.mtimeMs } : null;
113
+ } catch {
114
+ return null;
115
+ }
116
+ })
117
+ .filter((x): x is { dir: string; mtime: number } => x !== null)
118
+ .sort((a, b) => b.mtime - a.mtime);
119
+
120
+ for (const { dir } of sessionDirs) {
121
+ const candidate = resolve(join(projectArtifactsDir, dir), name);
122
+ if (existsSync(candidate)) return candidate;
123
+ }
124
+
125
+ return null;
126
+ }
127
+
128
+ pi.registerTool({
129
+ name: "read_artifact",
130
+ label: "Read Artifact",
131
+ description:
132
+ "Read a session-scoped artifact file by name (e.g. 'plans/my-plan.md', 'context/auth.md'). " +
133
+ "Searches the current session first, then other sessions for the same project. " +
134
+ "Use this to read artifacts written by sub-agents or previous sessions.",
135
+ promptSnippet:
136
+ "Read a session-scoped artifact file by name (e.g. 'plans/my-plan.md', 'context/auth.md'). " +
137
+ "Searches the current session first, then other sessions for the same project. " +
138
+ "Use this to read artifacts written by sub-agents or previous sessions.",
139
+ promptGuidelines: [
140
+ "Use read_artifact to read files written by write_artifact — especially artifacts from sub-agents.",
141
+ "The name param should match what was passed to write_artifact (e.g. 'plans/2026-03-16-fullstack-counter.md').",
142
+ "When a sub-agent reports it wrote an artifact, use read_artifact to access it — don't use the read tool or bash.",
143
+ ],
144
+ parameters: Type.Object({
145
+ name: Type.String({
146
+ description: "Artifact name, e.g. 'plan.md' or 'plans/2026-03-16-fullstack-counter.md'",
147
+ }),
148
+ }),
149
+
150
+ renderCall(args, theme) {
151
+ const name = args.name ?? "...";
152
+ return new Text(
153
+ theme.fg("toolTitle", theme.bold("read_artifact")) + " " + theme.fg("accent", name),
154
+ 0,
155
+ 0,
156
+ );
157
+ },
158
+
159
+ renderResult(result, { expanded }, theme) {
160
+ const details = result.details as
161
+ | { path?: string; name?: string; content?: string; sessionId?: string }
162
+ | undefined;
163
+ const name = details?.name ?? "artifact";
164
+ const content = details?.content ?? "";
165
+
166
+ let text = theme.fg("success", "✓") + " " + theme.fg("accent", details?.path ?? name);
167
+
168
+ if (content) {
169
+ const lang = getLanguageFromPath(name);
170
+ const lines = lang ? highlightCode(content, lang) : content.split("\n");
171
+ const totalLines = lines.length;
172
+ const maxLines = expanded ? lines.length : PREVIEW_LINES;
173
+ const displayLines = lines.slice(0, maxLines);
174
+ const remaining = totalLines - maxLines;
175
+
176
+ text +=
177
+ "\n\n" +
178
+ displayLines
179
+ .map((line: string) => (lang ? line : theme.fg("toolOutput", line)))
180
+ .join("\n");
181
+
182
+ if (remaining > 0) {
183
+ text +=
184
+ theme.fg("muted", `\n... (${remaining} more lines, ${totalLines} total,`) +
185
+ ` ${keyHint("app.tools.expand", "to expand")})`;
186
+ }
187
+ }
188
+
189
+ return new Text(text, 0, 0);
190
+ },
191
+
192
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
193
+ const sessionDir = ctx.sessionManager.getSessionDir();
194
+ const sessionId = ctx.sessionManager.getSessionId();
195
+ const projectArtifactsDir = join(sessionDir, "artifacts");
196
+
197
+ const found = findArtifact(projectArtifactsDir, sessionId, params.name);
198
+
199
+ if (!found) {
200
+ // List available artifacts to help the agent
201
+ const available: string[] = [];
202
+ if (existsSync(projectArtifactsDir)) {
203
+ const collectArtifacts = (dir: string, prefix: string) => {
204
+ try {
205
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
206
+ if (entry.isDirectory()) {
207
+ collectArtifacts(
208
+ join(dir, entry.name),
209
+ prefix ? `${prefix}/${entry.name}` : entry.name,
210
+ );
211
+ } else {
212
+ available.push(prefix ? `${prefix}/${entry.name}` : entry.name);
213
+ }
214
+ }
215
+ } catch {}
216
+ };
217
+ for (const sessionDir of readdirSync(projectArtifactsDir)) {
218
+ const fullPath = join(projectArtifactsDir, sessionDir);
219
+ try {
220
+ if (statSync(fullPath).isDirectory()) {
221
+ collectArtifacts(fullPath, "");
222
+ }
223
+ } catch {}
224
+ }
225
+ }
226
+
227
+ const uniqueNames = [...new Set(available)].sort();
228
+ let msg = `Artifact not found: ${params.name}`;
229
+ if (uniqueNames.length > 0) {
230
+ msg += `\n\nAvailable artifacts:\n${uniqueNames.map((n) => ` - ${n}`).join("\n")}`;
231
+ }
232
+
233
+ return {
234
+ content: [{ type: "text", text: msg }],
235
+ isError: true,
236
+ };
237
+ }
238
+
239
+ // Safety: ensure we're not escaping the artifacts directory
240
+ if (!found.startsWith(projectArtifactsDir)) {
241
+ throw new Error(`Path escapes artifact directory: ${params.name}`);
242
+ }
243
+
244
+ const content = readFileSync(found, "utf-8");
245
+
246
+ return {
247
+ content: [{ type: "text", text: content }],
248
+ details: { path: found, name: params.name, sessionId, content },
249
+ };
250
+ },
251
+ });
252
+ }