"use strict";
/**
* @file windowsTools.ts
* Windows integration tools: PowerShell bridge, screenshot + vision, file search,
* active window info, PowerPoint COM automation.
*
* Windows-native counterpart of macosTools.ts. Drops AppleScript/JXA/Spotlight/
* Keynote in favor of PowerShell (encoded), .NET screen capture, Get-ChildItem
* search, user32 GetForegroundWindow, and PowerPoint COM automation.
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.createWindowsTools = createWindowsTools;
const sdk_1 = require("@lmstudio/sdk");
const zod_1 = require("zod");
const child_process_1 = require("child_process");
const util_1 = require("util");
const path_1 = require("path");
const os_1 = require("os");
const promises_1 = require("fs/promises");
const crypto_1 = require("crypto");
const shared_1 = require("./shared");
const errorCodes_1 = require("./errorCodes");
const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
const POWERSHELL_BIN = "powershell.exe";
const PS_BASE_ARGS = ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass"];
/** Patterns blocked in PowerShell for safety. Case-insensitive substring match. */
const POWERSHELL_BLOCKED = [
"format-volume",
"clear-disk",
"remove-computer",
"stop-computer",
"restart-computer",
"format c:",
"format d:",
"cipher /w",
"del /f /q /s c:\\",
"rd /s /q c:\\",
"rmdir /s /q c:\\",
];
/** Encode a PowerShell command as UTF-16LE base64 for -EncodedCommand. */
function encodePsCommand(script) {
return Buffer.from(script, "utf16le").toString("base64");
}
/** Detect if a PowerShell script was truncated by output token limits. */
function detectPSTruncation(script) {
const trimmed = script.trim();
const opens = (trimmed.match(/\{/g) || []).length;
const closes = (trimmed.match(/\}/g) || []).length;
if (opens > 0 && opens > closes + 1) {
return `unbalanced braces: ${opens} { vs ${closes} }`;
}
const parens = (trimmed.match(/\(/g) || []).length - (trimmed.match(/\)/g) || []).length;
if (parens > 1) {
return "unbalanced parentheses";
}
const quoteCount = (trimmed.match(/"/g) || []).length;
if (quoteCount % 2 !== 0) {
return "unclosed double-quoted string literal";
}
return null;
}
/** Escape a value for a single-quoted PowerShell string literal. */
function psq(s) {
return s.replace(/'/g, "''");
}
function createWindowsTools(config, limits) {
if (process.platform !== "win32")
return []; // Windows only
const tools = [];
const MAX_OUTPUT = limits?.maxOutput ?? 4_000;
// ── PowerShell Bridge ───────────────────────────────────────────────
tools.push((0, sdk_1.tool)({
name: "run_powershell",
description: (0, sdk_1.text) `
Execute PowerShell code on Windows via powershell.exe (5.1).
Use this to control Windows apps, automate the OS, read the registry,
send keys, manage processes, etc.
⚠️ IMPORTANT: For complex scripts (>30 lines or scripts that embed large text),
use script_file instead of script to avoid output truncation:
1. Save the script to a .ps1 file with save_file
2. Call run_powershell with script_file="C:\\path\\to\\script.ps1"
Examples:
- Open Notepad: 'Start-Process notepad.exe'
- Read clipboard: 'Get-Clipboard'
- Set clipboard: 'Set-Clipboard "hello"'
- List top processes by RAM: 'Get-Process | Sort-Object WS -desc | Select -First 5 Name,WS'
- Move a file: 'Move-Item "C:\\Users\\me\\Downloads\\x.pdf" "C:\\Users\\me\\Documents\\"'
- Toast notification: 'Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.MessageBox]::Show("hi")'
Inline scripts are passed via -EncodedCommand (UTF-16LE base64) so quoting
never escapes wrong. File scripts run via -File.
`,
parameters: {
script: zod_1.z.string().optional().describe("PowerShell code to execute (inline). For long scripts, use script_file instead."),
script_file: zod_1.z.string().optional().describe("Path to a .ps1 file to execute. Use this for complex/long scripts to avoid truncation."),
timeout_seconds: zod_1.z.number().optional().describe("Timeout in seconds (default: 30, max: 120)."),
},
implementation: (0, shared_1.createSafeToolImplementation)(async ({ script, script_file, timeout_seconds }) => {
const timeout = Math.min(timeout_seconds || 30, 120) * 1000;
if (!script && !script_file) {
return (0, errorCodes_1.toolError)(errorCodes_1.MISSING_PARAM, "Either 'script' or 'script_file' must be provided.");
}
// ── File-based execution ──
if (script_file) {
try {
await (0, promises_1.stat)(script_file);
}
catch {
return (0, errorCodes_1.toolError)(errorCodes_1.FILE_NOT_FOUND, `Script file not found: ${script_file}`);
}
const fileContent = await (0, promises_1.readFile)(script_file, "utf-8");
const fileLower = fileContent.toLowerCase();
for (const b of POWERSHELL_BLOCKED) {
if (fileLower.includes(b)) {
return (0, errorCodes_1.toolError)(errorCodes_1.BLOCKED_PATTERN, `Blocked: Script file contains "${b}" which is not allowed for safety.`);
}
}
try {
const { stdout, stderr } = await execFileAsync(POWERSHELL_BIN, [...PS_BASE_ARGS, "-File", script_file], {
timeout,
maxBuffer: 1024 * 1024,
});
const output = stdout.trim();
const truncated = output.length > MAX_OUTPUT;
return {
success: true,
output: truncated
? output.substring(0, MAX_OUTPUT) + `\n... (truncated, ${output.length} chars total)`
: output || "(no output)",
...(stderr.trim() ? { warnings: stderr.trim() } : {}),
};
}
catch (e) {
const msg = e.message || String(e);
if (msg.includes("ETIMEDOUT") || msg.includes("killed")) {
return (0, errorCodes_1.toolError)(errorCodes_1.TIMEOUT, `PowerShell timed out after ${(timeout / 1000)}s.`);
}
return (0, errorCodes_1.toolError)(errorCodes_1.EXEC_FAILED, `PowerShell failed: ${msg.substring(0, 500)}`);
}
}
// ── Inline execution via -EncodedCommand ──
const inlineScript = script;
const lower = inlineScript.toLowerCase();
for (const b of POWERSHELL_BLOCKED) {
if (lower.includes(b)) {
return (0, errorCodes_1.toolError)(errorCodes_1.BLOCKED_PATTERN, `Blocked: PowerShell containing "${b}" is not allowed for safety. Use the appropriate tool instead.`);
}
}
const truncationWarning = detectPSTruncation(inlineScript);
if (truncationWarning) {
return (0, errorCodes_1.toolError)(errorCodes_1.SCRIPT_TRUNCATED, `Script appears truncated (${truncationWarning}). Your output was likely cut off.`, `Use script_file instead: save the script with save_file, then call run_powershell with script_file="C:\\path\\to\\script.ps1".`);
}
try {
const encoded = encodePsCommand(inlineScript);
const { stdout, stderr } = await execFileAsync(POWERSHELL_BIN, [...PS_BASE_ARGS, "-EncodedCommand", encoded], {
timeout,
maxBuffer: 1024 * 1024,
});
const output = stdout.trim();
const truncated = output.length > MAX_OUTPUT;
return {
success: true,
output: truncated
? output.substring(0, MAX_OUTPUT) + `\n... (truncated, ${output.length} chars total)`
: output || "(no output)",
...(stderr.trim() ? { warnings: stderr.trim() } : {}),
};
}
catch (e) {
const msg = e.message || String(e);
if (msg.includes("ETIMEDOUT") || msg.includes("killed")) {
return (0, errorCodes_1.toolError)(errorCodes_1.TIMEOUT, `PowerShell timed out after ${(timeout / 1000)}s.`);
}
return (0, errorCodes_1.toolError)(errorCodes_1.EXEC_FAILED, `PowerShell failed: ${msg.substring(0, 500)}`);
}
}, config.allowPowerShell, "run_powershell"),
}));
// ── Screenshot + Vision ─────────────────────────────────────────────
tools.push((0, sdk_1.tool)({
name: "take_screenshot",
description: (0, sdk_1.text) `
Capture a screenshot of the Windows screen and return it as a base64 image
for visual analysis. Use it to read error messages, check UI state, or
verify visual output.
COST: each screenshot adds ~10-25K tokens to the conversation and those
tokens REPLAY in every later turn. Budget yourself 1-2 screenshots per
task. Prefer mode='window' or mode='region' over full screen.
Modes:
- Full screen (default): entire primary display
- Window: a specific app's main window (by process name, e.g. 'chrome', 'notepad')
- Region: a specific rectangle (x, y, width, height)
`,
parameters: {
mode: zod_1.z.enum(["screen", "window", "region"]).optional().describe("Capture mode (default: screen)."),
app_name: zod_1.z.string().optional().describe("Process name for window capture (e.g. 'chrome', 'notepad', 'code')."),
region: zod_1.z.object({
x: zod_1.z.number(),
y: zod_1.z.number(),
width: zod_1.z.number(),
height: zod_1.z.number(),
}).optional().describe("Region coordinates for region capture."),
max_dimension: zod_1.z.number().optional().describe("Max width/height in pixels (default: 768)."),
},
implementation: (0, shared_1.createSafeToolImplementation)(async ({ mode = "screen", app_name, region, max_dimension }) => {
const maxDim = Math.min(max_dimension || 768, 2048);
const tmpDir = (0, path_1.join)((0, os_1.tmpdir)(), `maestro-ss-${(0, crypto_1.randomBytes)(6).toString("hex")}`);
await (0, promises_1.mkdir)(tmpDir, { recursive: true });
const rawPath = (0, path_1.join)(tmpDir, "raw.png");
const resizedPath = (0, path_1.join)(tmpDir, "resized.jpg");
try {
let psCapture;
if (mode === "window" && app_name) {
psCapture = buildWindowCaptureScript(app_name, rawPath);
}
else if (mode === "region" && region) {
psCapture = buildRegionCaptureScript(region, rawPath);
}
else {
psCapture = buildScreenCaptureScript(rawPath);
}
const encoded = encodePsCommand(psCapture);
await execFileAsync(POWERSHELL_BIN, [...PS_BASE_ARGS, "-EncodedCommand", encoded], {
timeout: 15000,
maxBuffer: 1024 * 1024,
});
// Try ffmpeg first (reuse existing infrastructure), fall back to PowerShell.
try {
const { getFfmpegPath } = await Promise.resolve().then(() => __importStar(require("../media/ffmpegPath")));
const ffmpeg = await getFfmpegPath();
await execFileAsync(ffmpeg, [
"-i", rawPath,
"-vf", `scale='min(${maxDim},iw)':'min(${maxDim},ih)':force_original_aspect_ratio=decrease`,
"-q:v", "10", "-y", resizedPath,
]);
}
catch {
const encodedResize = encodePsCommand(buildResizeScript(rawPath, resizedPath, maxDim));
await execFileAsync(POWERSHELL_BIN, [...PS_BASE_ARGS, "-EncodedCommand", encodedResize], {
timeout: 15000,
maxBuffer: 1024 * 1024,
});
}
const buf = await (0, promises_1.readFile)(resizedPath);
const dataUri = `data:image/jpeg;base64,${buf.toString("base64")}`;
return {
success: true,
image: dataUri,
mode,
...(app_name ? { app: app_name } : {}),
bytes: buf.byteLength,
note: "Screenshot captured. Analyze the image above. NOTE: this image will replay in every later turn — extract what you need now and avoid taking another unless strictly necessary.",
};
}
catch (e) {
return (0, errorCodes_1.toolError)(errorCodes_1.EXEC_FAILED, `Screenshot failed: ${e.message || String(e)}`);
}
finally {
await (0, promises_1.rm)(tmpDir, { recursive: true, force: true }).catch(() => { });
}
}, config.allowScreenshot, "take_screenshot"),
}));
// ── Windows File Search ─────────────────────────────────────────────
tools.push((0, sdk_1.tool)({
name: "windows_search",
description: (0, sdk_1.text) `
Search files on Windows by name or content using PowerShell.
Faster than enumerating directories manually.
Defaults to ~/Documents when no directory is given.
Examples:
- Name search (wildcards): query='*.pdf' or query='budget*'
- Content search: set content=true and query is the text to find inside files
For huge folder trees use a narrower 'directory'. This walks the filesystem;
it is not indexed search.
`,
parameters: {
query: zod_1.z.string().describe("Search query — supports wildcards (e.g. '*.pdf', 'budget*')."),
directory: zod_1.z.string().optional().describe("Limit search to a specific directory (default: %USERPROFILE%\\Documents)."),
limit: zod_1.z.number().optional().describe("Max results to return (default: 20, max: 50)."),
content: zod_1.z.boolean().optional().describe("If true, search inside file CONTENTS instead of names. Slower."),
},
implementation: async ({ query, directory, limit, content }) => {
const maxResults = Math.min(limit || 20, 50);
const dir = directory ||
(process.env.USERPROFILE ? `${process.env.USERPROFILE}\\Documents` : "C:\\");
try {
const ps = content
? `
$ErrorActionPreference = 'SilentlyContinue'
$files = Get-ChildItem -Path '${psq(dir)}' -Recurse -File -ErrorAction SilentlyContinue
$matches = $files | Select-String -Pattern '${psq(query)}' -List -ErrorAction SilentlyContinue |
Select-Object -First ${maxResults} -Property Path, LineNumber, Line
if ($matches) { $matches | ConvertTo-Json -Compress -Depth 3 } else { '[]' }
`
: `
$ErrorActionPreference = 'SilentlyContinue'
$files = Get-ChildItem -Path '${psq(dir)}' -Recurse -File -Filter '${psq(query)}' -ErrorAction SilentlyContinue |
Select-Object -First ${maxResults} -Property FullName, Length, LastWriteTime
if ($files) { $files | ConvertTo-Json -Compress -Depth 3 } else { '[]' }
`;
const encoded = encodePsCommand(ps);
const { stdout } = await execFileAsync(POWERSHELL_BIN, [...PS_BASE_ARGS, "-EncodedCommand", encoded], {
timeout: 30000,
maxBuffer: 4 * 1024 * 1024,
});
const trimmed = stdout.trim() || "[]";
let parsed;
try {
parsed = JSON.parse(trimmed);
}
catch {
return (0, errorCodes_1.toolError)(errorCodes_1.EXEC_FAILED, `Failed to parse search results: ${trimmed.substring(0, 200)}`);
}
const arr = Array.isArray(parsed) ? parsed : [parsed];
return {
query,
directory: dir,
total_found: arr.length,
results: arr,
...(arr.length === maxResults ? {
note: `Showing up to ${maxResults} results. Increase 'limit' or narrow your query/directory if you need more.`,
} : {}),
};
}
catch (e) {
return (0, errorCodes_1.toolError)(errorCodes_1.EXEC_FAILED, `Windows search failed: ${e.message || String(e)}`);
}
},
}));
// ── Active Window Info ──────────────────────────────────────────────
tools.push((0, sdk_1.tool)({
name: "get_active_window",
description: (0, sdk_1.text) `
Get information about the currently active (foreground) Windows window:
title, owning process name, and process ID. Useful for understanding
the user's current context.
`,
parameters: {},
implementation: async () => {
const ps = `
Add-Type @"
using System;
using System.Runtime.InteropServices;
using System.Text;
public class _MW {
[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll", CharSet = CharSet.Unicode)] public static extern int GetWindowText(IntPtr hWnd, StringBuilder text, int count);
[DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint pid);
}
"@
$h = [_MW]::GetForegroundWindow()
$sb = New-Object System.Text.StringBuilder 512
[void][_MW]::GetWindowText($h, $sb, 512)
$pidOut = 0
[void][_MW]::GetWindowThreadProcessId($h, [ref]$pidOut)
$proc = Get-Process -Id $pidOut -ErrorAction SilentlyContinue
$result = @{
title = $sb.ToString()
processName = if ($proc) { $proc.ProcessName } else { 'unknown' }
pid = [int]$pidOut
}
$result | ConvertTo-Json -Compress
`;
try {
const encoded = encodePsCommand(ps);
const { stdout } = await execFileAsync(POWERSHELL_BIN, [...PS_BASE_ARGS, "-EncodedCommand", encoded], { timeout: 5000 });
const trimmed = stdout.trim();
try {
return JSON.parse(trimmed);
}
catch {
return { raw: trimmed };
}
}
catch (e) {
return (0, errorCodes_1.toolError)(errorCodes_1.EXEC_FAILED, `Failed to get active window: ${e.message || String(e)}`);
}
},
}));
// ── PowerPoint Builder ──────────────────────────────────────────────
tools.push((0, sdk_1.tool)({
name: "build_powerpoint",
description: (0, sdk_1.text) `
Build a PowerPoint (.pptx) presentation from structured data via PowerPoint
COM automation. You don't write any PowerShell — provide an array of slides.
Each slide has a title, optional body, optional image path, and optional notes.
Use this instead of run_powershell when building presentations.
Requires Microsoft PowerPoint to be installed locally.
`,
parameters: {
slides: zod_1.z.array(zod_1.z.object({
title: zod_1.z.string().describe("Slide title text."),
body: zod_1.z.string().optional().describe("Slide body text."),
image_path: zod_1.z.string().optional().describe("Absolute path to an image file to add to the slide."),
notes: zod_1.z.string().optional().describe("Presenter notes for this slide."),
})).describe("Array of slides to create."),
output_path: zod_1.z.string().optional().describe("Where to save the .pptx (default: %USERPROFILE%\\Documents\\maestro-pres-<timestamp>.pptx)."),
},
implementation: (0, shared_1.createSafeToolImplementation)(async ({ slides, output_path }) => {
if (!slides || slides.length === 0) {
return (0, errorCodes_1.toolError)(errorCodes_1.NO_SLIDES, "At least one slide is required.");
}
if (slides.length > 100) {
return (0, errorCodes_1.toolError)(errorCodes_1.SLIDES_LIMIT, "Maximum 100 slides per call.");
}
const docsDir = process.env.USERPROFILE ? (0, path_1.join)(process.env.USERPROFILE, "Documents") : (0, os_1.tmpdir)();
const outPath = output_path || (0, path_1.join)(docsDir, `maestro-pres-${Date.now()}.pptx`);
// Pass slides as JSON file to avoid PS quoting issues.
const tmpJson = (0, path_1.join)((0, os_1.tmpdir)(), `maestro-ppt-${(0, crypto_1.randomBytes)(6).toString("hex")}.json`);
const tmpScript = (0, path_1.join)((0, os_1.tmpdir)(), `maestro-ppt-${(0, crypto_1.randomBytes)(6).toString("hex")}.ps1`);
try {
await (0, promises_1.writeFile)(tmpJson, JSON.stringify(slides), "utf-8");
const psScript = `
$ErrorActionPreference = 'Stop'
$slides = Get-Content -Raw -Path '${psq(tmpJson)}' | ConvertFrom-Json
$ppt = New-Object -ComObject PowerPoint.Application
try {
$ppt.Visible = [Microsoft.Office.Core.MsoTriState]::msoTrue
} catch {
# Some PowerPoint editions reject hiding the window; ignore.
}
$pres = $ppt.Presentations.Add()
$layoutTitle = 1 # ppLayoutTitle
$layoutText = 2 # ppLayoutText (title + content)
foreach ($s in $slides) {
$hasBody = $false
if ($s.PSObject.Properties.Match('body').Count -gt 0 -and $s.body) { $hasBody = $true }
$layout = $layoutTitle
if ($hasBody) { $layout = $layoutText }
$slide = $pres.Slides.Add($pres.Slides.Count + 1, $layout)
if ($slide.Shapes.HasTitle) { $slide.Shapes.Title.TextFrame.TextRange.Text = $s.title }
if ($hasBody -and $slide.Shapes.Count -ge 2) {
try { $slide.Shapes.Item(2).TextFrame.TextRange.Text = $s.body } catch {}
}
if ($s.PSObject.Properties.Match('image_path').Count -gt 0 -and $s.image_path) {
try { [void]$slide.Shapes.AddPicture($s.image_path, $false, $true, 500, 100, 350, 450) } catch {}
}
if ($s.PSObject.Properties.Match('notes').Count -gt 0 -and $s.notes) {
try { $slide.NotesPage.Shapes.Placeholders.Item(2).TextFrame.TextRange.Text = $s.notes } catch {}
}
}
$pres.SaveAs('${psq(outPath)}')
$pres.Close()
$ppt.Quit()
[System.Runtime.InteropServices.Marshal]::ReleaseComObject($pres) | Out-Null
[System.Runtime.InteropServices.Marshal]::ReleaseComObject($ppt) | Out-Null
Write-Output ('Saved: ${psq(outPath)}')
`;
await (0, promises_1.writeFile)(tmpScript, psScript, "utf-8");
const { stdout, stderr } = await execFileAsync(POWERSHELL_BIN, [...PS_BASE_ARGS, "-File", tmpScript], {
timeout: 120000,
maxBuffer: 1024 * 1024,
});
return {
success: true,
output: stdout.trim() || "(no output)",
slides_created: slides.length,
output_path: outPath,
...(stderr.trim() ? { warnings: stderr.trim() } : {}),
};
}
catch (e) {
const msg = e.message || String(e);
if (msg.toLowerCase().includes("cannot find") && msg.toLowerCase().includes("powerpoint")) {
return (0, errorCodes_1.toolError)(errorCodes_1.EXEC_FAILED, "PowerPoint COM automation failed — Microsoft PowerPoint does not appear to be installed.");
}
return (0, errorCodes_1.toolError)(errorCodes_1.EXEC_FAILED, `PowerPoint automation failed: ${msg.substring(0, 500)}`);
}
finally {
await (0, promises_1.rm)(tmpScript, { force: true }).catch(() => { });
await (0, promises_1.rm)(tmpJson, { force: true }).catch(() => { });
}
}, config.allowPowerShell, "build_powerpoint"),
}));
return tools;
}
// ─── Capture / resize helpers ──────────────────────────────────────────
function buildScreenCaptureScript(outPath) {
return `
Add-Type -AssemblyName System.Drawing
Add-Type -AssemblyName System.Windows.Forms
$screen = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds
$bmp = New-Object System.Drawing.Bitmap $screen.Width, $screen.Height
$g = [System.Drawing.Graphics]::FromImage($bmp)
$g.CopyFromScreen($screen.X, $screen.Y, 0, 0, $bmp.Size)
$bmp.Save('${psq(outPath)}', [System.Drawing.Imaging.ImageFormat]::Png)
$g.Dispose(); $bmp.Dispose()
`;
}
function buildRegionCaptureScript(region, outPath) {
return `
Add-Type -AssemblyName System.Drawing
$bmp = New-Object System.Drawing.Bitmap ${Math.max(1, Math.floor(region.width))}, ${Math.max(1, Math.floor(region.height))}
$g = [System.Drawing.Graphics]::FromImage($bmp)
$g.CopyFromScreen(${Math.floor(region.x)}, ${Math.floor(region.y)}, 0, 0, $bmp.Size)
$bmp.Save('${psq(outPath)}', [System.Drawing.Imaging.ImageFormat]::Png)
$g.Dispose(); $bmp.Dispose()
`;
}
function buildWindowCaptureScript(appName, outPath) {
return `
Add-Type @"
using System;
using System.Runtime.InteropServices;
public struct RECT { public int Left; public int Top; public int Right; public int Bottom; }
public class _UWin {
[DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr hWnd, out RECT r);
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd);
}
"@
Add-Type -AssemblyName System.Drawing
$proc = Get-Process -Name '${psq(appName)}' -ErrorAction SilentlyContinue | Where-Object { $_.MainWindowHandle -ne 0 } | Select-Object -First 1
if (-not $proc) { throw "Process '${psq(appName)}' not found or has no visible window." }
$h = $proc.MainWindowHandle
[void][_UWin]::SetForegroundWindow($h)
Start-Sleep -Milliseconds 300
$r = New-Object RECT
[void][_UWin]::GetWindowRect($h, [ref]$r)
$w = $r.Right - $r.Left
$hgt = $r.Bottom - $r.Top
if ($w -le 0 -or $hgt -le 0) { throw "Window has zero size or is minimized." }
$bmp = New-Object System.Drawing.Bitmap $w, $hgt
$g = [System.Drawing.Graphics]::FromImage($bmp)
$g.CopyFromScreen($r.Left, $r.Top, 0, 0, $bmp.Size)
$bmp.Save('${psq(outPath)}', [System.Drawing.Imaging.ImageFormat]::Png)
$g.Dispose(); $bmp.Dispose()
`;
}
function buildResizeScript(inPath, outPath, maxDim) {
return `
Add-Type -AssemblyName System.Drawing
$img = [System.Drawing.Image]::FromFile('${psq(inPath)}')
$ratio = [Math]::Min(${maxDim} / $img.Width, ${maxDim} / $img.Height)
$nw = [int]($img.Width * $ratio)
$nh = [int]($img.Height * $ratio)
$bmp = New-Object System.Drawing.Bitmap $nw, $nh
$g = [System.Drawing.Graphics]::FromImage($bmp)
$g.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
$g.DrawImage($img, 0, 0, $nw, $nh)
$encoder = [System.Drawing.Imaging.ImageCodecInfo]::GetImageEncoders() | Where-Object { $_.MimeType -eq 'image/jpeg' }
$params = New-Object System.Drawing.Imaging.EncoderParameters 1
$params.Param[0] = New-Object System.Drawing.Imaging.EncoderParameter ([System.Drawing.Imaging.Encoder]::Quality, [long]45)
$bmp.Save('${psq(outPath)}', $encoder, $params)
$img.Dispose(); $g.Dispose(); $bmp.Dispose()
`;
}