@aexol/spectral 0.9.211 → 0.9.215
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/dist/agent/agents.d.ts.map +1 -1
- package/dist/agent/agents.js +40 -0
- package/dist/backend/generated-images.d.ts +90 -0
- package/dist/backend/generated-images.d.ts.map +1 -0
- package/dist/backend/generated-images.js +214 -0
- package/dist/backend/machine-routing.d.ts +25 -0
- package/dist/backend/machine-routing.d.ts.map +1 -0
- package/dist/backend/machine-routing.js +35 -0
- package/dist/commands/serve.d.ts.map +1 -1
- package/dist/commands/serve.js +31 -6
- package/dist/extensions/browser/browser-service.d.ts +75 -1
- package/dist/extensions/browser/browser-service.d.ts.map +1 -1
- package/dist/extensions/browser/browser-service.js +600 -34
- package/dist/extensions/browser/index.d.ts +10 -4
- package/dist/extensions/browser/index.d.ts.map +1 -1
- package/dist/extensions/browser/index.js +44 -5
- package/dist/extensions/browser/stream/stream-tool.d.ts +10 -0
- package/dist/extensions/browser/stream/stream-tool.d.ts.map +1 -1
- package/dist/extensions/browser/stream/stream-tool.js +42 -14
- package/dist/extensions/browser/tools/core.d.ts.map +1 -1
- package/dist/extensions/browser/tools/core.js +63 -2
- package/dist/extensions/desktop-control/index.d.ts +55 -0
- package/dist/extensions/desktop-control/index.d.ts.map +1 -0
- package/dist/extensions/desktop-control/index.js +633 -0
- package/dist/extensions/desktop-control/keys.d.ts +33 -0
- package/dist/extensions/desktop-control/keys.d.ts.map +1 -0
- package/dist/extensions/desktop-control/keys.js +170 -0
- package/dist/extensions/desktop-control/linux.d.ts +26 -0
- package/dist/extensions/desktop-control/linux.d.ts.map +1 -0
- package/dist/extensions/desktop-control/linux.js +195 -0
- package/dist/extensions/desktop-control/macos.d.ts +53 -0
- package/dist/extensions/desktop-control/macos.d.ts.map +1 -0
- package/dist/extensions/desktop-control/macos.js +382 -0
- package/dist/extensions/desktop-control/safety.d.ts +89 -0
- package/dist/extensions/desktop-control/safety.d.ts.map +1 -0
- package/dist/extensions/desktop-control/safety.js +157 -0
- package/dist/extensions/desktop-control/types.d.ts +79 -0
- package/dist/extensions/desktop-control/types.d.ts.map +1 -0
- package/dist/extensions/desktop-control/types.js +10 -0
- package/dist/extensions/desktop-control/windows.d.ts +31 -0
- package/dist/extensions/desktop-control/windows.d.ts.map +1 -0
- package/dist/extensions/desktop-control/windows.js +276 -0
- package/dist/extensions/desktop-screenshot/index.d.ts +103 -0
- package/dist/extensions/desktop-screenshot/index.d.ts.map +1 -1
- package/dist/extensions/desktop-screenshot/index.js +363 -17
- package/dist/extensions/image-generation/index.d.ts.map +1 -1
- package/dist/extensions/image-generation/index.js +133 -34
- package/dist/extensions/seo/index.d.ts.map +1 -1
- package/dist/extensions/seo/index.js +4 -0
- package/dist/extensions/seo/tools/backend-bridge.d.ts +4 -0
- package/dist/extensions/seo/tools/backend-bridge.d.ts.map +1 -0
- package/dist/extensions/seo/tools/backend-bridge.js +378 -0
- package/dist/extensions/seo/tools/full-audit.d.ts +2 -0
- package/dist/extensions/seo/tools/full-audit.d.ts.map +1 -1
- package/dist/extensions/seo/tools/full-audit.js +1 -1
- package/dist/extensions/seo/tools/report.d.ts +10 -0
- package/dist/extensions/seo/tools/report.d.ts.map +1 -1
- package/dist/extensions/seo/tools/report.js +1 -1
- package/dist/extensions/spectral-vision-fallback.d.ts.map +1 -1
- package/dist/extensions/spectral-vision-fallback.js +11 -1
- package/dist/generated/zeus/const.d.ts.map +1 -1
- package/dist/generated/zeus/const.js +148 -4
- package/dist/generated/zeus/index.d.ts +649 -0
- package/dist/generated/zeus/index.d.ts.map +1 -1
- package/dist/mcp/tool-registrar.d.ts.map +1 -1
- package/dist/mcp/tool-registrar.js +5 -2
- package/dist/mcp/types.d.ts +5 -0
- package/dist/mcp/types.d.ts.map +1 -1
- package/dist/relay/client.d.ts +11 -0
- package/dist/relay/client.d.ts.map +1 -1
- package/dist/relay/client.js +24 -6
- package/dist/relay/dispatcher.d.ts +12 -1
- package/dist/relay/dispatcher.d.ts.map +1 -1
- package/dist/relay/dispatcher.js +44 -42
- package/dist/sdk/ai/providers/faux.js +1 -1
- package/dist/sdk/ai/providers/openai-completions.d.ts.map +1 -1
- package/dist/sdk/ai/providers/openai-completions.js +57 -16
- package/dist/sdk/ai/providers/openrouter-images.d.ts.map +1 -1
- package/dist/sdk/ai/providers/openrouter-images.js +45 -9
- package/dist/sdk/ai/providers/simple-options.d.ts.map +1 -1
- package/dist/sdk/ai/providers/simple-options.js +2 -0
- package/dist/sdk/ai/types.d.ts +37 -1
- package/dist/sdk/ai/types.d.ts.map +1 -1
- package/dist/sdk/ai/utils/image-source.d.ts +121 -0
- package/dist/sdk/ai/utils/image-source.d.ts.map +1 -0
- package/dist/sdk/ai/utils/image-source.js +410 -0
- package/dist/sdk/coding-agent/core/extensions/native-extensions.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/extensions/native-extensions.js +14 -0
- package/dist/sdk/coding-agent/core/extensions/types.d.ts +9 -0
- package/dist/sdk/coding-agent/core/extensions/types.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/system-prompt.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/system-prompt.js +15 -9
- package/dist/sdk/coding-agent/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/sdk/coding-agent/modes/rpc/rpc-mode.js +16 -3
- package/dist/sdk/coding-agent/utils/image-resize.d.ts.map +1 -1
- package/dist/sdk/coding-agent/utils/image-resize.js +7 -3
- package/dist/server/agent-bridge.d.ts +14 -0
- package/dist/server/agent-bridge.d.ts.map +1 -1
- package/dist/server/agent-bridge.js +105 -26
- package/dist/server/handlers/queue.d.ts.map +1 -1
- package/dist/server/handlers/queue.js +11 -34
- package/dist/server/image-attachments.d.ts +109 -0
- package/dist/server/image-attachments.d.ts.map +1 -0
- package/dist/server/image-attachments.js +195 -0
- package/dist/server/session-stream.d.ts +69 -0
- package/dist/server/session-stream.d.ts.map +1 -1
- package/dist/server/session-stream.js +388 -32
- package/dist/server/storage.d.ts.map +1 -1
- package/dist/server/storage.js +10 -31
- package/dist/server/ui/terminal-confirm.d.ts +145 -0
- package/dist/server/ui/terminal-confirm.d.ts.map +1 -0
- package/dist/server/ui/terminal-confirm.js +333 -0
- package/dist/server/wire.d.ts +30 -5
- package/dist/server/wire.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -18,13 +18,17 @@ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
18
18
|
import { tmpdir } from "node:os";
|
|
19
19
|
import { join } from "node:path";
|
|
20
20
|
import { randomUUID } from "node:crypto";
|
|
21
|
+
import { uploadImage } from "../../backend/generated-images.js";
|
|
22
|
+
import { resolveBackendRouting } from "../../backend/machine-routing.js";
|
|
23
|
+
import { resizeImage } from "../../sdk/coding-agent/utils/image-resize.js";
|
|
24
|
+
import { formatImageBytes, MAX_INLINE_IMAGE_BASE64_CHARS, } from "../../server/image-attachments.js";
|
|
21
25
|
const SCREENSHOTS_DIR = ".screenshots";
|
|
22
|
-
function tmpDir() {
|
|
26
|
+
export function tmpDir() {
|
|
23
27
|
const d = join(tmpdir(), "spectral-screenshots");
|
|
24
28
|
mkdirSync(d, { recursive: true });
|
|
25
29
|
return d;
|
|
26
30
|
}
|
|
27
|
-
function saveScreenshot(buf, format, cwd) {
|
|
31
|
+
export function saveScreenshot(buf, format, cwd) {
|
|
28
32
|
const dir = join(cwd, SCREENSHOTS_DIR);
|
|
29
33
|
mkdirSync(dir, { recursive: true });
|
|
30
34
|
const now = new Date();
|
|
@@ -176,6 +180,221 @@ function darwinWindowScreenshot(windowId, options) {
|
|
|
176
180
|
});
|
|
177
181
|
});
|
|
178
182
|
}
|
|
183
|
+
// ===========================================================================
|
|
184
|
+
// macOS coordinate-space normalization (Retina pixels → logical points)
|
|
185
|
+
// ===========================================================================
|
|
186
|
+
/**
|
|
187
|
+
* `screencapture` returns images in *physical* pixels — 2× (or 3×) the
|
|
188
|
+
* logical resolution on Retina displays — while the `desktop_*` tools
|
|
189
|
+
* (CGEvent) operate in *logical* screen points. A model reading coordinates
|
|
190
|
+
* straight off an unnormalized capture would click ~2× too far. The helpers
|
|
191
|
+
* below probe the display geometry (JXA/AppKit, logical points), resample
|
|
192
|
+
* the capture to a 1:1 logical-points image with `sips`, and produce
|
|
193
|
+
* metadata that states the applied scale explicitly.
|
|
194
|
+
*/
|
|
195
|
+
const COORD_SPACE_NOTE = "Coordinates for desktop_* tools are logical screen points — the image above is already scaled 1:1 to that space.";
|
|
196
|
+
const DARWIN_GEOMETRY_TTL_MS = 10_000;
|
|
197
|
+
let darwinGeometryCache = null;
|
|
198
|
+
/** Test hook: forget the cached geometry so the next probe hits osascript again. */
|
|
199
|
+
export function resetDarwinScreenGeometryCache() {
|
|
200
|
+
darwinGeometryCache = null;
|
|
201
|
+
}
|
|
202
|
+
function runCommand(cmd, args, timeoutMs) {
|
|
203
|
+
return new Promise((resolve, reject) => {
|
|
204
|
+
execFile(cmd, args, { timeout: timeoutMs }, (err, stdout) => {
|
|
205
|
+
if (err)
|
|
206
|
+
return reject(err);
|
|
207
|
+
resolve(stdout);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
// NSScreen reports logical points. AppKit's global space is bottom-left
|
|
212
|
+
// origin while CGEvent's is top-left origin of the primary display, so y is
|
|
213
|
+
// flipped before the geometry is reported (the desktop_* tools are CGEvent).
|
|
214
|
+
const DARWIN_GEOMETRY_JXA = [
|
|
215
|
+
"ObjC.import('AppKit');",
|
|
216
|
+
"const screens = $.NSScreen.screens;",
|
|
217
|
+
"const primaryF = screens.objectAtIndex(0).frame;",
|
|
218
|
+
"const mainF = $.NSScreen.mainScreen.frame;",
|
|
219
|
+
"JSON.stringify({",
|
|
220
|
+
" count: Number(screens.count),",
|
|
221
|
+
" primary: { x: Number(primaryF.origin.x), y: Number(primaryF.origin.y), w: Number(primaryF.size.width), h: Number(primaryF.size.height) },",
|
|
222
|
+
" main: { x: Number(mainF.origin.x), y: Number(mainF.origin.y), w: Number(mainF.size.width), h: Number(mainF.size.height) },",
|
|
223
|
+
" backingScale: Number($.NSScreen.mainScreen.backingScaleFactor)",
|
|
224
|
+
"});",
|
|
225
|
+
].join("\n");
|
|
226
|
+
/**
|
|
227
|
+
* Logical display geometry via JXA/AppKit, cached for a few seconds (display
|
|
228
|
+
* arrangements rarely change mid-session). Resolves to `null` when the probe
|
|
229
|
+
* fails — captures are then left untouched and the tool result says the
|
|
230
|
+
* scale is unverified. Exported for tests.
|
|
231
|
+
*/
|
|
232
|
+
export async function darwinScreenGeometry() {
|
|
233
|
+
const now = Date.now();
|
|
234
|
+
if (darwinGeometryCache && now - darwinGeometryCache.at < DARWIN_GEOMETRY_TTL_MS) {
|
|
235
|
+
return darwinGeometryCache.geometry;
|
|
236
|
+
}
|
|
237
|
+
const geometry = await (async () => {
|
|
238
|
+
try {
|
|
239
|
+
const out = await runCommand("osascript", ["-l", "JavaScript", "-e", DARWIN_GEOMETRY_JXA], 5000);
|
|
240
|
+
const raw = JSON.parse(out.trim());
|
|
241
|
+
if (!raw || raw.count < 1 || !raw.primary || !raw.main || !Number.isFinite(raw.backingScale))
|
|
242
|
+
return null;
|
|
243
|
+
const toCgY = (r) => raw.primary.h - (r.y + r.h);
|
|
244
|
+
return {
|
|
245
|
+
displayCount: raw.count,
|
|
246
|
+
primaryOrigin: { x: raw.primary.x, y: toCgY(raw.primary) },
|
|
247
|
+
main: { x: raw.main.x, y: toCgY(raw.main), width: raw.main.w, height: raw.main.h },
|
|
248
|
+
backingScaleFactor: raw.backingScale,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
})();
|
|
255
|
+
darwinGeometryCache = { at: now, geometry };
|
|
256
|
+
return geometry;
|
|
257
|
+
}
|
|
258
|
+
async function probeDarwinImageSize(path) {
|
|
259
|
+
try {
|
|
260
|
+
const out = await runCommand("sips", ["-g", "pixelWidth", "-g", "pixelHeight", path], 5000);
|
|
261
|
+
const w = /pixelWidth:\s*(\d+)/.exec(out);
|
|
262
|
+
const h = /pixelHeight:\s*(\d+)/.exec(out);
|
|
263
|
+
if (!w || !h)
|
|
264
|
+
return null;
|
|
265
|
+
return { width: Number(w[1]), height: Number(h[1]) };
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
function nearSize(a, b) {
|
|
272
|
+
return Math.abs(a - b) <= Math.max(2, b * 0.02);
|
|
273
|
+
}
|
|
274
|
+
function fmtScale(scale) {
|
|
275
|
+
return `${Math.round(scale * 100) / 100}×`;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Normalize a macOS capture so image pixels map 1:1 onto the logical screen
|
|
279
|
+
* points the desktop_* tools accept. Full-screen captures are resampled to
|
|
280
|
+
* the main screen's logical width when the file is a Retina (backing-scale)
|
|
281
|
+
* capture; window captures are resampled by the display's backing scale
|
|
282
|
+
* factor (their coordinates are window-local, which the metadata states).
|
|
283
|
+
* Never throws: on any probe failure the original buffer comes back with a
|
|
284
|
+
* caution note instead of a silently wrong scale. Exported for tests.
|
|
285
|
+
*/
|
|
286
|
+
export async function normalizeDarwinScreenshot(buf, format, mode) {
|
|
287
|
+
const unchanged = (metadata, details = {}) => ({ buffer: buf, metadata, details });
|
|
288
|
+
try {
|
|
289
|
+
const geometry = await darwinScreenGeometry();
|
|
290
|
+
const inPath = join(tmpDir(), `${randomUUID()}.${format}`);
|
|
291
|
+
writeFileSync(inPath, buf);
|
|
292
|
+
const size = await probeDarwinImageSize(inPath);
|
|
293
|
+
if (!size) {
|
|
294
|
+
const factor = geometry && geometry.backingScaleFactor > 1 ? Math.round(geometry.backingScaleFactor) : 2;
|
|
295
|
+
return unchanged(geometry
|
|
296
|
+
? `Note: image size could not be verified (main screen: ${geometry.main.width}×${geometry.main.height} pt at ` +
|
|
297
|
+
`${fmtScale(geometry.backingScaleFactor)} backing scale) — divide read-off coordinates by ${factor} on a ` +
|
|
298
|
+
"Retina Mac before using desktop_* tools."
|
|
299
|
+
: "Note: image size and screen geometry could not be verified — divide read-off coordinates by 2 on a " +
|
|
300
|
+
"Retina Mac before using desktop_* tools.");
|
|
301
|
+
}
|
|
302
|
+
let targetWidth = null;
|
|
303
|
+
let scaleApplied;
|
|
304
|
+
if (mode === "screen" && geometry) {
|
|
305
|
+
const logicalW = geometry.main.width;
|
|
306
|
+
if (nearSize(size.width, logicalW * geometry.backingScaleFactor) && logicalW < size.width) {
|
|
307
|
+
targetWidth = Math.round(logicalW);
|
|
308
|
+
scaleApplied = size.width / logicalW;
|
|
309
|
+
}
|
|
310
|
+
else if (nearSize(size.width, logicalW)) {
|
|
311
|
+
scaleApplied = 1;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
else if (mode === "window" && geometry && geometry.backingScaleFactor > 1.01) {
|
|
315
|
+
targetWidth = Math.round(size.width / geometry.backingScaleFactor);
|
|
316
|
+
scaleApplied = geometry.backingScaleFactor;
|
|
317
|
+
}
|
|
318
|
+
let outBuf = buf;
|
|
319
|
+
let outWidth = size.width;
|
|
320
|
+
let outHeight = size.height;
|
|
321
|
+
let resampled = false;
|
|
322
|
+
if (targetWidth !== null && targetWidth > 0 && targetWidth < size.width) {
|
|
323
|
+
const outPath = join(tmpDir(), `${randomUUID()}.${format}`);
|
|
324
|
+
await runCommand("sips", ["--resampleWidth", String(targetWidth), inPath, "--out", outPath], 15000);
|
|
325
|
+
outBuf = readFileSync(outPath);
|
|
326
|
+
outWidth = targetWidth;
|
|
327
|
+
outHeight = Math.round(size.height * (targetWidth / size.width));
|
|
328
|
+
resampled = true;
|
|
329
|
+
}
|
|
330
|
+
const displayNote = geometry
|
|
331
|
+
? `${geometry.displayCount} display(s); primary display global origin (${geometry.primaryOrigin.x}, ${geometry.primaryOrigin.y}) pt in the global CGEvent space` +
|
|
332
|
+
(geometry.main.x !== 0 || geometry.main.y !== 0
|
|
333
|
+
? `; main screen top-left at (${geometry.main.x}, ${geometry.main.y}) pt`
|
|
334
|
+
: "")
|
|
335
|
+
: "";
|
|
336
|
+
const windowLocalNote = "Coordinates for desktop_* tools are global logical screen points — these are window-local, " +
|
|
337
|
+
"so offset them by the window's position (or capture the full desktop) before clicking.";
|
|
338
|
+
let metadata;
|
|
339
|
+
if (mode === "screen") {
|
|
340
|
+
if (resampled && geometry) {
|
|
341
|
+
metadata =
|
|
342
|
+
`Image ${size.width}×${size.height} px is a ${fmtScale(scaleApplied ?? 2)} Retina capture → resampled to ` +
|
|
343
|
+
`${outWidth}×${outHeight} px for the ${geometry.main.width}×${geometry.main.height} pt logical display. ` +
|
|
344
|
+
`${COORD_SPACE_NOTE} ${displayNote}.`;
|
|
345
|
+
}
|
|
346
|
+
else if (scaleApplied === 1 && geometry) {
|
|
347
|
+
metadata =
|
|
348
|
+
`Image ${outWidth}×${outHeight} px on the ${geometry.main.width}×${geometry.main.height} pt logical display ` +
|
|
349
|
+
`(scale 1× — no resampling needed). ${COORD_SPACE_NOTE} ${displayNote}.`;
|
|
350
|
+
}
|
|
351
|
+
else if (geometry) {
|
|
352
|
+
metadata =
|
|
353
|
+
`Image ${outWidth}×${outHeight} px does not match the main screen's geometry ` +
|
|
354
|
+
`(${geometry.main.width}×${geometry.main.height} pt at ${fmtScale(geometry.backingScaleFactor)}; a secondary display?) — ` +
|
|
355
|
+
"desktop_* tools take logical screen points, so verify the scale before converting read-off coordinates.";
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
metadata =
|
|
359
|
+
`Image ${outWidth}×${outHeight} px; screen geometry could not be probed — desktop_* tools take logical ` +
|
|
360
|
+
"screen points, so on a Retina Mac (2×) divide read-off coordinates by 2 first.";
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
metadata = resampled && geometry
|
|
365
|
+
? `Window image ${size.width}×${size.height} px at ${fmtScale(scaleApplied ?? geometry.backingScaleFactor)} backing scale → resampled to ` +
|
|
366
|
+
`${outWidth}×${outHeight} px (window-local logical points, assuming the window sits on the main display). ` +
|
|
367
|
+
windowLocalNote
|
|
368
|
+
: geometry
|
|
369
|
+
? `Window image ${outWidth}×${outHeight} px at ${fmtScale(geometry.backingScaleFactor)} backing scale ` +
|
|
370
|
+
`(window-local logical points, no resampling needed). ${windowLocalNote}`
|
|
371
|
+
: `Window image ${outWidth}×${outHeight} px; screen geometry could not be probed — on a Retina Mac (2×) ` +
|
|
372
|
+
"window captures are 2× physical pixels, so divide read-off coordinates by 2 before clicking.";
|
|
373
|
+
}
|
|
374
|
+
return {
|
|
375
|
+
buffer: outBuf,
|
|
376
|
+
metadata,
|
|
377
|
+
details: {
|
|
378
|
+
imageWidthPx: outWidth,
|
|
379
|
+
imageHeightPx: outHeight,
|
|
380
|
+
...(geometry
|
|
381
|
+
? {
|
|
382
|
+
logicalWidthPt: geometry.main.width,
|
|
383
|
+
logicalHeightPt: geometry.main.height,
|
|
384
|
+
displayCount: geometry.displayCount,
|
|
385
|
+
primaryOriginPt: geometry.primaryOrigin,
|
|
386
|
+
}
|
|
387
|
+
: {}),
|
|
388
|
+
...(scaleApplied !== undefined ? { scaleApplied } : {}),
|
|
389
|
+
resampled,
|
|
390
|
+
},
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
catch {
|
|
394
|
+
return unchanged("Note: coordinate-space normalization failed — desktop_* tools take logical screen points; " +
|
|
395
|
+
"on a Retina Mac (2×) divide read-off coordinates by 2 before clicking.");
|
|
396
|
+
}
|
|
397
|
+
}
|
|
179
398
|
function parseLinuxDisplaysOutput(output) {
|
|
180
399
|
return output
|
|
181
400
|
.split("\n")
|
|
@@ -272,9 +491,9 @@ function linuxWindowScreenshot(windowId, options) {
|
|
|
272
491
|
* System.Windows.Forms / CopyFromScreen is initialized even in sessions that
|
|
273
492
|
* default to MTA (avoids COM failures).
|
|
274
493
|
*/
|
|
275
|
-
function runPowerShell(psPath) {
|
|
494
|
+
export function runPowerShell(psPath, opts) {
|
|
276
495
|
return new Promise((resolve, reject) => {
|
|
277
|
-
const child = crossSpawn("powershell.exe", ["-NoProfile", "-Sta", "-ExecutionPolicy", "Bypass", "-File", psPath], { timeout: 15000, windowsHide: true });
|
|
496
|
+
const child = crossSpawn("powershell.exe", ["-NoProfile", "-Sta", "-ExecutionPolicy", "Bypass", "-File", psPath], { timeout: opts?.timeoutMs ?? 15000, windowsHide: true });
|
|
278
497
|
let stdout = "";
|
|
279
498
|
let stderr = "";
|
|
280
499
|
child.stdout?.on("data", (chunk) => { stdout += chunk.toString(); });
|
|
@@ -412,7 +631,7 @@ function win32WindowScreenshot(hwnd, options) {
|
|
|
412
631
|
// ===========================================================================
|
|
413
632
|
// Unified API
|
|
414
633
|
// ===========================================================================
|
|
415
|
-
function screenshot(options = {}) {
|
|
634
|
+
export function screenshot(options = {}) {
|
|
416
635
|
switch (process.platform) {
|
|
417
636
|
case "darwin": return darwinFullScreenshot(options);
|
|
418
637
|
case "linux": return linuxFullScreenshot(options);
|
|
@@ -420,7 +639,7 @@ function screenshot(options = {}) {
|
|
|
420
639
|
default: return Promise.reject(new Error(`Unsupported platform: ${process.platform}`));
|
|
421
640
|
}
|
|
422
641
|
}
|
|
423
|
-
function screenshotWindow(windowId, options, platform) {
|
|
642
|
+
export function screenshotWindow(windowId, options, platform) {
|
|
424
643
|
switch (platform) {
|
|
425
644
|
case "darwin": return darwinWindowScreenshot(windowId, options);
|
|
426
645
|
case "linux": return linuxWindowScreenshot(windowId, options);
|
|
@@ -428,7 +647,7 @@ function screenshotWindow(windowId, options, platform) {
|
|
|
428
647
|
default: return Promise.reject(new Error(`Window screenshot not supported on ${platform}`));
|
|
429
648
|
}
|
|
430
649
|
}
|
|
431
|
-
function listWindows() {
|
|
650
|
+
export function listWindows() {
|
|
432
651
|
switch (process.platform) {
|
|
433
652
|
case "darwin": return listDarwinWindows();
|
|
434
653
|
case "linux": return listLinuxWindows();
|
|
@@ -439,7 +658,7 @@ function listWindows() {
|
|
|
439
658
|
// ===========================================================================
|
|
440
659
|
// Helpers
|
|
441
660
|
// ===========================================================================
|
|
442
|
-
function formatWindowList(windows, maxRows = 40) {
|
|
661
|
+
export function formatWindowList(windows, maxRows = 40) {
|
|
443
662
|
if (windows.length === 0)
|
|
444
663
|
return "No visible windows found.";
|
|
445
664
|
const rows = windows.slice(0, maxRows);
|
|
@@ -454,6 +673,41 @@ function formatWindowList(windows, maxRows = 40) {
|
|
|
454
673
|
return `${header}\n${body}`;
|
|
455
674
|
}
|
|
456
675
|
// ===========================================================================
|
|
676
|
+
// Inline fallback sizing
|
|
677
|
+
// ===========================================================================
|
|
678
|
+
/**
|
|
679
|
+
* Last-resort inline carrier for a screenshot that could not be hosted.
|
|
680
|
+
*
|
|
681
|
+
* Returns the image unchanged when its base64 payload already fits
|
|
682
|
+
* `MAX_INLINE_IMAGE_BASE64_CHARS`, a downscaled/re-encoded copy when resizing
|
|
683
|
+
* gets it under the cap, and `null` when even that fails (no image resizer
|
|
684
|
+
* available, or the capture is absurdly large). Callers MUST treat `null` as
|
|
685
|
+
* "do not attach base64" and report the failure — never as "attach it anyway".
|
|
686
|
+
*
|
|
687
|
+
* Exported for tests.
|
|
688
|
+
*/
|
|
689
|
+
export async function fitInlineScreenshot(image, maxBase64Chars = MAX_INLINE_IMAGE_BASE64_CHARS) {
|
|
690
|
+
const data = image.data;
|
|
691
|
+
if (!data)
|
|
692
|
+
return null;
|
|
693
|
+
if (data.length <= maxBase64Chars)
|
|
694
|
+
return image;
|
|
695
|
+
let resized = null;
|
|
696
|
+
try {
|
|
697
|
+
resized = await resizeImage(image, { maxBytes: maxBase64Chars });
|
|
698
|
+
}
|
|
699
|
+
catch {
|
|
700
|
+
resized = null;
|
|
701
|
+
}
|
|
702
|
+
if (!resized || resized.data.length > maxBase64Chars)
|
|
703
|
+
return null;
|
|
704
|
+
return {
|
|
705
|
+
type: "image",
|
|
706
|
+
data: resized.data,
|
|
707
|
+
mimeType: resized.mimeType || image.mimeType,
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
// ===========================================================================
|
|
457
711
|
// Extension registration
|
|
458
712
|
// ===========================================================================
|
|
459
713
|
export default async function desktopScreenshotExtension(ext) {
|
|
@@ -477,15 +731,101 @@ export default async function desktopScreenshotExtension(ext) {
|
|
|
477
731
|
const p = params;
|
|
478
732
|
const format = p.format ?? "png";
|
|
479
733
|
const mimeType = (format === "jpg" || format === "jpeg") ? "image/jpeg" : "image/png";
|
|
480
|
-
|
|
481
|
-
|
|
734
|
+
/**
|
|
735
|
+
* A screenshot is worth megabytes of base64. When the CLI can reach
|
|
736
|
+
* the backend we upload it and put only the hosted url into the
|
|
737
|
+
* context; otherwise we fall back to inline base64, downscaled to
|
|
738
|
+
* the relay cap (best effort — a storage outage must never fail
|
|
739
|
+
* the screenshot, but it must also never dump megabytes of base64
|
|
740
|
+
* into the session history).
|
|
741
|
+
*/
|
|
742
|
+
const makeImageResult = async (buf, label, coordMeta) => {
|
|
482
743
|
const savedPath = saveScreenshot(buf, format, ctx.cwd);
|
|
744
|
+
const routing = await resolveBackendRouting();
|
|
745
|
+
let imageBlock = {
|
|
746
|
+
type: "image",
|
|
747
|
+
data: buf.toString("base64"),
|
|
748
|
+
mimeType,
|
|
749
|
+
};
|
|
750
|
+
let hostedUrl;
|
|
751
|
+
if (routing) {
|
|
752
|
+
try {
|
|
753
|
+
const upload = await uploadImage({
|
|
754
|
+
backendUrl: routing.backendUrl,
|
|
755
|
+
token: routing.machineJwt,
|
|
756
|
+
// Copy: `buf` may be a pooled Buffer view.
|
|
757
|
+
bytes: new Uint8Array(buf),
|
|
758
|
+
contentType: mimeType,
|
|
759
|
+
kind: "attachment",
|
|
760
|
+
signal: _signal,
|
|
761
|
+
});
|
|
762
|
+
imageBlock = {
|
|
763
|
+
type: "image",
|
|
764
|
+
url: upload.url,
|
|
765
|
+
imageId: upload.imageId,
|
|
766
|
+
mimeType,
|
|
767
|
+
};
|
|
768
|
+
hostedUrl = upload.url;
|
|
769
|
+
}
|
|
770
|
+
catch (error) {
|
|
771
|
+
process.stderr.write(`[desktop_screenshot] could not host the screenshot (${error instanceof Error ? error.message : String(error)}); ` +
|
|
772
|
+
"falling back to a downscaled inline image.\n");
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
// Inline fallback (no backend routing, or the upload failed):
|
|
776
|
+
// a full-screen retina PNG is worth several megabytes of
|
|
777
|
+
// base64, which would be dumped into the tool result AND
|
|
778
|
+
// persisted in the session JSONL. Downscale it to fit the
|
|
779
|
+
// relay cap, or refuse it with a clear message instead of
|
|
780
|
+
// silently shipping megabytes of base64.
|
|
781
|
+
if (!hostedUrl) {
|
|
782
|
+
const fitted = await fitInlineScreenshot(imageBlock);
|
|
783
|
+
if (!fitted) {
|
|
784
|
+
const base64Chars = imageBlock.data?.length ?? 0;
|
|
785
|
+
return {
|
|
786
|
+
content: [
|
|
787
|
+
{
|
|
788
|
+
type: "text",
|
|
789
|
+
text: `❌ ${label} was saved to ${savedPath} but NOT attached: the image is too large to send ` +
|
|
790
|
+
`inline (${formatImageBytes(base64Chars)} of base64, limit ` +
|
|
791
|
+
`${formatImageBytes(MAX_INLINE_IMAGE_BASE64_CHARS)}) and backend hosting ` +
|
|
792
|
+
`${routing ? "failed" : "is not configured"}. ` +
|
|
793
|
+
"Retry with format=jpg, a smaller window, or once image hosting is available.",
|
|
794
|
+
},
|
|
795
|
+
],
|
|
796
|
+
details: {
|
|
797
|
+
format,
|
|
798
|
+
sizeBytes: buf.length,
|
|
799
|
+
platform: process.platform,
|
|
800
|
+
savedPath,
|
|
801
|
+
hosted: false,
|
|
802
|
+
inlineTooLarge: true,
|
|
803
|
+
isError: true,
|
|
804
|
+
},
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
imageBlock = fitted;
|
|
808
|
+
}
|
|
483
809
|
return {
|
|
484
810
|
content: [
|
|
485
|
-
{
|
|
486
|
-
|
|
811
|
+
{
|
|
812
|
+
type: "text",
|
|
813
|
+
text: `✓ ${label} (${format.toUpperCase()}, ${(buf.length / 1024).toFixed(1)} KB).\n` +
|
|
814
|
+
`Saved: ${savedPath}` +
|
|
815
|
+
(hostedUrl ? `\nHosted: ${hostedUrl}` : "") +
|
|
816
|
+
(coordMeta?.metadata ? `\n\n${coordMeta.metadata}` : "") +
|
|
817
|
+
"\nThe image is attached and will be analyzed by the vision model.",
|
|
818
|
+
},
|
|
819
|
+
imageBlock,
|
|
487
820
|
],
|
|
488
|
-
details: {
|
|
821
|
+
details: {
|
|
822
|
+
format,
|
|
823
|
+
sizeBytes: buf.length,
|
|
824
|
+
platform: process.platform,
|
|
825
|
+
savedPath,
|
|
826
|
+
...(hostedUrl ? { url: hostedUrl, hosted: true } : { hosted: false }),
|
|
827
|
+
...(coordMeta?.details ?? {}),
|
|
828
|
+
},
|
|
489
829
|
};
|
|
490
830
|
};
|
|
491
831
|
const makeError = (msg) => ({
|
|
@@ -509,11 +849,17 @@ export default async function desktopScreenshotExtension(ext) {
|
|
|
509
849
|
details: { isError: true, searched: p.windowTitle },
|
|
510
850
|
};
|
|
511
851
|
}
|
|
512
|
-
const
|
|
513
|
-
|
|
852
|
+
const raw = await screenshotWindow(match.id, { format }, process.platform);
|
|
853
|
+
const coordMeta = process.platform === "darwin"
|
|
854
|
+
? await normalizeDarwinScreenshot(raw, format, "window")
|
|
855
|
+
: undefined;
|
|
856
|
+
return await makeImageResult(coordMeta?.buffer ?? raw, `Window "${match.title}" (${match.owner})`, coordMeta);
|
|
514
857
|
}
|
|
515
|
-
const
|
|
516
|
-
|
|
858
|
+
const raw = await screenshot({ format });
|
|
859
|
+
const coordMeta = process.platform === "darwin"
|
|
860
|
+
? await normalizeDarwinScreenshot(raw, format, "screen")
|
|
861
|
+
: undefined;
|
|
862
|
+
return await makeImageResult(coordMeta?.buffer ?? raw, "Desktop screenshot", coordMeta);
|
|
517
863
|
}
|
|
518
864
|
catch (err) {
|
|
519
865
|
return makeError(err instanceof Error ? err.message : String(err));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/extensions/image-generation/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAmB,YAAY,EAAkB,MAAM,iCAAiC,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/extensions/image-generation/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAmB,YAAY,EAAkB,MAAM,iCAAiC,CAAC;AA0QrG,wBAA8B,wBAAwB,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAiGvF"}
|