import sharpModule from 'sharp' export const sharp = sharpModule interface NativeModule { hasClipboardImage(): boolean readClipboardImage( maxWidth?: number, maxHeight?: number, ): { png: Buffer width: number height: number originalWidth: number originalHeight: number } | null } function createDarwinNativeModule(): NativeModule { return { hasClipboardImage(): boolean { try { const result = Bun.spawnSync({ cmd: [ 'osascript', '-e', 'try\nthe clipboard as «class PNGf»\nreturn "yes"\non error\nreturn "no"\nend try', ], stdout: 'pipe', stderr: 'pipe', }) const output = result.stdout.toString().trim() return output === 'yes' } catch { return false } }, readClipboardImage( maxWidth?: number, maxHeight?: number, ) { try { // Use osascript to read clipboard image as PNG data and write to a temp file, // then read the temp file back const tmpPath = `/tmp/claude_clipboard_native_${Date.now()}.png` const script = ` set png_data to (the clipboard as «class PNGf») set fp to open for access POSIX file "${tmpPath}" with write permission write png_data to fp close access fp return "${tmpPath}" ` const result = Bun.spawnSync({ cmd: ['osascript', '-e', script], stdout: 'pipe', stderr: 'pipe', }) if (result.exitCode !== 0) { return null } const file = Bun.file(tmpPath) // Use synchronous read via Node compat const fs = require('fs') const buffer: Buffer = fs.readFileSync(tmpPath) // Clean up temp file try { fs.unlinkSync(tmpPath) } catch { // ignore cleanup errors } if (buffer.length === 0) { return null } // Read PNG dimensions from IHDR chunk // PNG header: 8 bytes signature, then IHDR chunk // IHDR starts at offset 8 (4 bytes length) + 4 bytes "IHDR" + 4 bytes width + 4 bytes height let width = 0 let height = 0 if (buffer.length > 24 && buffer[12] === 0x49 && buffer[13] === 0x48 && buffer[14] === 0x44 && buffer[15] === 0x52) { width = buffer.readUInt32BE(16) height = buffer.readUInt32BE(20) } const originalWidth = width const originalHeight = height // If maxWidth/maxHeight are specified and the image exceeds them, // we still return the full PNG - the caller handles resizing via sharp // But we report the capped dimensions if (maxWidth && maxHeight) { if (width > maxWidth || height > maxHeight) { const scale = Math.min(maxWidth / width, maxHeight / height) width = Math.round(width * scale) height = Math.round(height * scale) } } return { png: buffer, width, height, originalWidth, originalHeight, } } catch { return null } }, } } export function getNativeModule(): NativeModule | null { if (process.platform === 'darwin') { return createDarwinNativeModule() } return null } export default sharp