diff --git a/src/tools/FileReadTool/imageProcessor.ts b/src/tools/FileReadTool/imageProcessor.ts index 20afa900..e2f62255 100644 --- a/src/tools/FileReadTool/imageProcessor.ts +++ b/src/tools/FileReadTool/imageProcessor.ts @@ -34,6 +34,17 @@ type SharpCreator = (options: SharpCreatorOptions) => SharpInstance let imageProcessorModule: { default: SharpFunction } | null = null let imageCreatorModule: { default: SharpCreator } | null = null +/** + * Error thrown when no image processor is available (e.g., in the open build + * where sharp and image-processor-napi are stubbed out). + */ +export class ImageProcessorUnavailableError extends Error { + constructor() { + super('No image processor available (sharp is not installed)') + this.name = 'ImageProcessorUnavailableError' + } +} + export async function getImageProcessor(): Promise { if (imageProcessorModule) { return imageProcessorModule.default @@ -44,10 +55,14 @@ export async function getImageProcessor(): Promise { try { // Use the native image processor module const imageProcessor = await import('image-processor-napi') + if ((imageProcessor as { __stub?: boolean }).__stub) { + throw new ImageProcessorUnavailableError() + } const sharp = imageProcessor.sharp || imageProcessor.default imageProcessorModule = { default: sharp } return sharp - } catch { + } catch (e) { + if (e instanceof ImageProcessorUnavailableError) throw e // Fall back to sharp if native module is not available // biome-ignore lint/suspicious/noConsole: intentional warning console.warn( @@ -58,12 +73,20 @@ export async function getImageProcessor(): Promise { // Use sharp for non-bundled builds or as fallback. // Single structural cast: our SharpFunction is a subset of sharp's actual type surface. - const imported = (await import( - 'sharp' - )) as unknown as MaybeDefault - const sharp = unwrapDefault(imported) - imageProcessorModule = { default: sharp } - return sharp + try { + const imported = (await import( + 'sharp' + )) as unknown as MaybeDefault & { __stub?: boolean } + if (imported && (imported as { __stub?: boolean }).__stub) { + throw new ImageProcessorUnavailableError() + } + const sharp = unwrapDefault(imported as MaybeDefault) + imageProcessorModule = { default: sharp } + return sharp + } catch (e) { + if (e instanceof ImageProcessorUnavailableError) throw e + throw new ImageProcessorUnavailableError() + } } /** diff --git a/src/utils/imagePaste.ts b/src/utils/imagePaste.ts index 9d3c2b04..1445251a 100644 --- a/src/utils/imagePaste.ts +++ b/src/utils/imagePaste.ts @@ -28,6 +28,31 @@ type SupportedPlatform = 'darwin' | 'linux' | 'win32' // Threshold in characters for when to consider text a "large paste" export const PASTE_THRESHOLD = 800 + +export const LINUX_CLIPBOARD_IMAGE_MIME_TYPES = [ + 'image/png', + 'image/jpeg', + 'image/jpg', + 'image/gif', + 'image/webp', + 'image/bmp', +] + +export function buildLinuxClipboardCheckCommand(): string { + const mimePattern = LINUX_CLIPBOARD_IMAGE_MIME_TYPES.map(mimeType => + mimeType.replace('/', '\\/'), + ).join('|') + + return `xclip -selection clipboard -t TARGETS -o 2>/dev/null | grep -E "${mimePattern}" || wl-paste -l 2>/dev/null | grep -E "${mimePattern}"` +} + +export function buildLinuxClipboardSaveCommand(screenshotPath: string): string { + return LINUX_CLIPBOARD_IMAGE_MIME_TYPES.flatMap(mimeType => [ + `xclip -selection clipboard -t ${mimeType} -o > "${screenshotPath}" 2>/dev/null`, + `wl-paste --type ${mimeType} > "${screenshotPath}" 2>/dev/null`, + ]).join(' || ') +} + function getClipboardCommands() { const platform = process.platform as SupportedPlatform @@ -62,9 +87,8 @@ function getClipboardCommands() { deleteFile: `rm -f "${screenshotPath}"`, }, linux: { - checkImage: - 'xclip -selection clipboard -t TARGETS -o 2>/dev/null | grep -E "image/(png|jpeg|jpg|gif|webp|bmp)" || wl-paste -l 2>/dev/null | grep -E "image/(png|jpeg|jpg|gif|webp|bmp)"', - saveImage: `xclip -selection clipboard -t image/png -o > "${screenshotPath}" 2>/dev/null || wl-paste --type image/png > "${screenshotPath}" 2>/dev/null || xclip -selection clipboard -t image/bmp -o > "${screenshotPath}" 2>/dev/null || wl-paste --type image/bmp > "${screenshotPath}"`, + checkImage: buildLinuxClipboardCheckCommand(), + saveImage: buildLinuxClipboardSaveCommand(screenshotPath), getPath: 'xclip -selection clipboard -t text/plain -o 2>/dev/null || wl-paste 2>/dev/null', deleteFile: `rm -f "${screenshotPath}"`,