@oh-my-pi/pi-coding-agent 4.8.0 → 4.8.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 +6 -0
- package/package.json +6 -8
- package/src/utils/image-convert.ts +38 -12
- package/src/utils/image-resize.ts +128 -109
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
3
|
-
"version": "4.8.
|
|
3
|
+
"version": "4.8.3",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"ompConfig": {
|
|
@@ -39,10 +39,10 @@
|
|
|
39
39
|
"prepublishOnly": "bun run generate-template && bun run clean && bun run build"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@oh-my-pi/pi-
|
|
43
|
-
"@oh-my-pi/pi-
|
|
44
|
-
"@oh-my-pi/pi-git-tool": "4.8.
|
|
45
|
-
"@oh-my-pi/pi-tui": "4.8.
|
|
42
|
+
"@oh-my-pi/pi-agent-core": "4.8.3",
|
|
43
|
+
"@oh-my-pi/pi-ai": "4.8.3",
|
|
44
|
+
"@oh-my-pi/pi-git-tool": "4.8.3",
|
|
45
|
+
"@oh-my-pi/pi-tui": "4.8.3",
|
|
46
46
|
"@openai/agents": "^0.3.7",
|
|
47
47
|
"@sinclair/typebox": "^0.34.46",
|
|
48
48
|
"ajv": "^8.17.1",
|
|
@@ -60,6 +60,7 @@
|
|
|
60
60
|
"node-html-parser": "^6.1.13",
|
|
61
61
|
"smol-toml": "^1.6.0",
|
|
62
62
|
"strip-ansi": "^7.1.2",
|
|
63
|
+
"wasm-vips": "^0.0.16",
|
|
63
64
|
"winston": "^3.17.0",
|
|
64
65
|
"winston-daily-rotate-file": "^5.0.0",
|
|
65
66
|
"zod": "^4.3.5"
|
|
@@ -71,9 +72,6 @@
|
|
|
71
72
|
"@types/node": "^24.3.0",
|
|
72
73
|
"ms": "^2.1.3"
|
|
73
74
|
},
|
|
74
|
-
"optionalDependencies": {
|
|
75
|
-
"sharp": "^0.34.2"
|
|
76
|
-
},
|
|
77
75
|
"keywords": [
|
|
78
76
|
"coding-agent",
|
|
79
77
|
"ai",
|
|
@@ -1,9 +1,29 @@
|
|
|
1
1
|
import { convertToPngWithImageMagick } from "./image-magick";
|
|
2
2
|
|
|
3
|
+
// Cached vips instance
|
|
4
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5
|
+
let vipsInstance: any;
|
|
6
|
+
let vipsLoadFailed = false;
|
|
7
|
+
|
|
8
|
+
async function getVips() {
|
|
9
|
+
if (vipsLoadFailed) return undefined;
|
|
10
|
+
if (vipsInstance) return vipsInstance;
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const wasmVips = await import("wasm-vips");
|
|
14
|
+
const Vips = wasmVips.default ?? wasmVips;
|
|
15
|
+
vipsInstance = await Vips();
|
|
16
|
+
return vipsInstance;
|
|
17
|
+
} catch {
|
|
18
|
+
vipsLoadFailed = true;
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
3
23
|
/**
|
|
4
24
|
* Convert image to PNG format for terminal display.
|
|
5
25
|
* Kitty graphics protocol requires PNG format (f=100).
|
|
6
|
-
* Uses
|
|
26
|
+
* Uses wasm-vips if available, falls back to ImageMagick (magick/convert).
|
|
7
27
|
*/
|
|
8
28
|
export async function convertToPng(
|
|
9
29
|
base64Data: string,
|
|
@@ -14,17 +34,23 @@ export async function convertToPng(
|
|
|
14
34
|
return { data: base64Data, mimeType };
|
|
15
35
|
}
|
|
16
36
|
|
|
17
|
-
// Try
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
37
|
+
// Try wasm-vips first
|
|
38
|
+
const vips = await getVips();
|
|
39
|
+
if (vips) {
|
|
40
|
+
let image: ReturnType<typeof vips.Image.newFromBuffer> | undefined;
|
|
41
|
+
try {
|
|
42
|
+
const buffer = Buffer.from(base64Data, "base64");
|
|
43
|
+
image = vips.Image.newFromBuffer(buffer);
|
|
44
|
+
const pngBuffer = image.writeToBuffer(".png");
|
|
45
|
+
return {
|
|
46
|
+
data: Buffer.from(pngBuffer).toString("base64"),
|
|
47
|
+
mimeType: "image/png",
|
|
48
|
+
};
|
|
49
|
+
} catch {
|
|
50
|
+
// wasm-vips failed, try ImageMagick fallback
|
|
51
|
+
} finally {
|
|
52
|
+
image?.delete();
|
|
53
|
+
}
|
|
28
54
|
}
|
|
29
55
|
|
|
30
56
|
// Fall back to ImageMagick
|
|
@@ -29,18 +29,16 @@ const DEFAULT_OPTIONS: Required<ImageResizeOptions> = {
|
|
|
29
29
|
};
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
|
-
* Fallback resize using ImageMagick when
|
|
32
|
+
* Fallback resize using ImageMagick when wasm-vips is unavailable.
|
|
33
33
|
*/
|
|
34
34
|
async function resizeImageWithImageMagick(
|
|
35
35
|
img: ImageContent,
|
|
36
36
|
opts: Required<ImageResizeOptions>,
|
|
37
37
|
): Promise<ResizedImage> {
|
|
38
|
-
// Try to get dimensions first
|
|
39
38
|
const dims = await getImageDimensionsWithImageMagick(img.data);
|
|
40
39
|
const originalWidth = dims?.width ?? 0;
|
|
41
40
|
const originalHeight = dims?.height ?? 0;
|
|
42
41
|
|
|
43
|
-
// Try to resize
|
|
44
42
|
const result = await resizeWithImageMagick(
|
|
45
43
|
img.data,
|
|
46
44
|
img.mimeType,
|
|
@@ -62,7 +60,6 @@ async function resizeImageWithImageMagick(
|
|
|
62
60
|
};
|
|
63
61
|
}
|
|
64
62
|
|
|
65
|
-
// ImageMagick not available or resize not needed - return original
|
|
66
63
|
return {
|
|
67
64
|
data: img.data,
|
|
68
65
|
mimeType: img.mimeType,
|
|
@@ -76,18 +73,37 @@ async function resizeImageWithImageMagick(
|
|
|
76
73
|
|
|
77
74
|
/** Helper to pick the smaller of two buffers */
|
|
78
75
|
function pickSmaller(
|
|
79
|
-
a: { buffer:
|
|
80
|
-
b: { buffer:
|
|
81
|
-
): { buffer:
|
|
76
|
+
a: { buffer: Uint8Array; mimeType: string },
|
|
77
|
+
b: { buffer: Uint8Array; mimeType: string },
|
|
78
|
+
): { buffer: Uint8Array; mimeType: string } {
|
|
82
79
|
return a.buffer.length <= b.buffer.length ? a : b;
|
|
83
80
|
}
|
|
84
81
|
|
|
82
|
+
// Cached vips instance
|
|
83
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
84
|
+
let vipsInstance: any;
|
|
85
|
+
let vipsLoadFailed = false;
|
|
86
|
+
|
|
87
|
+
async function getVips() {
|
|
88
|
+
if (vipsLoadFailed) return undefined;
|
|
89
|
+
if (vipsInstance) return vipsInstance;
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const wasmVips = await import("wasm-vips");
|
|
93
|
+
const Vips = wasmVips.default ?? wasmVips;
|
|
94
|
+
vipsInstance = await Vips();
|
|
95
|
+
return vipsInstance;
|
|
96
|
+
} catch {
|
|
97
|
+
vipsLoadFailed = true;
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
85
102
|
/**
|
|
86
103
|
* Resize an image to fit within the specified max dimensions and file size.
|
|
87
104
|
* Returns the original image if it already fits within the limits.
|
|
88
105
|
*
|
|
89
|
-
* Uses
|
|
90
|
-
* environments), returns the original image unchanged.
|
|
106
|
+
* Uses wasm-vips for image processing. Falls back to ImageMagick if unavailable.
|
|
91
107
|
*
|
|
92
108
|
* Strategy for staying under maxBytes:
|
|
93
109
|
* 1. First resize to maxWidth/maxHeight
|
|
@@ -99,96 +115,79 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption
|
|
|
99
115
|
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
100
116
|
const buffer = Buffer.from(img.data, "base64");
|
|
101
117
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
sharp = (await import("sharp")).default;
|
|
105
|
-
} catch {
|
|
106
|
-
// Sharp not available - try ImageMagick fallback
|
|
118
|
+
const vips = await getVips();
|
|
119
|
+
if (!vips) {
|
|
107
120
|
return resizeImageWithImageMagick(img, opts);
|
|
108
121
|
}
|
|
109
122
|
|
|
110
|
-
|
|
111
|
-
|
|
123
|
+
let image: ReturnType<typeof vips.Image.newFromBuffer> | undefined;
|
|
124
|
+
try {
|
|
125
|
+
image = vips.Image.newFromBuffer(buffer);
|
|
126
|
+
const originalWidth = image.width;
|
|
127
|
+
const originalHeight = image.height;
|
|
128
|
+
const format = img.mimeType?.split("/")[1] ?? "png";
|
|
129
|
+
|
|
130
|
+
// Check if already within all limits (dimensions AND size)
|
|
131
|
+
const originalSize = buffer.length;
|
|
132
|
+
if (originalWidth <= opts.maxWidth && originalHeight <= opts.maxHeight && originalSize <= opts.maxBytes) {
|
|
133
|
+
return {
|
|
134
|
+
data: img.data,
|
|
135
|
+
mimeType: img.mimeType ?? `image/${format}`,
|
|
136
|
+
originalWidth,
|
|
137
|
+
originalHeight,
|
|
138
|
+
width: originalWidth,
|
|
139
|
+
height: originalHeight,
|
|
140
|
+
wasResized: false,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
112
143
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
144
|
+
// Calculate initial dimensions respecting max limits
|
|
145
|
+
let targetWidth = originalWidth;
|
|
146
|
+
let targetHeight = originalHeight;
|
|
116
147
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
width: originalWidth,
|
|
126
|
-
height: originalHeight,
|
|
127
|
-
wasResized: false,
|
|
128
|
-
};
|
|
129
|
-
}
|
|
148
|
+
if (targetWidth > opts.maxWidth) {
|
|
149
|
+
targetHeight = Math.round((targetHeight * opts.maxWidth) / targetWidth);
|
|
150
|
+
targetWidth = opts.maxWidth;
|
|
151
|
+
}
|
|
152
|
+
if (targetHeight > opts.maxHeight) {
|
|
153
|
+
targetWidth = Math.round((targetWidth * opts.maxHeight) / targetHeight);
|
|
154
|
+
targetHeight = opts.maxHeight;
|
|
155
|
+
}
|
|
130
156
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
157
|
+
// Helper to resize and encode in both formats, returning the smaller one
|
|
158
|
+
function tryBothFormats(
|
|
159
|
+
width: number,
|
|
160
|
+
height: number,
|
|
161
|
+
jpegQuality: number,
|
|
162
|
+
): { buffer: Uint8Array; mimeType: string } {
|
|
163
|
+
const scale = Math.min(width / originalWidth, height / originalHeight);
|
|
164
|
+
const resized = image!.resize(scale);
|
|
134
165
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
targetWidth = opts.maxWidth;
|
|
138
|
-
}
|
|
139
|
-
if (targetHeight > opts.maxHeight) {
|
|
140
|
-
targetWidth = Math.round((targetWidth * opts.maxHeight) / targetHeight);
|
|
141
|
-
targetHeight = opts.maxHeight;
|
|
142
|
-
}
|
|
166
|
+
const pngBuffer = resized.writeToBuffer(".png");
|
|
167
|
+
const jpegBuffer = resized.writeToBuffer(".jpg", { Q: jpegQuality });
|
|
143
168
|
|
|
144
|
-
|
|
145
|
-
async function tryBothFormats(
|
|
146
|
-
width: number,
|
|
147
|
-
height: number,
|
|
148
|
-
jpegQuality: number,
|
|
149
|
-
): Promise<{ buffer: Buffer; mimeType: string }> {
|
|
150
|
-
const resized = await sharp!(buffer)
|
|
151
|
-
.resize(width, height, { fit: "inside", withoutEnlargement: true })
|
|
152
|
-
.toBuffer();
|
|
153
|
-
|
|
154
|
-
const [pngBuffer, jpegBuffer] = await Promise.all([
|
|
155
|
-
sharp!(resized).png({ compressionLevel: 9 }).toBuffer(),
|
|
156
|
-
sharp!(resized).jpeg({ quality: jpegQuality }).toBuffer(),
|
|
157
|
-
]);
|
|
158
|
-
|
|
159
|
-
return pickSmaller({ buffer: pngBuffer, mimeType: "image/png" }, { buffer: jpegBuffer, mimeType: "image/jpeg" });
|
|
160
|
-
}
|
|
169
|
+
resized.delete();
|
|
161
170
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
171
|
+
return pickSmaller(
|
|
172
|
+
{ buffer: pngBuffer, mimeType: "image/png" },
|
|
173
|
+
{ buffer: jpegBuffer, mimeType: "image/jpeg" },
|
|
174
|
+
);
|
|
175
|
+
}
|
|
165
176
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
177
|
+
// Try to produce an image under maxBytes
|
|
178
|
+
const qualitySteps = [85, 70, 55, 40];
|
|
179
|
+
const scaleSteps = [1.0, 0.75, 0.5, 0.35, 0.25];
|
|
169
180
|
|
|
170
|
-
|
|
171
|
-
|
|
181
|
+
let best: { buffer: Uint8Array; mimeType: string };
|
|
182
|
+
let finalWidth = targetWidth;
|
|
183
|
+
let finalHeight = targetHeight;
|
|
172
184
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
data: best.buffer.toString("base64"),
|
|
176
|
-
mimeType: best.mimeType,
|
|
177
|
-
originalWidth,
|
|
178
|
-
originalHeight,
|
|
179
|
-
width: finalWidth,
|
|
180
|
-
height: finalHeight,
|
|
181
|
-
wasResized: true,
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// Still too large - try JPEG with decreasing quality (and compare to PNG each time)
|
|
186
|
-
for (const quality of qualitySteps) {
|
|
187
|
-
best = await tryBothFormats(targetWidth, targetHeight, quality);
|
|
185
|
+
// First attempt: resize to target dimensions, try both formats
|
|
186
|
+
best = tryBothFormats(targetWidth, targetHeight, opts.jpegQuality);
|
|
188
187
|
|
|
189
188
|
if (best.buffer.length <= opts.maxBytes) {
|
|
190
189
|
return {
|
|
191
|
-
data: best.buffer.toString("base64"),
|
|
190
|
+
data: Buffer.from(best.buffer).toString("base64"),
|
|
192
191
|
mimeType: best.mimeType,
|
|
193
192
|
originalWidth,
|
|
194
193
|
originalHeight,
|
|
@@ -197,24 +196,14 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption
|
|
|
197
196
|
wasResized: true,
|
|
198
197
|
};
|
|
199
198
|
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// Still too large - reduce dimensions progressively
|
|
203
|
-
for (const scale of scaleSteps) {
|
|
204
|
-
finalWidth = Math.round(targetWidth * scale);
|
|
205
|
-
finalHeight = Math.round(targetHeight * scale);
|
|
206
|
-
|
|
207
|
-
// Skip if dimensions are too small
|
|
208
|
-
if (finalWidth < 100 || finalHeight < 100) {
|
|
209
|
-
break;
|
|
210
|
-
}
|
|
211
199
|
|
|
200
|
+
// Still too large - try JPEG with decreasing quality
|
|
212
201
|
for (const quality of qualitySteps) {
|
|
213
|
-
best =
|
|
202
|
+
best = tryBothFormats(targetWidth, targetHeight, quality);
|
|
214
203
|
|
|
215
204
|
if (best.buffer.length <= opts.maxBytes) {
|
|
216
205
|
return {
|
|
217
|
-
data: best.buffer.toString("base64"),
|
|
206
|
+
data: Buffer.from(best.buffer).toString("base64"),
|
|
218
207
|
mimeType: best.mimeType,
|
|
219
208
|
originalWidth,
|
|
220
209
|
originalHeight,
|
|
@@ -224,19 +213,49 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption
|
|
|
224
213
|
};
|
|
225
214
|
}
|
|
226
215
|
}
|
|
227
|
-
}
|
|
228
216
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
217
|
+
// Still too large - reduce dimensions progressively
|
|
218
|
+
for (const scale of scaleSteps) {
|
|
219
|
+
finalWidth = Math.round(targetWidth * scale);
|
|
220
|
+
finalHeight = Math.round(targetHeight * scale);
|
|
221
|
+
|
|
222
|
+
if (finalWidth < 100 || finalHeight < 100) {
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
for (const quality of qualitySteps) {
|
|
227
|
+
best = tryBothFormats(finalWidth, finalHeight, quality);
|
|
228
|
+
|
|
229
|
+
if (best.buffer.length <= opts.maxBytes) {
|
|
230
|
+
return {
|
|
231
|
+
data: Buffer.from(best.buffer).toString("base64"),
|
|
232
|
+
mimeType: best.mimeType,
|
|
233
|
+
originalWidth,
|
|
234
|
+
originalHeight,
|
|
235
|
+
width: finalWidth,
|
|
236
|
+
height: finalHeight,
|
|
237
|
+
wasResized: true,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Last resort: return smallest version we produced
|
|
244
|
+
return {
|
|
245
|
+
data: Buffer.from(best.buffer).toString("base64"),
|
|
246
|
+
mimeType: best.mimeType,
|
|
247
|
+
originalWidth,
|
|
248
|
+
originalHeight,
|
|
249
|
+
width: finalWidth,
|
|
250
|
+
height: finalHeight,
|
|
251
|
+
wasResized: true,
|
|
252
|
+
};
|
|
253
|
+
} catch {
|
|
254
|
+
// wasm-vips failed - try ImageMagick fallback
|
|
255
|
+
return resizeImageWithImageMagick(img, opts);
|
|
256
|
+
} finally {
|
|
257
|
+
image?.delete();
|
|
258
|
+
}
|
|
240
259
|
}
|
|
241
260
|
|
|
242
261
|
/**
|