@oh-my-pi/pi-coding-agent 4.8.3 → 4.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2 -0
- package/package.json +5 -5
- package/src/utils/image-convert.ts +11 -29
- package/src/utils/image-resize.ts +102 -121
- package/src/utils/vips.ts +23 -0
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.
|
|
3
|
+
"version": "4.9.0",
|
|
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-agent-core": "4.
|
|
43
|
-
"@oh-my-pi/pi-ai": "4.
|
|
44
|
-
"@oh-my-pi/pi-git-tool": "4.
|
|
45
|
-
"@oh-my-pi/pi-tui": "4.
|
|
42
|
+
"@oh-my-pi/pi-agent-core": "4.9.0",
|
|
43
|
+
"@oh-my-pi/pi-ai": "4.9.0",
|
|
44
|
+
"@oh-my-pi/pi-git-tool": "4.9.0",
|
|
45
|
+
"@oh-my-pi/pi-tui": "4.9.0",
|
|
46
46
|
"@openai/agents": "^0.3.7",
|
|
47
47
|
"@sinclair/typebox": "^0.34.46",
|
|
48
48
|
"ajv": "^8.17.1",
|
|
@@ -1,24 +1,6 @@
|
|
|
1
|
+
import { logger } from "../core/logger";
|
|
1
2
|
import { convertToPngWithImageMagick } from "./image-magick";
|
|
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
|
-
}
|
|
3
|
+
import { Vips } from "./vips";
|
|
22
4
|
|
|
23
5
|
/**
|
|
24
6
|
* Convert image to PNG format for terminal display.
|
|
@@ -34,23 +16,23 @@ export async function convertToPng(
|
|
|
34
16
|
return { data: base64Data, mimeType };
|
|
35
17
|
}
|
|
36
18
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
let image: ReturnType<typeof vips.Image.newFromBuffer> | undefined;
|
|
19
|
+
try {
|
|
20
|
+
const { Image } = await Vips();
|
|
21
|
+
const image = Image.newFromBuffer(Buffer.from(base64Data, "base64"));
|
|
41
22
|
try {
|
|
42
|
-
const buffer = Buffer.from(base64Data, "base64");
|
|
43
|
-
image = vips.Image.newFromBuffer(buffer);
|
|
44
23
|
const pngBuffer = image.writeToBuffer(".png");
|
|
45
24
|
return {
|
|
46
25
|
data: Buffer.from(pngBuffer).toString("base64"),
|
|
47
26
|
mimeType: "image/png",
|
|
48
27
|
};
|
|
49
|
-
} catch {
|
|
50
|
-
// wasm-vips failed, try ImageMagick fallback
|
|
51
28
|
} finally {
|
|
52
|
-
image
|
|
29
|
+
image.delete();
|
|
53
30
|
}
|
|
31
|
+
} catch (error) {
|
|
32
|
+
// wasm-vips failed, try ImageMagick fallback
|
|
33
|
+
logger.error("Failed to convert image to PNG with wasm-vips", {
|
|
34
|
+
error: error instanceof Error ? error.message : String(error),
|
|
35
|
+
});
|
|
54
36
|
}
|
|
55
37
|
|
|
56
38
|
// Fall back to ImageMagick
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
|
2
|
+
import { logger } from "../core/logger";
|
|
2
3
|
import { getImageDimensionsWithImageMagick, resizeWithImageMagick } from "./image-magick";
|
|
4
|
+
import { Vips } from "./vips";
|
|
3
5
|
|
|
4
6
|
export interface ImageResizeOptions {
|
|
5
7
|
maxWidth?: number; // Default: 2000
|
|
@@ -79,26 +81,6 @@ function pickSmaller(
|
|
|
79
81
|
return a.buffer.length <= b.buffer.length ? a : b;
|
|
80
82
|
}
|
|
81
83
|
|
|
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
|
-
|
|
102
84
|
/**
|
|
103
85
|
* Resize an image to fit within the specified max dimensions and file size.
|
|
104
86
|
* Returns the original image if it already fits within the limits.
|
|
@@ -115,91 +97,71 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption
|
|
|
115
97
|
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
116
98
|
const buffer = Buffer.from(img.data, "base64");
|
|
117
99
|
|
|
118
|
-
const vips = await getVips();
|
|
119
|
-
if (!vips) {
|
|
120
|
-
return resizeImageWithImageMagick(img, opts);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
let image: ReturnType<typeof vips.Image.newFromBuffer> | undefined;
|
|
124
100
|
try {
|
|
125
|
-
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
101
|
+
const { Image } = await Vips();
|
|
102
|
+
const image = Image.newFromBuffer(buffer);
|
|
103
|
+
try {
|
|
104
|
+
const originalWidth = image.width;
|
|
105
|
+
const originalHeight = image.height;
|
|
106
|
+
const format = img.mimeType?.split("/")[1] ?? "png";
|
|
107
|
+
|
|
108
|
+
// Check if already within all limits (dimensions AND size)
|
|
109
|
+
const originalSize = buffer.length;
|
|
110
|
+
if (originalWidth <= opts.maxWidth && originalHeight <= opts.maxHeight && originalSize <= opts.maxBytes) {
|
|
111
|
+
return {
|
|
112
|
+
data: img.data,
|
|
113
|
+
mimeType: img.mimeType ?? `image/${format}`,
|
|
114
|
+
originalWidth,
|
|
115
|
+
originalHeight,
|
|
116
|
+
width: originalWidth,
|
|
117
|
+
height: originalHeight,
|
|
118
|
+
wasResized: false,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
143
121
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
122
|
+
// Calculate initial dimensions respecting max limits
|
|
123
|
+
let targetWidth = originalWidth;
|
|
124
|
+
let targetHeight = originalHeight;
|
|
147
125
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
126
|
+
if (targetWidth > opts.maxWidth) {
|
|
127
|
+
targetHeight = Math.round((targetHeight * opts.maxWidth) / targetWidth);
|
|
128
|
+
targetWidth = opts.maxWidth;
|
|
129
|
+
}
|
|
130
|
+
if (targetHeight > opts.maxHeight) {
|
|
131
|
+
targetWidth = Math.round((targetWidth * opts.maxHeight) / targetHeight);
|
|
132
|
+
targetHeight = opts.maxHeight;
|
|
133
|
+
}
|
|
156
134
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
135
|
+
// Helper to resize and encode in both formats, returning the smaller one
|
|
136
|
+
function tryBothFormats(
|
|
137
|
+
width: number,
|
|
138
|
+
height: number,
|
|
139
|
+
jpegQuality: number,
|
|
140
|
+
): { buffer: Uint8Array; mimeType: string } {
|
|
141
|
+
const scale = Math.min(width / originalWidth, height / originalHeight);
|
|
142
|
+
const resized = image!.resize(scale);
|
|
165
143
|
|
|
166
|
-
|
|
167
|
-
|
|
144
|
+
const pngBuffer = resized.writeToBuffer(".png");
|
|
145
|
+
const jpegBuffer = resized.writeToBuffer(".jpg", { Q: jpegQuality });
|
|
168
146
|
|
|
169
|
-
|
|
147
|
+
resized.delete();
|
|
170
148
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
149
|
+
return pickSmaller(
|
|
150
|
+
{ buffer: pngBuffer, mimeType: "image/png" },
|
|
151
|
+
{ buffer: jpegBuffer, mimeType: "image/jpeg" },
|
|
152
|
+
);
|
|
153
|
+
}
|
|
176
154
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
155
|
+
// Try to produce an image under maxBytes
|
|
156
|
+
const qualitySteps = [85, 70, 55, 40];
|
|
157
|
+
const scaleSteps = [1.0, 0.75, 0.5, 0.35, 0.25];
|
|
180
158
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
159
|
+
let best: { buffer: Uint8Array; mimeType: string };
|
|
160
|
+
let finalWidth = targetWidth;
|
|
161
|
+
let finalHeight = targetHeight;
|
|
184
162
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
if (best.buffer.length <= opts.maxBytes) {
|
|
189
|
-
return {
|
|
190
|
-
data: Buffer.from(best.buffer).toString("base64"),
|
|
191
|
-
mimeType: best.mimeType,
|
|
192
|
-
originalWidth,
|
|
193
|
-
originalHeight,
|
|
194
|
-
width: finalWidth,
|
|
195
|
-
height: finalHeight,
|
|
196
|
-
wasResized: true,
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// Still too large - try JPEG with decreasing quality
|
|
201
|
-
for (const quality of qualitySteps) {
|
|
202
|
-
best = tryBothFormats(targetWidth, targetHeight, quality);
|
|
163
|
+
// First attempt: resize to target dimensions, try both formats
|
|
164
|
+
best = tryBothFormats(targetWidth, targetHeight, opts.jpegQuality);
|
|
203
165
|
|
|
204
166
|
if (best.buffer.length <= opts.maxBytes) {
|
|
205
167
|
return {
|
|
@@ -212,19 +174,10 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption
|
|
|
212
174
|
wasResized: true,
|
|
213
175
|
};
|
|
214
176
|
}
|
|
215
|
-
}
|
|
216
|
-
|
|
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
177
|
|
|
178
|
+
// Still too large - try JPEG with decreasing quality
|
|
226
179
|
for (const quality of qualitySteps) {
|
|
227
|
-
best = tryBothFormats(
|
|
180
|
+
best = tryBothFormats(targetWidth, targetHeight, quality);
|
|
228
181
|
|
|
229
182
|
if (best.buffer.length <= opts.maxBytes) {
|
|
230
183
|
return {
|
|
@@ -238,23 +191,51 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption
|
|
|
238
191
|
};
|
|
239
192
|
}
|
|
240
193
|
}
|
|
241
|
-
}
|
|
242
194
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
195
|
+
// Still too large - reduce dimensions progressively
|
|
196
|
+
for (const scale of scaleSteps) {
|
|
197
|
+
finalWidth = Math.round(targetWidth * scale);
|
|
198
|
+
finalHeight = Math.round(targetHeight * scale);
|
|
199
|
+
|
|
200
|
+
if (finalWidth < 100 || finalHeight < 100) {
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
for (const quality of qualitySteps) {
|
|
205
|
+
best = tryBothFormats(finalWidth, finalHeight, quality);
|
|
206
|
+
|
|
207
|
+
if (best.buffer.length <= opts.maxBytes) {
|
|
208
|
+
return {
|
|
209
|
+
data: Buffer.from(best.buffer).toString("base64"),
|
|
210
|
+
mimeType: best.mimeType,
|
|
211
|
+
originalWidth,
|
|
212
|
+
originalHeight,
|
|
213
|
+
width: finalWidth,
|
|
214
|
+
height: finalHeight,
|
|
215
|
+
wasResized: true,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Last resort: return smallest version we produced
|
|
222
|
+
return {
|
|
223
|
+
data: Buffer.from(best.buffer).toString("base64"),
|
|
224
|
+
mimeType: best.mimeType,
|
|
225
|
+
originalWidth,
|
|
226
|
+
originalHeight,
|
|
227
|
+
width: finalWidth,
|
|
228
|
+
height: finalHeight,
|
|
229
|
+
wasResized: true,
|
|
230
|
+
};
|
|
231
|
+
} finally {
|
|
232
|
+
image.delete();
|
|
233
|
+
}
|
|
234
|
+
} catch (error) {
|
|
235
|
+
logger.error("Failed to resize image with wasm-vips", {
|
|
236
|
+
error: error instanceof Error ? error.message : String(error),
|
|
237
|
+
});
|
|
255
238
|
return resizeImageWithImageMagick(img, opts);
|
|
256
|
-
} finally {
|
|
257
|
-
image?.delete();
|
|
258
239
|
}
|
|
259
240
|
}
|
|
260
241
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type realVips from "wasm-vips";
|
|
2
|
+
import { logger } from "../core/logger";
|
|
3
|
+
|
|
4
|
+
// Cached vips instance
|
|
5
|
+
let _vips: Promise<typeof realVips> | undefined;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Get the vips instance.
|
|
9
|
+
* @returns The vips instance.
|
|
10
|
+
*/
|
|
11
|
+
export function Vips(): Promise<typeof realVips> {
|
|
12
|
+
if (_vips) return _vips;
|
|
13
|
+
|
|
14
|
+
let instance: Promise<typeof realVips> | undefined;
|
|
15
|
+
try {
|
|
16
|
+
instance = import("wasm-vips").then((mod) => (mod.default ?? mod)());
|
|
17
|
+
} catch (error) {
|
|
18
|
+
logger.error("Failed to import wasm-vips", { error: error instanceof Error ? error.message : String(error) });
|
|
19
|
+
instance = Promise.reject(error);
|
|
20
|
+
}
|
|
21
|
+
_vips = instance;
|
|
22
|
+
return instance;
|
|
23
|
+
}
|