@mariozechner/pi-coding-agent 0.32.1 → 0.32.3
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 +29 -0
- package/README.md +11 -2
- package/dist/core/agent-session.d.ts +17 -3
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +68 -14
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/slash-commands.d.ts +4 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +12 -4
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +1 -1
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts +6 -0
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +48 -2
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +13 -19
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +3 -1
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +1 -0
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/dist/utils/image-convert.d.ts +9 -0
- package/dist/utils/image-convert.d.ts.map +1 -0
- package/dist/utils/image-convert.js +24 -0
- package/dist/utils/image-convert.js.map +1 -0
- package/dist/utils/image-resize.d.ts +8 -1
- package/dist/utils/image-resize.d.ts.map +1 -1
- package/dist/utils/image-resize.js +104 -49
- package/dist/utils/image-resize.js.map +1 -1
- package/docs/rpc.md +62 -14
- package/docs/sdk.md +40 -0
- package/package.json +4 -4
|
@@ -1,14 +1,27 @@
|
|
|
1
|
+
// 4.5MB - provides headroom below Anthropic's 5MB limit
|
|
2
|
+
const DEFAULT_MAX_BYTES = 4.5 * 1024 * 1024;
|
|
1
3
|
const DEFAULT_OPTIONS = {
|
|
2
4
|
maxWidth: 2000,
|
|
3
5
|
maxHeight: 2000,
|
|
6
|
+
maxBytes: DEFAULT_MAX_BYTES,
|
|
4
7
|
jpegQuality: 80,
|
|
5
8
|
};
|
|
9
|
+
/** Helper to pick the smaller of two buffers */
|
|
10
|
+
function pickSmaller(a, b) {
|
|
11
|
+
return a.buffer.length <= b.buffer.length ? a : b;
|
|
12
|
+
}
|
|
6
13
|
/**
|
|
7
|
-
* Resize an image to fit within the specified max dimensions.
|
|
14
|
+
* Resize an image to fit within the specified max dimensions and file size.
|
|
8
15
|
* Returns the original image if it already fits within the limits.
|
|
9
16
|
*
|
|
10
17
|
* Uses sharp for image processing. If sharp is not available (e.g., in some
|
|
11
18
|
* environments), returns the original image unchanged.
|
|
19
|
+
*
|
|
20
|
+
* Strategy for staying under maxBytes:
|
|
21
|
+
* 1. First resize to maxWidth/maxHeight
|
|
22
|
+
* 2. Try both PNG and JPEG formats, pick the smaller one
|
|
23
|
+
* 3. If still too large, try JPEG with decreasing quality
|
|
24
|
+
* 4. If still too large, progressively reduce dimensions
|
|
12
25
|
*/
|
|
13
26
|
export async function resizeImage(img, options) {
|
|
14
27
|
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
@@ -32,68 +45,110 @@ export async function resizeImage(img, options) {
|
|
|
32
45
|
}
|
|
33
46
|
const sharpImg = sharp(buffer);
|
|
34
47
|
const metadata = await sharpImg.metadata();
|
|
35
|
-
const
|
|
36
|
-
const
|
|
48
|
+
const originalWidth = metadata.width ?? 0;
|
|
49
|
+
const originalHeight = metadata.height ?? 0;
|
|
37
50
|
const format = metadata.format ?? img.mimeType?.split("/")[1] ?? "png";
|
|
38
|
-
// Check if already within limits
|
|
39
|
-
|
|
51
|
+
// Check if already within all limits (dimensions AND size)
|
|
52
|
+
const originalSize = buffer.length;
|
|
53
|
+
if (originalWidth <= opts.maxWidth && originalHeight <= opts.maxHeight && originalSize <= opts.maxBytes) {
|
|
40
54
|
return {
|
|
41
55
|
data: img.data,
|
|
42
56
|
mimeType: img.mimeType ?? `image/${format}`,
|
|
43
|
-
originalWidth
|
|
44
|
-
originalHeight
|
|
45
|
-
width,
|
|
46
|
-
height,
|
|
57
|
+
originalWidth,
|
|
58
|
+
originalHeight,
|
|
59
|
+
width: originalWidth,
|
|
60
|
+
height: originalHeight,
|
|
47
61
|
wasResized: false,
|
|
48
62
|
};
|
|
49
63
|
}
|
|
50
|
-
// Calculate
|
|
51
|
-
let
|
|
52
|
-
let
|
|
53
|
-
if (
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
if (newHeight > opts.maxHeight) {
|
|
58
|
-
newWidth = Math.round((newWidth * opts.maxHeight) / newHeight);
|
|
59
|
-
newHeight = opts.maxHeight;
|
|
64
|
+
// Calculate initial dimensions respecting max limits
|
|
65
|
+
let targetWidth = originalWidth;
|
|
66
|
+
let targetHeight = originalHeight;
|
|
67
|
+
if (targetWidth > opts.maxWidth) {
|
|
68
|
+
targetHeight = Math.round((targetHeight * opts.maxWidth) / targetWidth);
|
|
69
|
+
targetWidth = opts.maxWidth;
|
|
60
70
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
.toBuffer();
|
|
65
|
-
// Determine output format - preserve original if possible, otherwise use JPEG
|
|
66
|
-
let outputMimeType;
|
|
67
|
-
let outputBuffer;
|
|
68
|
-
if (format === "jpeg" || format === "jpg") {
|
|
69
|
-
outputBuffer = await sharp(resized).jpeg({ quality: opts.jpegQuality }).toBuffer();
|
|
70
|
-
outputMimeType = "image/jpeg";
|
|
71
|
+
if (targetHeight > opts.maxHeight) {
|
|
72
|
+
targetWidth = Math.round((targetWidth * opts.maxHeight) / targetHeight);
|
|
73
|
+
targetHeight = opts.maxHeight;
|
|
71
74
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
+
// Helper to resize and encode in both formats, returning the smaller one
|
|
76
|
+
async function tryBothFormats(width, height, jpegQuality) {
|
|
77
|
+
const resized = await sharp(buffer)
|
|
78
|
+
.resize(width, height, { fit: "inside", withoutEnlargement: true })
|
|
79
|
+
.toBuffer();
|
|
80
|
+
const [pngBuffer, jpegBuffer] = await Promise.all([
|
|
81
|
+
sharp(resized).png({ compressionLevel: 9 }).toBuffer(),
|
|
82
|
+
sharp(resized).jpeg({ quality: jpegQuality }).toBuffer(),
|
|
83
|
+
]);
|
|
84
|
+
return pickSmaller({ buffer: pngBuffer, mimeType: "image/png" }, { buffer: jpegBuffer, mimeType: "image/jpeg" });
|
|
75
85
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
86
|
+
// Try to produce an image under maxBytes
|
|
87
|
+
const qualitySteps = [85, 70, 55, 40];
|
|
88
|
+
const scaleSteps = [1.0, 0.75, 0.5, 0.35, 0.25];
|
|
89
|
+
let best;
|
|
90
|
+
let finalWidth = targetWidth;
|
|
91
|
+
let finalHeight = targetHeight;
|
|
92
|
+
// First attempt: resize to target dimensions, try both formats
|
|
93
|
+
best = await tryBothFormats(targetWidth, targetHeight, opts.jpegQuality);
|
|
94
|
+
if (best.buffer.length <= opts.maxBytes) {
|
|
95
|
+
return {
|
|
96
|
+
data: best.buffer.toString("base64"),
|
|
97
|
+
mimeType: best.mimeType,
|
|
98
|
+
originalWidth,
|
|
99
|
+
originalHeight,
|
|
100
|
+
width: finalWidth,
|
|
101
|
+
height: finalHeight,
|
|
102
|
+
wasResized: true,
|
|
103
|
+
};
|
|
80
104
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
105
|
+
// Still too large - try JPEG with decreasing quality (and compare to PNG each time)
|
|
106
|
+
for (const quality of qualitySteps) {
|
|
107
|
+
best = await tryBothFormats(targetWidth, targetHeight, quality);
|
|
108
|
+
if (best.buffer.length <= opts.maxBytes) {
|
|
109
|
+
return {
|
|
110
|
+
data: best.buffer.toString("base64"),
|
|
111
|
+
mimeType: best.mimeType,
|
|
112
|
+
originalWidth,
|
|
113
|
+
originalHeight,
|
|
114
|
+
width: finalWidth,
|
|
115
|
+
height: finalHeight,
|
|
116
|
+
wasResized: true,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
84
119
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
120
|
+
// Still too large - reduce dimensions progressively
|
|
121
|
+
for (const scale of scaleSteps) {
|
|
122
|
+
finalWidth = Math.round(targetWidth * scale);
|
|
123
|
+
finalHeight = Math.round(targetHeight * scale);
|
|
124
|
+
// Skip if dimensions are too small
|
|
125
|
+
if (finalWidth < 100 || finalHeight < 100) {
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
for (const quality of qualitySteps) {
|
|
129
|
+
best = await tryBothFormats(finalWidth, finalHeight, quality);
|
|
130
|
+
if (best.buffer.length <= opts.maxBytes) {
|
|
131
|
+
return {
|
|
132
|
+
data: best.buffer.toString("base64"),
|
|
133
|
+
mimeType: best.mimeType,
|
|
134
|
+
originalWidth,
|
|
135
|
+
originalHeight,
|
|
136
|
+
width: finalWidth,
|
|
137
|
+
height: finalHeight,
|
|
138
|
+
wasResized: true,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
89
142
|
}
|
|
143
|
+
// Last resort: return smallest version we produced even if over limit
|
|
144
|
+
// (the API will reject it, but at least we tried everything)
|
|
90
145
|
return {
|
|
91
|
-
data:
|
|
92
|
-
mimeType:
|
|
93
|
-
originalWidth
|
|
94
|
-
originalHeight
|
|
95
|
-
width:
|
|
96
|
-
height:
|
|
146
|
+
data: best.buffer.toString("base64"),
|
|
147
|
+
mimeType: best.mimeType,
|
|
148
|
+
originalWidth,
|
|
149
|
+
originalHeight,
|
|
150
|
+
width: finalWidth,
|
|
151
|
+
height: finalHeight,
|
|
97
152
|
wasResized: true,
|
|
98
153
|
};
|
|
99
154
|
}
|
|
@@ -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\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\nconst DEFAULT_OPTIONS: Required<ImageResizeOptions> = {\n\tmaxWidth: 2000,\n\tmaxHeight: 2000,\n\tjpegQuality: 80,\n};\n\n/**\n * Resize an image to fit within the specified max dimensions.\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 */\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 width = metadata.width ?? 0;\n\tconst height = metadata.height ?? 0;\n\tconst format = metadata.format ?? img.mimeType?.split(\"/\")[1] ?? \"png\";\n\n\t// Check if already within limits\n\tif (width <= opts.maxWidth && height <= opts.maxHeight) {\n\t\treturn {\n\t\t\tdata: img.data,\n\t\t\tmimeType: img.mimeType ?? `image/${format}`,\n\t\t\toriginalWidth: width,\n\t\t\toriginalHeight: height,\n\t\t\twidth,\n\t\t\theight,\n\t\t\twasResized: false,\n\t\t};\n\t}\n\n\t// Calculate new dimensions maintaining aspect ratio\n\tlet newWidth = width;\n\tlet newHeight = height;\n\n\tif (newWidth > opts.maxWidth) {\n\t\tnewHeight = Math.round((newHeight * opts.maxWidth) / newWidth);\n\t\tnewWidth = opts.maxWidth;\n\t}\n\tif (newHeight > opts.maxHeight) {\n\t\tnewWidth = Math.round((newWidth * opts.maxHeight) / newHeight);\n\t\tnewHeight = opts.maxHeight;\n\t}\n\n\t// Resize the image\n\tconst resized = await sharp(buffer)\n\t\t.resize(newWidth, newHeight, { fit: \"inside\", withoutEnlargement: true })\n\t\t.toBuffer();\n\n\t// Determine output format - preserve original if possible, otherwise use JPEG\n\tlet outputMimeType: string;\n\tlet outputBuffer: Buffer;\n\n\tif (format === \"jpeg\" || format === \"jpg\") {\n\t\toutputBuffer = await sharp(resized).jpeg({ quality: opts.jpegQuality }).toBuffer();\n\t\toutputMimeType = \"image/jpeg\";\n\t} else if (format === \"png\") {\n\t\toutputBuffer = resized;\n\t\toutputMimeType = \"image/png\";\n\t} else if (format === \"gif\") {\n\t\t// GIF resize might not preserve animation; convert to PNG for quality\n\t\toutputBuffer = resized;\n\t\toutputMimeType = \"image/png\";\n\t} else if (format === \"webp\") {\n\t\toutputBuffer = resized;\n\t\toutputMimeType = \"image/webp\";\n\t} else {\n\t\t// Default to JPEG for unknown formats\n\t\toutputBuffer = await sharp(resized).jpeg({ quality: opts.jpegQuality }).toBuffer();\n\t\toutputMimeType = \"image/jpeg\";\n\t}\n\n\treturn {\n\t\tdata: outputBuffer.toString(\"base64\"),\n\t\tmimeType: outputMimeType,\n\t\toriginalWidth: width,\n\t\toriginalHeight: height,\n\t\twidth: newWidth,\n\t\theight: newHeight,\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\";\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"]}
|
package/docs/rpc.md
CHANGED
|
@@ -41,6 +41,21 @@ With images:
|
|
|
41
41
|
{"type": "prompt", "message": "What's in this image?", "images": [{"type": "image", "source": {"type": "base64", "mediaType": "image/png", "data": "..."}}]}
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
+
**During streaming**: If the agent is already streaming, you must specify `streamingBehavior` to queue the message:
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{"type": "prompt", "message": "New instruction", "streamingBehavior": "steer"}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
- `"steer"`: Interrupt the agent mid-run. Message is delivered after current tool execution, remaining tools are skipped.
|
|
51
|
+
- `"followUp"`: Wait until the agent finishes. Message is delivered only when agent stops.
|
|
52
|
+
|
|
53
|
+
If the agent is streaming and no `streamingBehavior` is specified, the command returns an error.
|
|
54
|
+
|
|
55
|
+
**Hook commands**: If the message is a hook command (e.g., `/mycommand`), it executes immediately even during streaming. Hook commands manage their own LLM interaction via `pi.sendMessage()`.
|
|
56
|
+
|
|
57
|
+
**Slash commands**: File-based slash commands (from `.md` files) are expanded before sending/queueing.
|
|
58
|
+
|
|
44
59
|
Response:
|
|
45
60
|
```json
|
|
46
61
|
{"id": "req-1", "type": "response", "command": "prompt", "success": true}
|
|
@@ -48,20 +63,35 @@ Response:
|
|
|
48
63
|
|
|
49
64
|
The `images` field is optional. Each image uses `ImageContent` format with base64 or URL source.
|
|
50
65
|
|
|
51
|
-
####
|
|
66
|
+
#### steer
|
|
52
67
|
|
|
53
|
-
Queue a message to
|
|
68
|
+
Queue a steering message to interrupt the agent mid-run. Delivered after current tool execution, remaining tools are skipped. File-based slash commands are expanded. Hook commands are not allowed (use `prompt` instead).
|
|
54
69
|
|
|
55
70
|
```json
|
|
56
|
-
{"type": "
|
|
71
|
+
{"type": "steer", "message": "Stop and do this instead"}
|
|
57
72
|
```
|
|
58
73
|
|
|
59
74
|
Response:
|
|
60
75
|
```json
|
|
61
|
-
{"type": "response", "command": "
|
|
76
|
+
{"type": "response", "command": "steer", "success": true}
|
|
62
77
|
```
|
|
63
78
|
|
|
64
|
-
See [
|
|
79
|
+
See [set_steering_mode](#set_steering_mode) for controlling how steering messages are processed.
|
|
80
|
+
|
|
81
|
+
#### follow_up
|
|
82
|
+
|
|
83
|
+
Queue a follow-up message to be processed after the agent finishes. Delivered only when agent has no more tool calls or steering messages. File-based slash commands are expanded. Hook commands are not allowed (use `prompt` instead).
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
{"type": "follow_up", "message": "After you're done, also do this"}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Response:
|
|
90
|
+
```json
|
|
91
|
+
{"type": "response", "command": "follow_up", "success": true}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
See [set_follow_up_mode](#set_follow_up_mode) for controlling how follow-up messages are processed.
|
|
65
95
|
|
|
66
96
|
#### abort
|
|
67
97
|
|
|
@@ -120,12 +150,13 @@ Response:
|
|
|
120
150
|
"thinkingLevel": "medium",
|
|
121
151
|
"isStreaming": false,
|
|
122
152
|
"isCompacting": false,
|
|
123
|
-
"
|
|
153
|
+
"steeringMode": "all",
|
|
154
|
+
"followUpMode": "one-at-a-time",
|
|
124
155
|
"sessionFile": "/path/to/session.jsonl",
|
|
125
156
|
"sessionId": "abc123",
|
|
126
157
|
"autoCompactionEnabled": true,
|
|
127
158
|
"messageCount": 5,
|
|
128
|
-
"
|
|
159
|
+
"pendingMessageCount": 0
|
|
129
160
|
}
|
|
130
161
|
}
|
|
131
162
|
```
|
|
@@ -253,23 +284,40 @@ Response:
|
|
|
253
284
|
}
|
|
254
285
|
```
|
|
255
286
|
|
|
256
|
-
### Queue
|
|
287
|
+
### Queue Modes
|
|
288
|
+
|
|
289
|
+
#### set_steering_mode
|
|
290
|
+
|
|
291
|
+
Control how steering messages (from `steer`) are delivered.
|
|
292
|
+
|
|
293
|
+
```json
|
|
294
|
+
{"type": "set_steering_mode", "mode": "one-at-a-time"}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Modes:
|
|
298
|
+
- `"all"`: Deliver all steering messages at the next interruption point
|
|
299
|
+
- `"one-at-a-time"`: Deliver one steering message per interruption (default)
|
|
300
|
+
|
|
301
|
+
Response:
|
|
302
|
+
```json
|
|
303
|
+
{"type": "response", "command": "set_steering_mode", "success": true}
|
|
304
|
+
```
|
|
257
305
|
|
|
258
|
-
####
|
|
306
|
+
#### set_follow_up_mode
|
|
259
307
|
|
|
260
|
-
Control how
|
|
308
|
+
Control how follow-up messages (from `follow_up`) are delivered.
|
|
261
309
|
|
|
262
310
|
```json
|
|
263
|
-
{"type": "
|
|
311
|
+
{"type": "set_follow_up_mode", "mode": "one-at-a-time"}
|
|
264
312
|
```
|
|
265
313
|
|
|
266
314
|
Modes:
|
|
267
|
-
- `"all"`:
|
|
268
|
-
- `"one-at-a-time"`:
|
|
315
|
+
- `"all"`: Deliver all follow-up messages when agent finishes
|
|
316
|
+
- `"one-at-a-time"`: Deliver one follow-up message per agent completion (default)
|
|
269
317
|
|
|
270
318
|
Response:
|
|
271
319
|
```json
|
|
272
|
-
{"type": "response", "command": "
|
|
320
|
+
{"type": "response", "command": "set_follow_up_mode", "success": true}
|
|
273
321
|
```
|
|
274
322
|
|
|
275
323
|
### Compaction
|
package/docs/sdk.md
CHANGED
|
@@ -77,8 +77,13 @@ The session manages the agent lifecycle, message history, and event streaming.
|
|
|
77
77
|
```typescript
|
|
78
78
|
interface AgentSession {
|
|
79
79
|
// Send a prompt and wait for completion
|
|
80
|
+
// If streaming, requires streamingBehavior option to queue the message
|
|
80
81
|
prompt(text: string, options?: PromptOptions): Promise<void>;
|
|
81
82
|
|
|
83
|
+
// Queue messages during streaming
|
|
84
|
+
steer(text: string): Promise<void>; // Interrupt: delivered after current tool, skips remaining
|
|
85
|
+
followUp(text: string): Promise<void>; // Wait: delivered only when agent finishes
|
|
86
|
+
|
|
82
87
|
// Subscribe to events (returns unsubscribe function)
|
|
83
88
|
subscribe(listener: (event: AgentSessionEvent) => void): () => void;
|
|
84
89
|
|
|
@@ -122,6 +127,41 @@ interface AgentSession {
|
|
|
122
127
|
}
|
|
123
128
|
```
|
|
124
129
|
|
|
130
|
+
### Prompting and Message Queueing
|
|
131
|
+
|
|
132
|
+
The `prompt()` method handles slash commands, hook commands, and message sending:
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
// Basic prompt (when not streaming)
|
|
136
|
+
await session.prompt("What files are here?");
|
|
137
|
+
|
|
138
|
+
// With images
|
|
139
|
+
await session.prompt("What's in this image?", {
|
|
140
|
+
images: [{ type: "image", source: { type: "base64", mediaType: "image/png", data: "..." } }]
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// During streaming: must specify how to queue the message
|
|
144
|
+
await session.prompt("Stop and do this instead", { streamingBehavior: "steer" });
|
|
145
|
+
await session.prompt("After you're done, also check X", { streamingBehavior: "followUp" });
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
**Behavior:**
|
|
149
|
+
- **Hook commands** (e.g., `/mycommand`): Execute immediately, even during streaming. They manage their own LLM interaction via `pi.sendMessage()`.
|
|
150
|
+
- **File-based slash commands** (from `.md` files): Expanded to their content before sending/queueing.
|
|
151
|
+
- **During streaming without `streamingBehavior`**: Throws an error. Use `steer()` or `followUp()` directly, or specify the option.
|
|
152
|
+
|
|
153
|
+
For explicit queueing during streaming:
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
// Interrupt the agent (delivered after current tool, skips remaining tools)
|
|
157
|
+
await session.steer("New instruction");
|
|
158
|
+
|
|
159
|
+
// Wait for agent to finish (delivered only when agent stops)
|
|
160
|
+
await session.followUp("After you're done, also do this");
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Both `steer()` and `followUp()` expand file-based slash commands but error on hook commands (hook commands cannot be queued).
|
|
164
|
+
|
|
125
165
|
### Agent and AgentState
|
|
126
166
|
|
|
127
167
|
The `Agent` class (from `@mariozechner/pi-agent-core`) handles the core LLM interaction. Access it via `session.agent`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mariozechner/pi-coding-agent",
|
|
3
|
-
"version": "0.32.
|
|
3
|
+
"version": "0.32.3",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"piConfig": {
|
|
@@ -38,9 +38,9 @@
|
|
|
38
38
|
"prepublishOnly": "npm run clean && npm run build"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@mariozechner/pi-agent-core": "^0.32.
|
|
42
|
-
"@mariozechner/pi-ai": "^0.32.
|
|
43
|
-
"@mariozechner/pi-tui": "^0.32.
|
|
41
|
+
"@mariozechner/pi-agent-core": "^0.32.3",
|
|
42
|
+
"@mariozechner/pi-ai": "^0.32.3",
|
|
43
|
+
"@mariozechner/pi-tui": "^0.32.3",
|
|
44
44
|
"chalk": "^5.5.0",
|
|
45
45
|
"cli-highlight": "^2.1.11",
|
|
46
46
|
"diff": "^8.0.2",
|