@mariozechner/pi-coding-agent 0.45.2 → 0.45.4
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/CHANGELOG.md +25 -1
- package/README.md +2 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +1 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/core/extensions/loader.d.ts +2 -0
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +33 -6
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/model-registry.d.ts +4 -0
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +6 -0
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +1 -0
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +7 -5
- package/dist/core/sdk.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/modes/interactive/theme/light.json +9 -9
- package/dist/utils/image-convert.d.ts.map +1 -1
- package/dist/utils/image-convert.js +11 -4
- package/dist/utils/image-convert.js.map +1 -1
- package/dist/utils/image-resize.d.ts +1 -1
- package/dist/utils/image-resize.d.ts.map +1 -1
- package/dist/utils/image-resize.js +47 -25
- package/dist/utils/image-resize.js.map +1 -1
- package/dist/utils/vips.d.ts +11 -0
- package/dist/utils/vips.d.ts.map +1 -0
- package/dist/utils/vips.js +35 -0
- package/dist/utils/vips.js.map +1 -0
- package/docs/extensions.md +18 -17
- package/docs/sdk.md +21 -48
- package/examples/README.md +5 -2
- package/examples/extensions/README.md +19 -2
- package/examples/extensions/plan-mode/README.md +65 -0
- package/examples/extensions/plan-mode/index.ts +340 -0
- package/examples/extensions/plan-mode/utils.ts +168 -0
- package/examples/extensions/question.ts +211 -13
- package/examples/extensions/questionnaire.ts +427 -0
- package/examples/extensions/summarize.ts +195 -0
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/examples/sdk/README.md +3 -4
- package/package.json +6 -6
- package/examples/extensions/plan-mode.ts +0 -548
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image-resize.js","sourceRoot":"","sources":["../../src/utils/image-resize.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ImageContent } from \"@mariozechner/pi-ai\";\n\nexport interface ImageResizeOptions {\n\tmaxWidth?: number; // Default: 2000\n\tmaxHeight?: number; // Default: 2000\n\tmaxBytes?: number; // Default: 4.5MB (below Anthropic's 5MB limit)\n\tjpegQuality?: number; // Default: 80\n}\n\nexport interface ResizedImage {\n\tdata: string; // base64\n\tmimeType: string;\n\toriginalWidth: number;\n\toriginalHeight: number;\n\twidth: number;\n\theight: number;\n\twasResized: boolean;\n}\n\n// 4.5MB - provides headroom below Anthropic's 5MB limit\nconst DEFAULT_MAX_BYTES = 4.5 * 1024 * 1024;\n\nconst DEFAULT_OPTIONS: Required<ImageResizeOptions> = {\n\tmaxWidth: 2000,\n\tmaxHeight: 2000,\n\tmaxBytes: DEFAULT_MAX_BYTES,\n\tjpegQuality: 80,\n};\n\n/** Helper to pick the smaller of two buffers */\nfunction pickSmaller(\n\ta: { buffer: Buffer; mimeType: string },\n\tb: { buffer: Buffer; mimeType: string },\n): { buffer: Buffer; mimeType: string } {\n\treturn a.buffer.length <= b.buffer.length ? a : b;\n}\n\n/**\n * Resize an image to fit within the specified max dimensions and file size.\n * Returns the original image if it already fits within the limits.\n *\n * Uses sharp for image processing. If sharp is not available (e.g., in some\n * environments), returns the original image unchanged.\n *\n * Strategy for staying under maxBytes:\n * 1. First resize to maxWidth/maxHeight\n * 2. Try both PNG and JPEG formats, pick the smaller one\n * 3. If still too large, try JPEG with decreasing quality\n * 4. If still too large, progressively reduce dimensions\n */\nexport async function resizeImage(img: ImageContent, options?: ImageResizeOptions): Promise<ResizedImage> {\n\tconst opts = { ...DEFAULT_OPTIONS, ...options };\n\tconst buffer = Buffer.from(img.data, \"base64\");\n\n\tlet sharp: typeof import(\"sharp\") | undefined;\n\ttry {\n\t\tsharp = (await import(\"sharp\")).default;\n\t} catch {\n\t\t// Sharp not available - return original image\n\t\t// We can't get dimensions without sharp, so return 0s\n\t\treturn {\n\t\t\tdata: img.data,\n\t\t\tmimeType: img.mimeType,\n\t\t\toriginalWidth: 0,\n\t\t\toriginalHeight: 0,\n\t\t\twidth: 0,\n\t\t\theight: 0,\n\t\t\twasResized: false,\n\t\t};\n\t}\n\n\tconst sharpImg = sharp(buffer);\n\tconst metadata = await sharpImg.metadata();\n\n\tconst originalWidth = metadata.width ?? 0;\n\tconst originalHeight = metadata.height ?? 0;\n\tconst format = metadata.format ?? img.mimeType?.split(\"/\")[1] ?? \"png\";\n\n\t// Check if already within all limits (dimensions AND size)\n\tconst originalSize = buffer.length;\n\tif (originalWidth <= opts.maxWidth && originalHeight <= opts.maxHeight && originalSize <= opts.maxBytes) {\n\t\treturn {\n\t\t\tdata: img.data,\n\t\t\tmimeType: img.mimeType ?? `image/${format}`,\n\t\t\toriginalWidth,\n\t\t\toriginalHeight,\n\t\t\twidth: originalWidth,\n\t\t\theight: originalHeight,\n\t\t\twasResized: false,\n\t\t};\n\t}\n\n\t// Calculate initial dimensions respecting max limits\n\tlet targetWidth = originalWidth;\n\tlet targetHeight = originalHeight;\n\n\tif (targetWidth > opts.maxWidth) {\n\t\ttargetHeight = Math.round((targetHeight * opts.maxWidth) / targetWidth);\n\t\ttargetWidth = opts.maxWidth;\n\t}\n\tif (targetHeight > opts.maxHeight) {\n\t\ttargetWidth = Math.round((targetWidth * opts.maxHeight) / targetHeight);\n\t\ttargetHeight = opts.maxHeight;\n\t}\n\n\t// Helper to resize and encode in both formats, returning the smaller one\n\tasync function tryBothFormats(\n\t\twidth: number,\n\t\theight: number,\n\t\tjpegQuality: number,\n\t): Promise<{ buffer: Buffer; mimeType: string }> {\n\t\tconst resized = await sharp!(buffer)\n\t\t\t.resize(width, height, { fit: \"inside\", withoutEnlargement: true })\n\t\t\t.toBuffer();\n\n\t\tconst [pngBuffer, jpegBuffer] = await Promise.all([\n\t\t\tsharp!(resized).png({ compressionLevel: 9 }).toBuffer(),\n\t\t\tsharp!(resized).jpeg({ quality: jpegQuality }).toBuffer(),\n\t\t]);\n\n\t\treturn pickSmaller({ buffer: pngBuffer, mimeType: \"image/png\" }, { buffer: jpegBuffer, mimeType: \"image/jpeg\" });\n\t}\n\n\t// Try to produce an image under maxBytes\n\tconst qualitySteps = [85, 70, 55, 40];\n\tconst scaleSteps = [1.0, 0.75, 0.5, 0.35, 0.25];\n\n\tlet best: { buffer: Buffer; mimeType: string };\n\tlet finalWidth = targetWidth;\n\tlet finalHeight = targetHeight;\n\n\t// First attempt: resize to target dimensions, try both formats\n\tbest = await tryBothFormats(targetWidth, targetHeight, opts.jpegQuality);\n\n\tif (best.buffer.length <= opts.maxBytes) {\n\t\treturn {\n\t\t\tdata: best.buffer.toString(\"base64\"),\n\t\t\tmimeType: best.mimeType,\n\t\t\toriginalWidth,\n\t\t\toriginalHeight,\n\t\t\twidth: finalWidth,\n\t\t\theight: finalHeight,\n\t\t\twasResized: true,\n\t\t};\n\t}\n\n\t// Still too large - try JPEG with decreasing quality (and compare to PNG each time)\n\tfor (const quality of qualitySteps) {\n\t\tbest = await tryBothFormats(targetWidth, targetHeight, quality);\n\n\t\tif (best.buffer.length <= opts.maxBytes) {\n\t\t\treturn {\n\t\t\t\tdata: best.buffer.toString(\"base64\"),\n\t\t\t\tmimeType: best.mimeType,\n\t\t\t\toriginalWidth,\n\t\t\t\toriginalHeight,\n\t\t\t\twidth: finalWidth,\n\t\t\t\theight: finalHeight,\n\t\t\t\twasResized: true,\n\t\t\t};\n\t\t}\n\t}\n\n\t// Still too large - reduce dimensions progressively\n\tfor (const scale of scaleSteps) {\n\t\tfinalWidth = Math.round(targetWidth * scale);\n\t\tfinalHeight = Math.round(targetHeight * scale);\n\n\t\t// Skip if dimensions are too small\n\t\tif (finalWidth < 100 || finalHeight < 100) {\n\t\t\tbreak;\n\t\t}\n\n\t\tfor (const quality of qualitySteps) {\n\t\t\tbest = await tryBothFormats(finalWidth, finalHeight, quality);\n\n\t\t\tif (best.buffer.length <= opts.maxBytes) {\n\t\t\t\treturn {\n\t\t\t\t\tdata: best.buffer.toString(\"base64\"),\n\t\t\t\t\tmimeType: best.mimeType,\n\t\t\t\t\toriginalWidth,\n\t\t\t\t\toriginalHeight,\n\t\t\t\t\twidth: finalWidth,\n\t\t\t\t\theight: finalHeight,\n\t\t\t\t\twasResized: true,\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\t}\n\n\t// Last resort: return smallest version we produced even if over limit\n\t// (the API will reject it, but at least we tried everything)\n\treturn {\n\t\tdata: best.buffer.toString(\"base64\"),\n\t\tmimeType: best.mimeType,\n\t\toriginalWidth,\n\t\toriginalHeight,\n\t\twidth: finalWidth,\n\t\theight: finalHeight,\n\t\twasResized: true,\n\t};\n}\n\n/**\n * Format a dimension note for resized images.\n * This helps the model understand the coordinate mapping.\n */\nexport function formatDimensionNote(result: ResizedImage): string | undefined {\n\tif (!result.wasResized) {\n\t\treturn undefined;\n\t}\n\n\tconst scale = result.originalWidth / result.width;\n\treturn `[Image: original ${result.originalWidth}x${result.originalHeight}, displayed at ${result.width}x${result.height}. Multiply coordinates by ${scale.toFixed(2)} to map to original image.]`;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"image-resize.js","sourceRoot":"","sources":["../../src/utils/image-resize.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ImageContent } from \"@mariozechner/pi-ai\";\nimport { getVips } from \"./vips.js\";\n\nexport interface ImageResizeOptions {\n\tmaxWidth?: number; // Default: 2000\n\tmaxHeight?: number; // Default: 2000\n\tmaxBytes?: number; // Default: 4.5MB (below Anthropic's 5MB limit)\n\tjpegQuality?: number; // Default: 80\n}\n\nexport interface ResizedImage {\n\tdata: string; // base64\n\tmimeType: string;\n\toriginalWidth: number;\n\toriginalHeight: number;\n\twidth: number;\n\theight: number;\n\twasResized: boolean;\n}\n\n// 4.5MB - provides headroom below Anthropic's 5MB limit\nconst DEFAULT_MAX_BYTES = 4.5 * 1024 * 1024;\n\nconst DEFAULT_OPTIONS: Required<ImageResizeOptions> = {\n\tmaxWidth: 2000,\n\tmaxHeight: 2000,\n\tmaxBytes: DEFAULT_MAX_BYTES,\n\tjpegQuality: 80,\n};\n\n/** Helper to pick the smaller of two buffers */\nfunction pickSmaller(\n\ta: { buffer: Uint8Array; mimeType: string },\n\tb: { buffer: Uint8Array; mimeType: string },\n): { buffer: Uint8Array; mimeType: string } {\n\treturn a.buffer.length <= b.buffer.length ? a : b;\n}\n\n/**\n * Resize an image to fit within the specified max dimensions and file size.\n * Returns the original image if it already fits within the limits.\n *\n * Uses wasm-vips for image processing. If wasm-vips is not available (e.g., in some\n * environments), returns the original image unchanged.\n *\n * Strategy for staying under maxBytes:\n * 1. First resize to maxWidth/maxHeight\n * 2. Try both PNG and JPEG formats, pick the smaller one\n * 3. If still too large, try JPEG with decreasing quality\n * 4. If still too large, progressively reduce dimensions\n */\nexport async function resizeImage(img: ImageContent, options?: ImageResizeOptions): Promise<ResizedImage> {\n\tconst opts = { ...DEFAULT_OPTIONS, ...options };\n\tconst buffer = Buffer.from(img.data, \"base64\");\n\n\tconst vipsOrNull = await getVips();\n\tif (!vipsOrNull) {\n\t\t// wasm-vips not available - return original image\n\t\t// We can't get dimensions without vips, so return 0s\n\t\treturn {\n\t\t\tdata: img.data,\n\t\t\tmimeType: img.mimeType,\n\t\t\toriginalWidth: 0,\n\t\t\toriginalHeight: 0,\n\t\t\twidth: 0,\n\t\t\theight: 0,\n\t\t\twasResized: false,\n\t\t};\n\t}\n\t// Capture non-null reference for use in nested functions\n\tconst vips = vipsOrNull;\n\n\t// Load image to get metadata\n\tlet sourceImg: InstanceType<typeof vips.Image>;\n\ttry {\n\t\tsourceImg = vips.Image.newFromBuffer(buffer);\n\t} catch {\n\t\t// Failed to load image\n\t\treturn {\n\t\t\tdata: img.data,\n\t\t\tmimeType: img.mimeType,\n\t\t\toriginalWidth: 0,\n\t\t\toriginalHeight: 0,\n\t\t\twidth: 0,\n\t\t\theight: 0,\n\t\t\twasResized: false,\n\t\t};\n\t}\n\n\tconst originalWidth = sourceImg.width;\n\tconst originalHeight = sourceImg.height;\n\n\t// Check if already within all limits (dimensions AND size)\n\tconst originalSize = buffer.length;\n\tif (originalWidth <= opts.maxWidth && originalHeight <= opts.maxHeight && originalSize <= opts.maxBytes) {\n\t\tsourceImg.delete();\n\t\tconst format = img.mimeType?.split(\"/\")[1] ?? \"png\";\n\t\treturn {\n\t\t\tdata: img.data,\n\t\t\tmimeType: img.mimeType ?? `image/${format}`,\n\t\t\toriginalWidth,\n\t\t\toriginalHeight,\n\t\t\twidth: originalWidth,\n\t\t\theight: originalHeight,\n\t\t\twasResized: false,\n\t\t};\n\t}\n\n\t// Calculate initial dimensions respecting max limits\n\tlet targetWidth = originalWidth;\n\tlet targetHeight = originalHeight;\n\n\tif (targetWidth > opts.maxWidth) {\n\t\ttargetHeight = Math.round((targetHeight * opts.maxWidth) / targetWidth);\n\t\ttargetWidth = opts.maxWidth;\n\t}\n\tif (targetHeight > opts.maxHeight) {\n\t\ttargetWidth = Math.round((targetWidth * opts.maxHeight) / targetHeight);\n\t\ttargetHeight = opts.maxHeight;\n\t}\n\n\t// Helper to resize and encode in both formats, returning the smaller one\n\tfunction tryBothFormats(\n\t\twidth: number,\n\t\theight: number,\n\t\tjpegQuality: number,\n\t): { buffer: Uint8Array; mimeType: string } {\n\t\t// Load image fresh and resize using scale factor\n\t\t// (Using newFromBuffer + resize instead of thumbnailBuffer to avoid lazy re-read issues)\n\t\tconst img = vips.Image.newFromBuffer(buffer);\n\t\tconst scale = Math.min(width / img.width, height / img.height);\n\t\tconst resized = scale < 1 ? img.resize(scale) : img;\n\n\t\tconst pngBuffer = resized.writeToBuffer(\".png\");\n\t\tconst jpegBuffer = resized.writeToBuffer(\".jpg\", { Q: jpegQuality });\n\n\t\tif (resized !== img) {\n\t\t\tresized.delete();\n\t\t}\n\t\timg.delete();\n\n\t\treturn pickSmaller({ buffer: pngBuffer, mimeType: \"image/png\" }, { buffer: jpegBuffer, mimeType: \"image/jpeg\" });\n\t}\n\n\t// Clean up the source image\n\tsourceImg.delete();\n\n\t// Try to produce an image under maxBytes\n\tconst qualitySteps = [85, 70, 55, 40];\n\tconst scaleSteps = [1.0, 0.75, 0.5, 0.35, 0.25];\n\n\tlet best: { buffer: Uint8Array; mimeType: string };\n\tlet finalWidth = targetWidth;\n\tlet finalHeight = targetHeight;\n\n\t// First attempt: resize to target dimensions, try both formats\n\tbest = tryBothFormats(targetWidth, targetHeight, opts.jpegQuality);\n\n\tif (best.buffer.length <= opts.maxBytes) {\n\t\treturn {\n\t\t\tdata: Buffer.from(best.buffer).toString(\"base64\"),\n\t\t\tmimeType: best.mimeType,\n\t\t\toriginalWidth,\n\t\t\toriginalHeight,\n\t\t\twidth: finalWidth,\n\t\t\theight: finalHeight,\n\t\t\twasResized: true,\n\t\t};\n\t}\n\n\t// Still too large - try JPEG with decreasing quality (and compare to PNG each time)\n\tfor (const quality of qualitySteps) {\n\t\tbest = tryBothFormats(targetWidth, targetHeight, quality);\n\n\t\tif (best.buffer.length <= opts.maxBytes) {\n\t\t\treturn {\n\t\t\t\tdata: Buffer.from(best.buffer).toString(\"base64\"),\n\t\t\t\tmimeType: best.mimeType,\n\t\t\t\toriginalWidth,\n\t\t\t\toriginalHeight,\n\t\t\t\twidth: finalWidth,\n\t\t\t\theight: finalHeight,\n\t\t\t\twasResized: true,\n\t\t\t};\n\t\t}\n\t}\n\n\t// Still too large - reduce dimensions progressively\n\tfor (const scale of scaleSteps) {\n\t\tfinalWidth = Math.round(targetWidth * scale);\n\t\tfinalHeight = Math.round(targetHeight * scale);\n\n\t\t// Skip if dimensions are too small\n\t\tif (finalWidth < 100 || finalHeight < 100) {\n\t\t\tbreak;\n\t\t}\n\n\t\tfor (const quality of qualitySteps) {\n\t\t\tbest = tryBothFormats(finalWidth, finalHeight, quality);\n\n\t\t\tif (best.buffer.length <= opts.maxBytes) {\n\t\t\t\treturn {\n\t\t\t\t\tdata: Buffer.from(best.buffer).toString(\"base64\"),\n\t\t\t\t\tmimeType: best.mimeType,\n\t\t\t\t\toriginalWidth,\n\t\t\t\t\toriginalHeight,\n\t\t\t\t\twidth: finalWidth,\n\t\t\t\t\theight: finalHeight,\n\t\t\t\t\twasResized: true,\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\t}\n\n\t// Last resort: return smallest version we produced even if over limit\n\t// (the API will reject it, but at least we tried everything)\n\treturn {\n\t\tdata: Buffer.from(best.buffer).toString(\"base64\"),\n\t\tmimeType: best.mimeType,\n\t\toriginalWidth,\n\t\toriginalHeight,\n\t\twidth: finalWidth,\n\t\theight: finalHeight,\n\t\twasResized: true,\n\t};\n}\n\n/**\n * Format a dimension note for resized images.\n * This helps the model understand the coordinate mapping.\n */\nexport function formatDimensionNote(result: ResizedImage): string | undefined {\n\tif (!result.wasResized) {\n\t\treturn undefined;\n\t}\n\n\tconst scale = result.originalWidth / result.width;\n\treturn `[Image: original ${result.originalWidth}x${result.originalHeight}, displayed at ${result.width}x${result.height}. Multiply coordinates by ${scale.toFixed(2)} to map to original image.]`;\n}\n"]}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Singleton wrapper for wasm-vips initialization.
|
|
3
|
+
* wasm-vips requires async initialization, so we cache the instance.
|
|
4
|
+
*/
|
|
5
|
+
import type Vips from "wasm-vips";
|
|
6
|
+
/**
|
|
7
|
+
* Get the initialized wasm-vips instance.
|
|
8
|
+
* Returns null if wasm-vips is not available or fails to initialize.
|
|
9
|
+
*/
|
|
10
|
+
export declare function getVips(): Promise<Awaited<ReturnType<typeof Vips>> | null>;
|
|
11
|
+
//# sourceMappingURL=vips.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vips.d.ts","sourceRoot":"","sources":["../../src/utils/vips.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAKlC;;;GAGG;AACH,wBAAsB,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,OAAO,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,CAyBhF","sourcesContent":["/**\n * Singleton wrapper for wasm-vips initialization.\n * wasm-vips requires async initialization, so we cache the instance.\n */\n\nimport type Vips from \"wasm-vips\";\n\nlet vipsInstance: Awaited<ReturnType<typeof Vips>> | null = null;\nlet vipsInitPromise: Promise<Awaited<ReturnType<typeof Vips>> | null> | null = null;\n\n/**\n * Get the initialized wasm-vips instance.\n * Returns null if wasm-vips is not available or fails to initialize.\n */\nexport async function getVips(): Promise<Awaited<ReturnType<typeof Vips>> | null> {\n\tif (vipsInstance) {\n\t\treturn vipsInstance;\n\t}\n\n\tif (vipsInitPromise) {\n\t\treturn vipsInitPromise;\n\t}\n\n\tvipsInitPromise = (async () => {\n\t\ttry {\n\t\t\tconst VipsInit = (await import(\"wasm-vips\")).default;\n\t\t\tvipsInstance = await VipsInit();\n\t\t\treturn vipsInstance;\n\t\t} catch {\n\t\t\t// wasm-vips not available\n\t\t\treturn null;\n\t\t}\n\t})();\n\n\tconst result = await vipsInitPromise;\n\tif (!result) {\n\t\tvipsInitPromise = null; // Allow retry on failure\n\t}\n\treturn result;\n}\n"]}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Singleton wrapper for wasm-vips initialization.
|
|
3
|
+
* wasm-vips requires async initialization, so we cache the instance.
|
|
4
|
+
*/
|
|
5
|
+
let vipsInstance = null;
|
|
6
|
+
let vipsInitPromise = null;
|
|
7
|
+
/**
|
|
8
|
+
* Get the initialized wasm-vips instance.
|
|
9
|
+
* Returns null if wasm-vips is not available or fails to initialize.
|
|
10
|
+
*/
|
|
11
|
+
export async function getVips() {
|
|
12
|
+
if (vipsInstance) {
|
|
13
|
+
return vipsInstance;
|
|
14
|
+
}
|
|
15
|
+
if (vipsInitPromise) {
|
|
16
|
+
return vipsInitPromise;
|
|
17
|
+
}
|
|
18
|
+
vipsInitPromise = (async () => {
|
|
19
|
+
try {
|
|
20
|
+
const VipsInit = (await import("wasm-vips")).default;
|
|
21
|
+
vipsInstance = await VipsInit();
|
|
22
|
+
return vipsInstance;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// wasm-vips not available
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
})();
|
|
29
|
+
const result = await vipsInitPromise;
|
|
30
|
+
if (!result) {
|
|
31
|
+
vipsInitPromise = null; // Allow retry on failure
|
|
32
|
+
}
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=vips.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vips.js","sourceRoot":"","sources":["../../src/utils/vips.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,IAAI,YAAY,GAA4C,IAAI,CAAC;AACjE,IAAI,eAAe,GAA4D,IAAI,CAAC;AAEpF;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,OAAO,GAAqD;IACjF,IAAI,YAAY,EAAE,CAAC;QAClB,OAAO,YAAY,CAAC;IACrB,CAAC;IAED,IAAI,eAAe,EAAE,CAAC;QACrB,OAAO,eAAe,CAAC;IACxB,CAAC;IAED,eAAe,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC;QAC9B,IAAI,CAAC;YACJ,MAAM,QAAQ,GAAG,CAAC,MAAM,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,OAAO,CAAC;YACrD,YAAY,GAAG,MAAM,QAAQ,EAAE,CAAC;YAChC,OAAO,YAAY,CAAC;QACrB,CAAC;QAAC,MAAM,CAAC;YACR,0BAA0B;YAC1B,OAAO,IAAI,CAAC;QACb,CAAC;IAAA,CACD,CAAC,EAAE,CAAC;IAEL,MAAM,MAAM,GAAG,MAAM,eAAe,CAAC;IACrC,IAAI,CAAC,MAAM,EAAE,CAAC;QACb,eAAe,GAAG,IAAI,CAAC,CAAC,yBAAyB;IAClD,CAAC;IACD,OAAO,MAAM,CAAC;AAAA,CACd","sourcesContent":["/**\n * Singleton wrapper for wasm-vips initialization.\n * wasm-vips requires async initialization, so we cache the instance.\n */\n\nimport type Vips from \"wasm-vips\";\n\nlet vipsInstance: Awaited<ReturnType<typeof Vips>> | null = null;\nlet vipsInitPromise: Promise<Awaited<ReturnType<typeof Vips>> | null> | null = null;\n\n/**\n * Get the initialized wasm-vips instance.\n * Returns null if wasm-vips is not available or fails to initialize.\n */\nexport async function getVips(): Promise<Awaited<ReturnType<typeof Vips>> | null> {\n\tif (vipsInstance) {\n\t\treturn vipsInstance;\n\t}\n\n\tif (vipsInitPromise) {\n\t\treturn vipsInitPromise;\n\t}\n\n\tvipsInitPromise = (async () => {\n\t\ttry {\n\t\t\tconst VipsInit = (await import(\"wasm-vips\")).default;\n\t\t\tvipsInstance = await VipsInit();\n\t\t\treturn vipsInstance;\n\t\t} catch {\n\t\t\t// wasm-vips not available\n\t\t\treturn null;\n\t\t}\n\t})();\n\n\tconst result = await vipsInitPromise;\n\tif (!result) {\n\t\tvipsInitPromise = null; // Allow retry on failure\n\t}\n\treturn result;\n}\n"]}
|
package/docs/extensions.md
CHANGED
|
@@ -18,6 +18,7 @@ Extensions are TypeScript modules that extend pi's behavior. They can subscribe
|
|
|
18
18
|
- Git checkpointing (stash at each turn, restore on branch)
|
|
19
19
|
- Path protection (block writes to `.env`, `node_modules/`)
|
|
20
20
|
- Custom compaction (summarize conversation your way)
|
|
21
|
+
- Conversation summaries (see `summarize.ts` example)
|
|
21
22
|
- Interactive tools (questions, wizards, custom dialogs)
|
|
22
23
|
- Stateful tools (todo lists, connection pools)
|
|
23
24
|
- External integrations (file watchers, webhooks, CI triggers)
|
|
@@ -438,7 +439,7 @@ pi.on("before_agent_start", async (event, ctx) => {
|
|
|
438
439
|
});
|
|
439
440
|
```
|
|
440
441
|
|
|
441
|
-
**Examples:** [claude-rules.ts](../examples/extensions/claude-rules.ts), [pirate.ts](../examples/extensions/pirate.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts), [preset.ts](../examples/extensions/preset.ts), [ssh.ts](../examples/extensions/ssh.ts)
|
|
442
|
+
**Examples:** [claude-rules.ts](../examples/extensions/claude-rules.ts), [pirate.ts](../examples/extensions/pirate.ts), [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts), [preset.ts](../examples/extensions/preset.ts), [ssh.ts](../examples/extensions/ssh.ts)
|
|
442
443
|
|
|
443
444
|
#### agent_start / agent_end
|
|
444
445
|
|
|
@@ -452,7 +453,7 @@ pi.on("agent_end", async (event, ctx) => {
|
|
|
452
453
|
});
|
|
453
454
|
```
|
|
454
455
|
|
|
455
|
-
**Examples:** [chalk-logger.ts](../examples/extensions/chalk-logger.ts), [git-checkpoint.ts](../examples/extensions/git-checkpoint.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts)
|
|
456
|
+
**Examples:** [chalk-logger.ts](../examples/extensions/chalk-logger.ts), [git-checkpoint.ts](../examples/extensions/git-checkpoint.ts), [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts)
|
|
456
457
|
|
|
457
458
|
#### turn_start / turn_end
|
|
458
459
|
|
|
@@ -468,7 +469,7 @@ pi.on("turn_end", async (event, ctx) => {
|
|
|
468
469
|
});
|
|
469
470
|
```
|
|
470
471
|
|
|
471
|
-
**Examples:** [git-checkpoint.ts](../examples/extensions/git-checkpoint.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts), [status-line.ts](../examples/extensions/status-line.ts)
|
|
472
|
+
**Examples:** [git-checkpoint.ts](../examples/extensions/git-checkpoint.ts), [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts), [status-line.ts](../examples/extensions/status-line.ts)
|
|
472
473
|
|
|
473
474
|
#### context
|
|
474
475
|
|
|
@@ -482,7 +483,7 @@ pi.on("context", async (event, ctx) => {
|
|
|
482
483
|
});
|
|
483
484
|
```
|
|
484
485
|
|
|
485
|
-
**Examples:** [plan-mode.ts](../examples/extensions/plan-mode.ts)
|
|
486
|
+
**Examples:** [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts)
|
|
486
487
|
|
|
487
488
|
### Model Events
|
|
488
489
|
|
|
@@ -527,7 +528,7 @@ pi.on("tool_call", async (event, ctx) => {
|
|
|
527
528
|
});
|
|
528
529
|
```
|
|
529
530
|
|
|
530
|
-
**Examples:** [chalk-logger.ts](../examples/extensions/chalk-logger.ts), [permission-gate.ts](../examples/extensions/permission-gate.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts), [protected-paths.ts](../examples/extensions/protected-paths.ts)
|
|
531
|
+
**Examples:** [chalk-logger.ts](../examples/extensions/chalk-logger.ts), [permission-gate.ts](../examples/extensions/permission-gate.ts), [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts), [protected-paths.ts](../examples/extensions/protected-paths.ts)
|
|
531
532
|
|
|
532
533
|
#### tool_result
|
|
533
534
|
|
|
@@ -549,7 +550,7 @@ pi.on("tool_result", async (event, ctx) => {
|
|
|
549
550
|
});
|
|
550
551
|
```
|
|
551
552
|
|
|
552
|
-
**Examples:** [git-checkpoint.ts](../examples/extensions/git-checkpoint.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts)
|
|
553
|
+
**Examples:** [git-checkpoint.ts](../examples/extensions/git-checkpoint.ts), [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts)
|
|
553
554
|
|
|
554
555
|
### User Bash Events
|
|
555
556
|
|
|
@@ -723,7 +724,7 @@ pi.registerTool({
|
|
|
723
724
|
});
|
|
724
725
|
```
|
|
725
726
|
|
|
726
|
-
**Examples:** [hello.ts](../examples/extensions/hello.ts), [question.ts](../examples/extensions/question.ts), [todo.ts](../examples/extensions/todo.ts), [truncated-tool.ts](../examples/extensions/truncated-tool.ts)
|
|
727
|
+
**Examples:** [hello.ts](../examples/extensions/hello.ts), [question.ts](../examples/extensions/question.ts), [questionnaire.ts](../examples/extensions/questionnaire.ts), [todo.ts](../examples/extensions/todo.ts), [truncated-tool.ts](../examples/extensions/truncated-tool.ts)
|
|
727
728
|
|
|
728
729
|
### pi.sendMessage(message, options?)
|
|
729
730
|
|
|
@@ -748,7 +749,7 @@ pi.sendMessage({
|
|
|
748
749
|
- `"nextTurn"` - Queued for next user prompt. Does not interrupt or trigger anything.
|
|
749
750
|
- `triggerTurn: true` - If agent is idle, trigger an LLM response immediately. Only applies to `"steer"` and `"followUp"` modes (ignored for `"nextTurn"`).
|
|
750
751
|
|
|
751
|
-
**Examples:** [file-trigger.ts](../examples/extensions/file-trigger.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts)
|
|
752
|
+
**Examples:** [file-trigger.ts](../examples/extensions/file-trigger.ts), [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts)
|
|
752
753
|
|
|
753
754
|
### pi.sendUserMessage(content, options?)
|
|
754
755
|
|
|
@@ -795,7 +796,7 @@ pi.on("session_start", async (_event, ctx) => {
|
|
|
795
796
|
});
|
|
796
797
|
```
|
|
797
798
|
|
|
798
|
-
**Examples:** [plan-mode.ts](../examples/extensions/plan-mode.ts), [preset.ts](../examples/extensions/preset.ts), [snake.ts](../examples/extensions/snake.ts), [tools.ts](../examples/extensions/tools.ts)
|
|
799
|
+
**Examples:** [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts), [preset.ts](../examples/extensions/preset.ts), [snake.ts](../examples/extensions/snake.ts), [tools.ts](../examples/extensions/tools.ts)
|
|
799
800
|
|
|
800
801
|
### pi.setSessionName(name)
|
|
801
802
|
|
|
@@ -830,7 +831,7 @@ pi.registerCommand("stats", {
|
|
|
830
831
|
});
|
|
831
832
|
```
|
|
832
833
|
|
|
833
|
-
**Examples:** [custom-footer.ts](../examples/extensions/custom-footer.ts), [custom-header.ts](../examples/extensions/custom-header.ts), [handoff.ts](../examples/extensions/handoff.ts), [pirate.ts](../examples/extensions/pirate.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts), [preset.ts](../examples/extensions/preset.ts), [qna.ts](../examples/extensions/qna.ts), [send-user-message.ts](../examples/extensions/send-user-message.ts), [snake.ts](../examples/extensions/snake.ts), [todo.ts](../examples/extensions/todo.ts), [tools.ts](../examples/extensions/tools.ts)
|
|
834
|
+
**Examples:** [custom-footer.ts](../examples/extensions/custom-footer.ts), [custom-header.ts](../examples/extensions/custom-header.ts), [handoff.ts](../examples/extensions/handoff.ts), [pirate.ts](../examples/extensions/pirate.ts), [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts), [preset.ts](../examples/extensions/preset.ts), [qna.ts](../examples/extensions/qna.ts), [send-user-message.ts](../examples/extensions/send-user-message.ts), [snake.ts](../examples/extensions/snake.ts), [summarize.ts](../examples/extensions/summarize.ts), [todo.ts](../examples/extensions/todo.ts), [tools.ts](../examples/extensions/tools.ts)
|
|
834
835
|
|
|
835
836
|
### pi.registerMessageRenderer(customType, renderer)
|
|
836
837
|
|
|
@@ -849,7 +850,7 @@ pi.registerShortcut("ctrl+shift+p", {
|
|
|
849
850
|
});
|
|
850
851
|
```
|
|
851
852
|
|
|
852
|
-
**Examples:** [plan-mode.ts](../examples/extensions/plan-mode.ts), [preset.ts](../examples/extensions/preset.ts)
|
|
853
|
+
**Examples:** [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts), [preset.ts](../examples/extensions/preset.ts)
|
|
853
854
|
|
|
854
855
|
### pi.registerFlag(name, options)
|
|
855
856
|
|
|
@@ -868,7 +869,7 @@ if (pi.getFlag("--plan")) {
|
|
|
868
869
|
}
|
|
869
870
|
```
|
|
870
871
|
|
|
871
|
-
**Examples:** [plan-mode.ts](../examples/extensions/plan-mode.ts), [preset.ts](../examples/extensions/preset.ts)
|
|
872
|
+
**Examples:** [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts), [preset.ts](../examples/extensions/preset.ts)
|
|
872
873
|
|
|
873
874
|
### pi.exec(command, args, options?)
|
|
874
875
|
|
|
@@ -892,7 +893,7 @@ const names = all.map(t => t.name); // Just names if needed
|
|
|
892
893
|
pi.setActiveTools(["read", "bash"]); // Switch to read-only
|
|
893
894
|
```
|
|
894
895
|
|
|
895
|
-
**Examples:** [plan-mode.ts](../examples/extensions/plan-mode.ts), [preset.ts](../examples/extensions/preset.ts), [tools.ts](../examples/extensions/tools.ts)
|
|
896
|
+
**Examples:** [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts), [preset.ts](../examples/extensions/preset.ts), [tools.ts](../examples/extensions/tools.ts)
|
|
896
897
|
|
|
897
898
|
### pi.setModel(model)
|
|
898
899
|
|
|
@@ -1243,7 +1244,7 @@ ctx.ui.notify("Done!", "info"); // "info" | "warning" | "error"
|
|
|
1243
1244
|
```
|
|
1244
1245
|
|
|
1245
1246
|
**Examples:**
|
|
1246
|
-
- `ctx.ui.select()`: [confirm-destructive.ts](../examples/extensions/confirm-destructive.ts), [dirty-repo-guard.ts](../examples/extensions/dirty-repo-guard.ts), [git-checkpoint.ts](../examples/extensions/git-checkpoint.ts), [permission-gate.ts](../examples/extensions/permission-gate.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts), [question.ts](../examples/extensions/question.ts)
|
|
1247
|
+
- `ctx.ui.select()`: [confirm-destructive.ts](../examples/extensions/confirm-destructive.ts), [dirty-repo-guard.ts](../examples/extensions/dirty-repo-guard.ts), [git-checkpoint.ts](../examples/extensions/git-checkpoint.ts), [permission-gate.ts](../examples/extensions/permission-gate.ts), [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts), [question.ts](../examples/extensions/question.ts), [questionnaire.ts](../examples/extensions/questionnaire.ts)
|
|
1247
1248
|
- `ctx.ui.confirm()`: [confirm-destructive.ts](../examples/extensions/confirm-destructive.ts)
|
|
1248
1249
|
- `ctx.ui.editor()`: [handoff.ts](../examples/extensions/handoff.ts)
|
|
1249
1250
|
- `ctx.ui.setEditorText()`: [handoff.ts](../examples/extensions/handoff.ts), [qna.ts](../examples/extensions/qna.ts)
|
|
@@ -1345,8 +1346,8 @@ ctx.ui.theme.fg("accent", "styled text"); // Access current theme
|
|
|
1345
1346
|
```
|
|
1346
1347
|
|
|
1347
1348
|
**Examples:**
|
|
1348
|
-
- `ctx.ui.setStatus()`: [plan-mode.ts](../examples/extensions/plan-mode.ts), [preset.ts](../examples/extensions/preset.ts), [status-line.ts](../examples/extensions/status-line.ts)
|
|
1349
|
-
- `ctx.ui.setWidget()`: [plan-mode.ts](../examples/extensions/plan-mode.ts)
|
|
1349
|
+
- `ctx.ui.setStatus()`: [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts), [preset.ts](../examples/extensions/preset.ts), [status-line.ts](../examples/extensions/status-line.ts)
|
|
1350
|
+
- `ctx.ui.setWidget()`: [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts)
|
|
1350
1351
|
- `ctx.ui.setFooter()`: [custom-footer.ts](../examples/extensions/custom-footer.ts)
|
|
1351
1352
|
- `ctx.ui.setHeader()`: [custom-header.ts](../examples/extensions/custom-header.ts)
|
|
1352
1353
|
- `ctx.ui.setEditorComponent()`: [modal-editor.ts](../examples/extensions/modal-editor.ts)
|
|
@@ -1397,7 +1398,7 @@ const result = await ctx.ui.custom<string | null>(
|
|
|
1397
1398
|
|
|
1398
1399
|
Overlay components should define a `width` property to control their size. The overlay is centered by default. See [overlay-test.ts](../examples/extensions/overlay-test.ts) for a complete example.
|
|
1399
1400
|
|
|
1400
|
-
**Examples:** [handoff.ts](../examples/extensions/handoff.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts), [preset.ts](../examples/extensions/preset.ts), [qna.ts](../examples/extensions/qna.ts), [snake.ts](../examples/extensions/snake.ts), [todo.ts](../examples/extensions/todo.ts), [tools.ts](../examples/extensions/tools.ts), [overlay-test.ts](../examples/extensions/overlay-test.ts)
|
|
1401
|
+
**Examples:** [handoff.ts](../examples/extensions/handoff.ts), [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts), [preset.ts](../examples/extensions/preset.ts), [qna.ts](../examples/extensions/qna.ts), [snake.ts](../examples/extensions/snake.ts), [summarize.ts](../examples/extensions/summarize.ts), [todo.ts](../examples/extensions/todo.ts), [tools.ts](../examples/extensions/tools.ts), [overlay-test.ts](../examples/extensions/overlay-test.ts)
|
|
1401
1402
|
|
|
1402
1403
|
### Custom Editor
|
|
1403
1404
|
|
package/docs/sdk.md
CHANGED
|
@@ -735,12 +735,12 @@ import {
|
|
|
735
735
|
discoverAuthStorage,
|
|
736
736
|
discoverModels,
|
|
737
737
|
discoverSkills,
|
|
738
|
-
|
|
739
|
-
discoverCustomTools,
|
|
738
|
+
discoverExtensions,
|
|
740
739
|
discoverContextFiles,
|
|
741
740
|
discoverPromptTemplates,
|
|
742
741
|
loadSettings,
|
|
743
742
|
buildSystemPrompt,
|
|
743
|
+
createEventBus,
|
|
744
744
|
} from "@mariozechner/pi-coding-agent";
|
|
745
745
|
|
|
746
746
|
// Auth and Models
|
|
@@ -754,19 +754,16 @@ const builtIn = getModel("anthropic", "claude-opus-4-5"); // Built-in only
|
|
|
754
754
|
// Skills
|
|
755
755
|
const { skills, warnings } = discoverSkills(cwd, agentDir, skillsSettings);
|
|
756
756
|
|
|
757
|
-
//
|
|
758
|
-
// Pass eventBus to share pi.events across
|
|
757
|
+
// Extensions (async - loads TypeScript)
|
|
758
|
+
// Pass eventBus to share pi.events across extensions
|
|
759
759
|
const eventBus = createEventBus();
|
|
760
|
-
const
|
|
761
|
-
|
|
762
|
-
// Custom tools (async - loads TypeScript)
|
|
763
|
-
const tools = await discoverCustomTools(eventBus, cwd, agentDir);
|
|
760
|
+
const { extensions, errors } = await discoverExtensions(eventBus, cwd, agentDir);
|
|
764
761
|
|
|
765
762
|
// Context files
|
|
766
763
|
const contextFiles = discoverContextFiles(cwd, agentDir);
|
|
767
764
|
|
|
768
765
|
// Prompt templates
|
|
769
|
-
const
|
|
766
|
+
const templates = discoverPromptTemplates(cwd, agentDir);
|
|
770
767
|
|
|
771
768
|
// Settings (global + project merged)
|
|
772
769
|
const settings = loadSettings(cwd, agentDir);
|
|
@@ -816,8 +813,8 @@ import {
|
|
|
816
813
|
SettingsManager,
|
|
817
814
|
readTool,
|
|
818
815
|
bashTool,
|
|
819
|
-
type
|
|
820
|
-
type
|
|
816
|
+
type ExtensionFactory,
|
|
817
|
+
type ToolDefinition,
|
|
821
818
|
} from "@mariozechner/pi-coding-agent";
|
|
822
819
|
|
|
823
820
|
// Set up auth storage (custom location)
|
|
@@ -831,16 +828,16 @@ if (process.env.MY_KEY) {
|
|
|
831
828
|
// Model registry (no custom models.json)
|
|
832
829
|
const modelRegistry = new ModelRegistry(authStorage);
|
|
833
830
|
|
|
834
|
-
// Inline
|
|
835
|
-
const
|
|
836
|
-
|
|
831
|
+
// Inline extension
|
|
832
|
+
const auditExtension: ExtensionFactory = (pi) => {
|
|
833
|
+
pi.on("tool_call", async (event) => {
|
|
837
834
|
console.log(`[Audit] ${event.toolName}`);
|
|
838
835
|
return undefined;
|
|
839
836
|
});
|
|
840
837
|
};
|
|
841
838
|
|
|
842
839
|
// Inline tool
|
|
843
|
-
const statusTool:
|
|
840
|
+
const statusTool: ToolDefinition = {
|
|
844
841
|
name: "status",
|
|
845
842
|
label: "Status",
|
|
846
843
|
description: "Get system status",
|
|
@@ -872,8 +869,8 @@ const { session } = await createAgentSession({
|
|
|
872
869
|
systemPrompt: "You are a minimal assistant. Be concise.",
|
|
873
870
|
|
|
874
871
|
tools: [readTool, bashTool],
|
|
875
|
-
customTools: [
|
|
876
|
-
|
|
872
|
+
customTools: [statusTool],
|
|
873
|
+
extensions: [auditExtension],
|
|
877
874
|
skills: [],
|
|
878
875
|
contextFiles: [],
|
|
879
876
|
promptTemplates: [],
|
|
@@ -961,7 +958,7 @@ The SDK is preferred when:
|
|
|
961
958
|
- You want type safety
|
|
962
959
|
- You're in the same Node.js process
|
|
963
960
|
- You need direct access to agent state
|
|
964
|
-
- You want to customize tools/
|
|
961
|
+
- You want to customize tools/extensions programmatically
|
|
965
962
|
|
|
966
963
|
RPC mode is preferred when:
|
|
967
964
|
- You're integrating from another language
|
|
@@ -984,12 +981,11 @@ discoverModels
|
|
|
984
981
|
|
|
985
982
|
// Discovery
|
|
986
983
|
discoverSkills
|
|
987
|
-
|
|
988
|
-
discoverCustomTools
|
|
984
|
+
discoverExtensions
|
|
989
985
|
discoverContextFiles
|
|
990
986
|
discoverPromptTemplates
|
|
991
987
|
|
|
992
|
-
// Event Bus (for shared
|
|
988
|
+
// Event Bus (for shared extension communication)
|
|
993
989
|
createEventBus
|
|
994
990
|
|
|
995
991
|
// Helpers
|
|
@@ -1015,8 +1011,9 @@ createGrepTool, createFindTool, createLsTool
|
|
|
1015
1011
|
// Types
|
|
1016
1012
|
type CreateAgentSessionOptions
|
|
1017
1013
|
type CreateAgentSessionResult
|
|
1018
|
-
type
|
|
1019
|
-
type
|
|
1014
|
+
type ExtensionFactory
|
|
1015
|
+
type ExtensionAPI
|
|
1016
|
+
type ToolDefinition
|
|
1020
1017
|
type Skill
|
|
1021
1018
|
type PromptTemplate
|
|
1022
1019
|
type Settings
|
|
@@ -1024,28 +1021,4 @@ type SkillsSettings
|
|
|
1024
1021
|
type Tool
|
|
1025
1022
|
```
|
|
1026
1023
|
|
|
1027
|
-
For
|
|
1028
|
-
|
|
1029
|
-
```typescript
|
|
1030
|
-
import type {
|
|
1031
|
-
HookAPI,
|
|
1032
|
-
HookMessage,
|
|
1033
|
-
HookFactory,
|
|
1034
|
-
HookEventContext,
|
|
1035
|
-
HookCommandContext,
|
|
1036
|
-
ToolCallEvent,
|
|
1037
|
-
ToolResultEvent,
|
|
1038
|
-
} from "@mariozechner/pi-coding-agent/hooks";
|
|
1039
|
-
```
|
|
1040
|
-
|
|
1041
|
-
For message utilities:
|
|
1042
|
-
|
|
1043
|
-
```typescript
|
|
1044
|
-
import { isHookMessage, createHookMessage } from "@mariozechner/pi-coding-agent";
|
|
1045
|
-
```
|
|
1046
|
-
|
|
1047
|
-
For config utilities:
|
|
1048
|
-
|
|
1049
|
-
```typescript
|
|
1050
|
-
import { getAgentDir } from "@mariozechner/pi-coding-agent/config";
|
|
1051
|
-
```
|
|
1024
|
+
For extension types, see [extensions.md](extensions.md) for the full API.
|
package/examples/README.md
CHANGED
|
@@ -10,9 +10,12 @@ Programmatic usage via `createAgentSession()`. Shows how to customize models, pr
|
|
|
10
10
|
### [extensions/](extensions/)
|
|
11
11
|
Example extensions demonstrating:
|
|
12
12
|
- Lifecycle event handlers (tool interception, safety gates, context modifications)
|
|
13
|
-
- Custom tools (todo lists, subagents)
|
|
13
|
+
- Custom tools (todo lists, questions, subagents, output truncation)
|
|
14
14
|
- Commands and keyboard shortcuts
|
|
15
|
-
-
|
|
15
|
+
- Custom UI (footers, headers, editors, overlays)
|
|
16
|
+
- Git integration (checkpoints, auto-commit)
|
|
17
|
+
- System prompt modifications and custom compaction
|
|
18
|
+
- External integrations (SSH, file watchers, system theme sync)
|
|
16
19
|
|
|
17
20
|
## Documentation
|
|
18
21
|
|
|
@@ -30,8 +30,10 @@ cp permission-gate.ts ~/.pi/agent/extensions/
|
|
|
30
30
|
|-----------|-------------|
|
|
31
31
|
| `todo.ts` | Todo list tool + `/todos` command with custom rendering and state persistence |
|
|
32
32
|
| `hello.ts` | Minimal custom tool example |
|
|
33
|
-
| `question.ts` | Demonstrates `ctx.ui.select()` for asking the user questions |
|
|
33
|
+
| `question.ts` | Demonstrates `ctx.ui.select()` for asking the user questions with custom UI |
|
|
34
|
+
| `questionnaire.ts` | Multi-question input with tab bar navigation between questions |
|
|
34
35
|
| `tool-override.ts` | Override built-in tools (e.g., add logging/access control to `read`) |
|
|
36
|
+
| `truncated-tool.ts` | Wraps ripgrep with proper output truncation (50KB/2000 lines) |
|
|
35
37
|
| `ssh.ts` | Delegate all tools to a remote machine via SSH using pluggable operations |
|
|
36
38
|
| `subagent/` | Delegate tasks to specialized subagents with isolated context windows |
|
|
37
39
|
|
|
@@ -40,16 +42,24 @@ cp permission-gate.ts ~/.pi/agent/extensions/
|
|
|
40
42
|
| Extension | Description |
|
|
41
43
|
|-----------|-------------|
|
|
42
44
|
| `preset.ts` | Named presets for model, thinking level, tools, and instructions via `--preset` flag and `/preset` command |
|
|
43
|
-
| `plan-mode
|
|
45
|
+
| `plan-mode/` | Claude Code-style plan mode for read-only exploration with `/plan` command and step tracking |
|
|
44
46
|
| `tools.ts` | Interactive `/tools` command to enable/disable tools with session persistence |
|
|
45
47
|
| `handoff.ts` | Transfer context to a new focused session via `/handoff <goal>` |
|
|
46
48
|
| `qna.ts` | Extracts questions from last response into editor via `ctx.ui.setEditorText()` |
|
|
47
49
|
| `status-line.ts` | Shows turn progress in footer via `ctx.ui.setStatus()` with themed colors |
|
|
50
|
+
| `model-status.ts` | Shows model changes in status bar via `model_select` hook |
|
|
48
51
|
| `snake.ts` | Snake game with custom UI, keyboard handling, and session persistence |
|
|
49
52
|
| `send-user-message.ts` | Demonstrates `pi.sendUserMessage()` for sending user messages from extensions |
|
|
50
53
|
| `timed-confirm.ts` | Demonstrates AbortSignal for auto-dismissing `ctx.ui.confirm()` and `ctx.ui.select()` dialogs |
|
|
51
54
|
| `modal-editor.ts` | Custom vim-like modal editor via `ctx.ui.setEditorComponent()` |
|
|
55
|
+
| `rainbow-editor.ts` | Animated rainbow text effect via custom editor |
|
|
52
56
|
| `notify.ts` | Desktop notifications via OSC 777 when agent finishes (Ghostty, iTerm2, WezTerm) |
|
|
57
|
+
| `summarize.ts` | Summarize conversation with GPT-5.2 and show in transient UI |
|
|
58
|
+
| `custom-footer.ts` | Custom footer with git branch and token stats via `ctx.ui.setFooter()` |
|
|
59
|
+
| `custom-header.ts` | Custom header via `ctx.ui.setHeader()` |
|
|
60
|
+
| `overlay-test.ts` | Test overlay rendering with inline text inputs |
|
|
61
|
+
| `shutdown-command.ts` | Adds `/quit` command demonstrating `ctx.shutdown()` |
|
|
62
|
+
| `interactive-shell.ts` | Run interactive commands (vim, htop) with full terminal via `user_bash` hook |
|
|
53
63
|
|
|
54
64
|
### Git Integration
|
|
55
65
|
|
|
@@ -63,8 +73,15 @@ cp permission-gate.ts ~/.pi/agent/extensions/
|
|
|
63
73
|
| Extension | Description |
|
|
64
74
|
|-----------|-------------|
|
|
65
75
|
| `pirate.ts` | Demonstrates `systemPromptAppend` to dynamically modify system prompt |
|
|
76
|
+
| `claude-rules.ts` | Scans `.claude/rules/` folder and lists rules in system prompt |
|
|
66
77
|
| `custom-compaction.ts` | Custom compaction that summarizes entire conversation |
|
|
67
78
|
|
|
79
|
+
### System Integration
|
|
80
|
+
|
|
81
|
+
| Extension | Description |
|
|
82
|
+
|-----------|-------------|
|
|
83
|
+
| `mac-system-theme.ts` | Syncs pi theme with macOS dark/light mode |
|
|
84
|
+
|
|
68
85
|
### External Dependencies
|
|
69
86
|
|
|
70
87
|
| Extension | Description |
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Plan Mode Extension
|
|
2
|
+
|
|
3
|
+
Read-only exploration mode for safe code analysis.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Read-only tools**: Restricts available tools to read, bash, grep, find, ls, question
|
|
8
|
+
- **Bash allowlist**: Only read-only bash commands are allowed
|
|
9
|
+
- **Plan extraction**: Extracts numbered steps from `Plan:` sections
|
|
10
|
+
- **Progress tracking**: Widget shows completion status during execution
|
|
11
|
+
- **[DONE:n] markers**: Explicit step completion tracking
|
|
12
|
+
- **Session persistence**: State survives session resume
|
|
13
|
+
|
|
14
|
+
## Commands
|
|
15
|
+
|
|
16
|
+
- `/plan` - Toggle plan mode
|
|
17
|
+
- `/todos` - Show current plan progress
|
|
18
|
+
- `Shift+P` - Toggle plan mode (shortcut)
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
1. Enable plan mode with `/plan` or `--plan` flag
|
|
23
|
+
2. Ask the agent to analyze code and create a plan
|
|
24
|
+
3. The agent should output a numbered plan under a `Plan:` header:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
Plan:
|
|
28
|
+
1. First step description
|
|
29
|
+
2. Second step description
|
|
30
|
+
3. Third step description
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
4. Choose "Execute the plan" when prompted
|
|
34
|
+
5. During execution, the agent marks steps complete with `[DONE:n]` tags
|
|
35
|
+
6. Progress widget shows completion status
|
|
36
|
+
|
|
37
|
+
## How It Works
|
|
38
|
+
|
|
39
|
+
### Plan Mode (Read-Only)
|
|
40
|
+
- Only read-only tools available
|
|
41
|
+
- Bash commands filtered through allowlist
|
|
42
|
+
- Agent creates a plan without making changes
|
|
43
|
+
|
|
44
|
+
### Execution Mode
|
|
45
|
+
- Full tool access restored
|
|
46
|
+
- Agent executes steps in order
|
|
47
|
+
- `[DONE:n]` markers track completion
|
|
48
|
+
- Widget shows progress
|
|
49
|
+
|
|
50
|
+
### Command Allowlist
|
|
51
|
+
|
|
52
|
+
Safe commands (allowed):
|
|
53
|
+
- File inspection: `cat`, `head`, `tail`, `less`, `more`
|
|
54
|
+
- Search: `grep`, `find`, `rg`, `fd`
|
|
55
|
+
- Directory: `ls`, `pwd`, `tree`
|
|
56
|
+
- Git read: `git status`, `git log`, `git diff`, `git branch`
|
|
57
|
+
- Package info: `npm list`, `npm outdated`, `yarn info`
|
|
58
|
+
- System info: `uname`, `whoami`, `date`, `uptime`
|
|
59
|
+
|
|
60
|
+
Blocked commands:
|
|
61
|
+
- File modification: `rm`, `mv`, `cp`, `mkdir`, `touch`
|
|
62
|
+
- Git write: `git add`, `git commit`, `git push`
|
|
63
|
+
- Package install: `npm install`, `yarn add`, `pip install`
|
|
64
|
+
- System: `sudo`, `kill`, `reboot`
|
|
65
|
+
- Editors: `vim`, `nano`, `code`
|