Something went wrong. Try again.
fork of prime agent with changes i want but are not upstreamable probqbly (and some fixes)
Something went wrong. Try again.
7.6 kB · 285 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286import { randomUUID } from "crypto";import { readFileSync, unlinkSync } from "fs";import { tmpdir } from "os";import { join } from "path";import { spawnSyncHidden } from "./child-process.js";
import { clipboard } from "./clipboard-native.js";import { loadPhoton } from "./photon.js";
export type ClipboardImage = { bytes: Uint8Array; mimeType: string;};
const SUPPORTED_IMAGE_MIME_TYPES = ["image/png", "image/jpeg", "image/webp", "image/gif"] as const;
const DEFAULT_LIST_TIMEOUT_MS = 1000;const DEFAULT_READ_TIMEOUT_MS = 3000;const DEFAULT_POWERSHELL_TIMEOUT_MS = 5000;const DEFAULT_MAX_BUFFER_BYTES = 50 * 1024 * 1024;
export function isWaylandSession(env: NodeJS.ProcessEnv = process.env): boolean { return Boolean(env.WAYLAND_DISPLAY) || env.XDG_SESSION_TYPE === "wayland";}
function baseMimeType(mimeType: string): string { return mimeType.split(";")[0]?.trim().toLowerCase() ?? mimeType.toLowerCase();}
function selectPreferredImageMimeType(mimeTypes: string[]): string | null { const normalized = mimeTypes .map((t) => t.trim()) .filter(Boolean) .map((t) => ({ raw: t, base: baseMimeType(t) }));
for (const preferred of SUPPORTED_IMAGE_MIME_TYPES) { const match = normalized.find((t) => t.base === preferred); if (match) { return match.raw; } }
const anyImage = normalized.find((t) => t.base.startsWith("image/")); return anyImage?.raw ?? null;}
function isSupportedImageMimeType(mimeType: string): boolean { const base = baseMimeType(mimeType); return SUPPORTED_IMAGE_MIME_TYPES.some((t) => t === base);}
/** * Convert unsupported image formats to PNG using Photon. * Returns null if conversion is unavailable or fails. */async function convertToPng(bytes: Uint8Array): Promise<Uint8Array | null> { const photon = await loadPhoton(); if (!photon) { return null; }
try { const image = photon.PhotonImage.new_from_byteslice(bytes); try { return image.get_bytes(); } finally { image.free(); } } catch { return null; }}
function runCommand( command: string, args: string[], options?: { timeoutMs?: number; maxBufferBytes?: number; env?: NodeJS.ProcessEnv },): { stdout: Buffer; ok: boolean } { const timeoutMs = options?.timeoutMs ?? DEFAULT_READ_TIMEOUT_MS; const maxBufferBytes = options?.maxBufferBytes ?? DEFAULT_MAX_BUFFER_BYTES;
const result = spawnSyncHidden(command, args, { timeout: timeoutMs, maxBuffer: maxBufferBytes, env: options?.env, });
if (result.error) { return { ok: false, stdout: Buffer.alloc(0) }; }
if (result.status !== 0) { return { ok: false, stdout: Buffer.alloc(0) }; }
const stdout = Buffer.isBuffer(result.stdout) ? result.stdout : Buffer.from(result.stdout ?? "", typeof result.stdout === "string" ? "utf-8" : undefined);
return { ok: true, stdout };}
function readClipboardImageViaWlPaste(): ClipboardImage | null { const list = runCommand("wl-paste", ["--list-types"], { timeoutMs: DEFAULT_LIST_TIMEOUT_MS }); if (!list.ok) { return null; }
const types = list.stdout .toString("utf-8") .split(/\r?\n/) .map((t) => t.trim()) .filter(Boolean);
const selectedType = selectPreferredImageMimeType(types); if (!selectedType) { return null; }
const data = runCommand("wl-paste", ["--type", selectedType, "--no-newline"]); if (!data.ok || data.stdout.length === 0) { return null; }
return { bytes: data.stdout, mimeType: baseMimeType(selectedType) };}
function isWSL(env: NodeJS.ProcessEnv = process.env): boolean { if (env.WSL_DISTRO_NAME || env.WSLENV) { return true; }
try { const release = readFileSync("/proc/version", "utf-8"); return /microsoft|wsl/i.test(release); } catch { return false; }}
/** * On WSL, the Linux clipboard (Wayland/X11) does not receive image data from * Windows screenshots (Win+Shift+S). PowerShell can access the Windows clipboard * directly, so we use it as a fallback. */function readClipboardImageViaPowerShell(): ClipboardImage | null { const tmpFile = join(tmpdir(), `pi-wsl-clip-${randomUUID()}.png`);
try { const winPathResult = runCommand("wslpath", ["-w", tmpFile], { timeoutMs: DEFAULT_LIST_TIMEOUT_MS }); if (!winPathResult.ok) { return null; }
const winPath = winPathResult.stdout.toString("utf-8").trim(); if (!winPath) { return null; }
const psQuotedWinPath = winPath.replaceAll("'", "''"); const psScript = [ "Add-Type -AssemblyName System.Windows.Forms", "Add-Type -AssemblyName System.Drawing", `$path = '${psQuotedWinPath}'`, "$img = [System.Windows.Forms.Clipboard]::GetImage()", "if ($img) { $img.Save($path, [System.Drawing.Imaging.ImageFormat]::Png); Write-Output 'ok' } else { Write-Output 'empty' }", ].join("; ");
const result = runCommand("powershell.exe", ["-NoProfile", "-Command", psScript], { timeoutMs: DEFAULT_POWERSHELL_TIMEOUT_MS, }); if (!result.ok) { return null; }
const output = result.stdout.toString("utf-8").trim(); if (output !== "ok") { return null; }
const bytes = readFileSync(tmpFile); if (bytes.length === 0) { return null; }
return { bytes: new Uint8Array(bytes), mimeType: "image/png" }; } catch { return null; } finally { try { unlinkSync(tmpFile); } catch { // Ignore cleanup errors. } }}
function readClipboardImageViaXclip(): ClipboardImage | null { const targets = runCommand("xclip", ["-selection", "clipboard", "-t", "TARGETS", "-o"], { timeoutMs: DEFAULT_LIST_TIMEOUT_MS, });
let candidateTypes: string[] = []; if (targets.ok) { candidateTypes = targets.stdout .toString("utf-8") .split(/\r?\n/) .map((t) => t.trim()) .filter(Boolean); }
const preferred = candidateTypes.length > 0 ? selectPreferredImageMimeType(candidateTypes) : null; const tryTypes = preferred ? [preferred, ...SUPPORTED_IMAGE_MIME_TYPES] : [...SUPPORTED_IMAGE_MIME_TYPES];
for (const mimeType of tryTypes) { const data = runCommand("xclip", ["-selection", "clipboard", "-t", mimeType, "-o"]); if (data.ok && data.stdout.length > 0) { return { bytes: data.stdout, mimeType: baseMimeType(mimeType) }; } }
return null;}
async function readClipboardImageViaNativeClipboard(): Promise<ClipboardImage | null> { if (!clipboard || !clipboard.hasImage()) { return null; }
const imageData = await clipboard.getImageBinary(); if (!imageData || imageData.length === 0) { return null; }
const bytes = imageData instanceof Uint8Array ? imageData : Uint8Array.from(imageData); return { bytes, mimeType: "image/png" };}
export async function readClipboardImage(options?: { env?: NodeJS.ProcessEnv; platform?: NodeJS.Platform;}): Promise<ClipboardImage | null> { const env = options?.env ?? process.env; const platform = options?.platform ?? process.platform;
if (env.TERMUX_VERSION) { return null; }
let image: ClipboardImage | null = null;
if (platform === "linux") { const wsl = isWSL(env); const wayland = isWaylandSession(env);
if (wayland || wsl) { image = readClipboardImageViaWlPaste() ?? readClipboardImageViaXclip(); }
if (!image && wsl) { image = readClipboardImageViaPowerShell(); }
if (!image && !wayland) { image = await readClipboardImageViaNativeClipboard(); } } else { image = await readClipboardImageViaNativeClipboard(); }
if (!image) { return null; }
// Convert unsupported formats (e.g., BMP from WSLg) to PNG if (!isSupportedImageMimeType(image.mimeType)) { const pngBytes = await convertToPng(image.bytes); if (!pngBytes) { return null; } return { bytes: pngBytes, mimeType: "image/png" }; }
return image;}