/**
 * mac-tools — pi extension
 *
 * Gives the agent macOS automation capabilities via a Swift CLI that interfaces
 * with Accessibility APIs, NSWorkspace, and CGWindowList.
 *
 * Architecture:
 *  - Swift CLI (`swift-cli/`) handles all macOS API calls
 *  - JSON protocol: stdin `{ command, params }` → stdout `{ success, data?, error? }`
 *  - TS extension invokes CLI per-command via execFileSync
 *  - Mtime-based compilation caching: recompiles only when source files change
 *  - All Swift debug output goes to stderr; only JSON on stdout
 */
import { StringEnum } from "@nerf/pi-ai";
import { Type } from "@sinclair/typebox";
import { execFileSync } from "node:child_process";
import { statSync, readdirSync } from "node:fs";
import path from "node:path";
// ---------------------------------------------------------------------------
// Paths
// ---------------------------------------------------------------------------
const EXTENSION_DIR = path.dirname(new URL(import.meta.url).pathname);
const SWIFT_CLI_DIR = path.join(EXTENSION_DIR, "swift-cli");
const SOURCES_DIR = path.join(SWIFT_CLI_DIR, "Sources");
const BINARY_PATH = path.join(SWIFT_CLI_DIR, ".build", "release", "mac-agent");
const PACKAGE_SWIFT = path.join(SWIFT_CLI_DIR, "Package.swift");
// ---------------------------------------------------------------------------
// Compilation caching
// ---------------------------------------------------------------------------
/** Get the latest mtime (ms) across all Swift source files and Package.swift. */
function getSourceMtime() {
    let latest = 0;
    // Check Package.swift
    try {
        latest = Math.max(latest, statSync(PACKAGE_SWIFT).mtimeMs);
    }
    catch { }
    // Check all files in Sources/
    try {
        const files = readdirSync(SOURCES_DIR);
        for (const f of files) {
            try {
                const mt = statSync(path.join(SOURCES_DIR, f)).mtimeMs;
                if (mt > latest)
                    latest = mt;
            }
            catch { }
        }
    }
    catch { }
    return latest;
}
/** Get the binary mtime (ms), or 0 if it doesn't exist. */
function getBinaryMtime() {
    try {
        return statSync(BINARY_PATH).mtimeMs;
    }
    catch {
        return 0;
    }
}
/** Compile the Swift CLI if source files are newer than the binary. */
function ensureCompiled() {
    const srcMtime = getSourceMtime();
    const binMtime = getBinaryMtime();
    if (binMtime > 0 && binMtime >= srcMtime) {
        return; // Binary is up-to-date
    }
    const action = binMtime === 0 ? "Compiling" : "Recompiling";
    try {
        execFileSync("swift", ["build", "-c", "release"], {
            cwd: SWIFT_CLI_DIR,
            timeout: 30_000,
            stdio: ["pipe", "pipe", "pipe"],
        });
    }
    catch (err) {
        const stderr = err.stderr?.toString() || "";
        const stdout = err.stdout?.toString() || "";
        throw new Error(`Swift compilation failed (${action.toLowerCase()}):\n${stderr || stdout || err.message}`);
    }
}
/**
 * Invoke the mac-agent CLI with a command and optional params.
 * Handles compilation caching, stdin/stdout JSON protocol, and error surfacing.
 */
function execMacAgent(command, params) {
    ensureCompiled();
    const input = JSON.stringify({ command, params: params ?? {} });
    let stdout;
    let stderr = "";
    // Interaction commands (click, type) can block while the target app
    // processes the action — e.g. TextEdit's AXPress on "New Document"
    // takes ~12s while it dismisses the Open dialog and creates a window.
    // Screenshots can also be slow for large retina windows.
    const slowCommands = new Set(["clickElement", "typeText", "screenshotWindow"]);
    const timeout = slowCommands.has(command) ? 30_000 : 10_000;
    try {
        const result = execFileSync(BINARY_PATH, [], {
            input,
            timeout,
            encoding: "utf-8",
            stdio: ["pipe", "pipe", "pipe"],
            maxBuffer: 5 * 1024 * 1024, // 5MB — needed for retina screenshot base64 payloads
        });
        stdout = typeof result === "string" ? result : String(result);
    }
    catch (err) {
        stderr = err.stderr?.toString() || "";
        const isTimeout = err.killed || err.signal === "SIGTERM";
        // If the process exited non-zero but produced stdout, try to parse it
        if (err.stdout) {
            stdout = err.stdout.toString();
        }
        else if (isTimeout) {
            throw new Error(`mac-agent timed out after ${timeout / 1000}s (command: ${command}). ` +
                `The target app may be slow to respond — AXPress can block while the app processes the action.`);
        }
        else {
            throw new Error(`mac-agent CLI failed (command: ${command}):\n${stderr || err.message}`);
        }
    }
    try {
        return JSON.parse(stdout.trim());
    }
    catch {
        throw new Error(`mac-agent returned invalid JSON (command: ${command}):\nstdout: ${stdout}\nstderr: ${stderr}`);
    }
}
// ---------------------------------------------------------------------------
// Extension entry point
// ---------------------------------------------------------------------------
export default function (pi) {
    // -----------------------------------------------------------------
    // mac_check_permissions
    // -----------------------------------------------------------------
    pi.registerTool({
        name: "mac_check_permissions",
        label: "Mac Permissions",
        description: "Check whether macOS Accessibility and Screen Recording permissions are enabled for the current terminal. " +
            "Returns { accessibilityEnabled, screenRecordingEnabled }. Accessibility is required for UI automation; " +
            "Screen Recording is required for mac_screenshot. Both are granted in System Settings > Privacy & Security.",
        promptGuidelines: [
            "Run this first if any mac tool returns a permission error.",
        ],
        parameters: Type.Object({}),
        async execute(_toolCallId) {
            const result = execMacAgent("checkPermissions");
            if (!result.success) {
                throw new Error("mac_check_permissions: " + result.error);
            }
            const accessibility = result.data?.accessibilityEnabled ?? false;
            const screenRecording = result.data?.screenRecordingEnabled ?? false;
            const lines = [];
            lines.push(accessibility
                ? "✅ Accessibility: enabled"
                : "❌ Accessibility: NOT enabled — grant in System Settings > Privacy & Security > Accessibility");
            lines.push(screenRecording
                ? "✅ Screen Recording: enabled"
                : "❌ Screen Recording: NOT enabled — grant in System Settings > Privacy & Security > Screen Recording");
            return {
                content: [{ type: "text", text: lines.join("\n") }],
                details: result.data,
            };
        },
    });
    // -----------------------------------------------------------------
    // mac_list_apps
    // -----------------------------------------------------------------
    pi.registerTool({
        name: "mac_list_apps",
        label: "List Apps",
        description: "List all running macOS applications. Returns an array of { name, bundleId, pid, isActive } " +
            "for user-facing apps (regular activation policy). Set includeBackground to true to also " +
            "include accessory/background apps.",
        promptGuidelines: [
            "Use to discover what apps are running before interacting with them.",
        ],
        parameters: Type.Object({
            includeBackground: Type.Optional(Type.Boolean({ description: "Include background/accessory apps (default: false)" })),
        }),
        async execute(_toolCallId, { includeBackground }) {
            const result = execMacAgent("listApps", includeBackground ? { includeBackground: true } : undefined);
            if (!result.success) {
                throw new Error("mac_list_apps: " + result.error);
            }
            const apps = result.data;
            const summary = apps.map(a => `${a.name} (${a.bundleId}) pid:${a.pid}${a.isActive ? " [active]" : ""}`).join("\n");
            return {
                content: [{ type: "text", text: `${apps.length} running apps:\n${summary}` }],
                details: { apps },
            };
        },
    });
    // -----------------------------------------------------------------
    // mac_launch_app
    // -----------------------------------------------------------------
    pi.registerTool({
        name: "mac_launch_app",
        label: "Launch App",
        description: "Launch a macOS application by name or bundle ID. " +
            "Returns { launched, name, bundleId, pid } on success. " +
            "Provide either 'name' (e.g. 'TextEdit') or 'bundleId' (e.g. 'com.apple.TextEdit').",
        promptGuidelines: [
            "Use app name for well-known apps; use bundleId when the name is ambiguous.",
        ],
        parameters: Type.Object({
            name: Type.Optional(Type.String({ description: "Application name (e.g. 'TextEdit', 'Safari')" })),
            bundleId: Type.Optional(Type.String({ description: "Bundle identifier (e.g. 'com.apple.TextEdit')" })),
        }),
        async execute(_toolCallId, { name, bundleId }) {
            if (!name && !bundleId) {
                throw new Error("mac_launch_app: provide either 'name' or 'bundleId' parameter");
            }
            const params = {};
            if (name)
                params.name = name;
            if (bundleId)
                params.bundleId = bundleId;
            const result = execMacAgent("launchApp", params);
            if (!result.success) {
                throw new Error("mac_launch_app: " + result.error);
            }
            const d = result.data;
            return {
                content: [{ type: "text", text: `Launched ${d.name} (${d.bundleId}) pid:${d.pid}` }],
                details: result.data,
            };
        },
    });
    // -----------------------------------------------------------------
    // mac_activate_app
    // -----------------------------------------------------------------
    pi.registerTool({
        name: "mac_activate_app",
        label: "Activate App",
        description: "Bring a running macOS application to the front. " +
            "Returns { activated, name } on success. Errors if the app is not running. " +
            "Provide either 'name' or 'bundleId'.",
        promptGuidelines: [
            "Activate an app before interacting with its UI to ensure it is frontmost.",
        ],
        parameters: Type.Object({
            name: Type.Optional(Type.String({ description: "Application name" })),
            bundleId: Type.Optional(Type.String({ description: "Bundle identifier" })),
        }),
        async execute(_toolCallId, { name, bundleId }) {
            if (!name && !bundleId) {
                throw new Error("mac_activate_app: provide either 'name' or 'bundleId' parameter");
            }
            const params = {};
            if (name)
                params.name = name;
            if (bundleId)
                params.bundleId = bundleId;
            const result = execMacAgent("activateApp", params);
            if (!result.success) {
                throw new Error("mac_activate_app: " + result.error);
            }
            return {
                content: [{ type: "text", text: `Activated ${result.data?.name}` }],
                details: result.data,
            };
        },
    });
    // -----------------------------------------------------------------
    // mac_quit_app
    // -----------------------------------------------------------------
    pi.registerTool({
        name: "mac_quit_app",
        label: "Quit App",
        description: "Quit a running macOS application. " +
            "Returns { quit, name } on success. Errors if the app is not running. " +
            "Provide either 'name' or 'bundleId'.",
        promptGuidelines: [
            "Use to clean up apps launched during automation — don't leave apps running unnecessarily.",
        ],
        parameters: Type.Object({
            name: Type.Optional(Type.String({ description: "Application name" })),
            bundleId: Type.Optional(Type.String({ description: "Bundle identifier" })),
        }),
        async execute(_toolCallId, { name, bundleId }) {
            if (!name && !bundleId) {
                throw new Error("mac_quit_app: provide either 'name' or 'bundleId' parameter");
            }
            const params = {};
            if (name)
                params.name = name;
            if (bundleId)
                params.bundleId = bundleId;
            const result = execMacAgent("quitApp", params);
            if (!result.success) {
                throw new Error("mac_quit_app: " + result.error);
            }
            return {
                content: [{ type: "text", text: `Quit ${result.data?.name}` }],
                details: result.data,
            };
        },
    });
    // -----------------------------------------------------------------
    // mac_list_windows
    // -----------------------------------------------------------------
    pi.registerTool({
        name: "mac_list_windows",
        label: "List Windows",
        description: "List all on-screen windows for a macOS application. " +
            "Returns an array of { windowId, title, bounds: {x,y,width,height}, isOnScreen, layer }. " +
            "The windowId can be used with getWindowInfo for detailed inspection or with screenshotWindow for capture. " +
            "Returns an empty array (not error) if the app is running but has no visible windows. " +
            "Errors if the app is not running.",
        promptGuidelines: [
            "Use to get windowId values needed by mac_screenshot.",
        ],
        parameters: Type.Object({
            app: Type.String({ description: "Application name (e.g. 'TextEdit') or bundle identifier (e.g. 'com.apple.TextEdit')" }),
        }),
        async execute(_toolCallId, { app }) {
            const result = execMacAgent("listWindows", { app });
            if (!result.success) {
                throw new Error("mac_list_windows: " + result.error);
            }
            const data = result.data;
            const windows = data.windows ?? [];
            if (windows.length === 0) {
                return {
                    content: [{ type: "text", text: `${data.app} (pid:${data.pid}) has no visible windows.` }],
                    details: data,
                };
            }
            const summary = windows.map(w => `  windowId:${w.windowId} "${w.title}" ${w.bounds.width}x${w.bounds.height} at (${w.bounds.x},${w.bounds.y}) layer:${w.layer}`).join("\n");
            return {
                content: [{ type: "text", text: `${data.app} (pid:${data.pid}) — ${windows.length} window(s):\n${summary}` }],
                details: data,
            };
        },
    });
    // -----------------------------------------------------------------
    // mac_find
    // -----------------------------------------------------------------
    pi.registerTool({
        name: "mac_find",
        label: "Find Elements",
        description: "Find UI elements in a macOS application's accessibility tree. Three modes:\n" +
            "- 'search' (default): Find elements matching role/title/value/identifier criteria. Returns a numbered list of matches.\n" +
            "- 'tree': Dump the full accessibility subtree as an indented tree. Use maxDepth/maxCount to bound output.\n" +
            "- 'focused': Get the currently focused element in the app. No criteria needed.\n" +
            "The 'app' param accepts an app name (e.g. 'Finder') or bundle ID (e.g. 'com.apple.Finder').",
        promptGuidelines: [
            "Prefer for targeted element search — use role/title/value criteria to narrow results.",
            "Use mode:focused to check the current focus target without search criteria.",
            "Use mac_get_tree instead of mode:tree when you just need to understand app structure.",
        ],
        parameters: Type.Object({
            app: Type.String({ description: "Application name or bundle identifier" }),
            mode: Type.Optional(StringEnum(["search", "tree", "focused"], { description: "'search' (default), 'tree', or 'focused'" })),
            role: Type.Optional(Type.String({ description: "AX role to match (e.g. 'AXButton', 'AXTextArea')" })),
            title: Type.Optional(Type.String({ description: "AX title to match" })),
            value: Type.Optional(Type.String({ description: "AX value to match" })),
            identifier: Type.Optional(Type.String({ description: "AX identifier to match" })),
            matchType: Type.Optional(Type.String({ description: "'exact' (default) or 'contains'" })),
            maxDepth: Type.Optional(Type.Number({ description: "Maximum tree depth to traverse (default: 10)" })),
            maxCount: Type.Optional(Type.Number({ description: "Maximum elements to return/visit (default: 100)" })),
        }),
        async execute(_toolCallId, args) {
            const mode = args.mode ?? "search";
            // --- Focused mode ---
            if (mode === "focused") {
                const result = execMacAgent("getFocusedElement", { app: args.app });
                if (!result.success) {
                    throw new Error("mac_find (focused): " + result.error);
                }
                const el = result.data;
                const parts = [el.role ?? "unknown"];
                if (el.title)
                    parts.push(`"${el.title}"`);
                if (el.value !== undefined)
                    parts.push(`[${el.value}]`);
                return {
                    content: [{ type: "text", text: `Focused element: ${parts.join(" ")}` }],
                    details: result.data,
                };
            }
            // --- Tree mode ---
            if (mode === "tree") {
                const params = { app: args.app };
                if (args.maxDepth !== undefined)
                    params.maxDepth = args.maxDepth;
                if (args.maxCount !== undefined)
                    params.maxCount = args.maxCount;
                const result = execMacAgent("getTree", params);
                if (!result.success) {
                    throw new Error("mac_find (tree): " + result.error);
                }
                const data = result.data;
                const lines = [];
                function renderTree(nodes, indent) {
                    for (const node of nodes) {
                        const parts = [node.role ?? "?"];
                        if (node.title)
                            parts.push(`"${node.title}"`);
                        if (node.value !== undefined && node.value !== "")
                            parts.push(`[${node.value}]`);
                        lines.push("  ".repeat(indent) + parts.join(" "));
                        if (node.children?.length) {
                            renderTree(node.children, indent + 1);
                        }
                    }
                }
                renderTree(data.tree ?? [], 0);
                const truncNote = data.truncated ? `\n(truncated — ${data.totalElements} elements visited)` : "";
                return {
                    content: [{ type: "text", text: `${lines.join("\n")}${truncNote}` }],
                    details: result.data,
                };
            }
            // --- Search mode (default) ---
            const params = { app: args.app };
            if (args.role)
                params.role = args.role;
            if (args.title)
                params.title = args.title;
            if (args.value)
                params.value = args.value;
            if (args.identifier)
                params.identifier = args.identifier;
            if (args.matchType)
                params.matchType = args.matchType;
            if (args.maxDepth !== undefined)
                params.maxDepth = args.maxDepth;
            if (args.maxCount !== undefined)
                params.maxCount = args.maxCount;
            const result = execMacAgent("findElements", params);
            if (!result.success) {
                throw new Error("mac_find (search): " + result.error);
            }
            const data = result.data;
            const elements = data.elements ?? [];
            if (elements.length === 0) {
                const criteria = [args.role, args.title, args.value, args.identifier].filter(Boolean).join(", ");
                return {
                    content: [{ type: "text", text: `No elements found matching: ${criteria || "(no criteria)"}` }],
                    details: result.data,
                };
            }
            const lines = elements.map((el, i) => {
                const parts = [`${i + 1}. ${el.role ?? "?"}`];
                if (el.title)
                    parts.push(`"${el.title}"`);
                if (el.value !== undefined && el.value !== "")
                    parts.push(`[${el.value}]`);
                return parts.join(" ");
            });
            const truncNote = data.truncated ? `\n(truncated — search stopped at limit)` : "";
            return {
                content: [{ type: "text", text: `${elements.length} element(s) found:\n${lines.join("\n")}${truncNote}` }],
                details: result.data,
            };
        },
    });
    // -----------------------------------------------------------------
    // mac_get_tree
    // -----------------------------------------------------------------
    pi.registerTool({
        name: "mac_get_tree",
        label: "Get UI Tree",
        description: "Get a compact accessibility tree of a macOS application's UI structure. " +
            "Returns an indented tree showing role, title, and value of each element. " +
            "Tighter defaults than mac_find's tree mode — designed for quick structure inspection. " +
            "Each line: `role \"title\" [value]` with 2-space indent per depth level. " +
            "Omits title/value when nil or empty.",
        promptGuidelines: [
            "Use for understanding app UI structure — start with low limits and increase if needed.",
            "Prefer mac_find search mode when you know what you're looking for.",
            "Check the truncation note to know if the tree was cut short.",
        ],
        parameters: Type.Object({
            app: Type.String({ description: "Application name or bundle identifier" }),
            maxDepth: Type.Optional(Type.Number({ description: "Maximum tree depth to traverse (default: 3)" })),
            maxCount: Type.Optional(Type.Number({ description: "Maximum elements to include (default: 50)" })),
        }),
        async execute(_toolCallId, args) {
            const params = { app: args.app };
            params.maxDepth = args.maxDepth ?? 3;
            params.maxCount = args.maxCount ?? 50;
            const result = execMacAgent("getTree", params);
            if (!result.success) {
                throw new Error("mac_get_tree: " + result.error);
            }
            const data = result.data;
            const lines = [];
            function renderNode(nodes, indent) {
                for (const node of nodes) {
                    const parts = [node.role ?? "?"];
                    if (node.title)
                        parts.push(`"${node.title}"`);
                    if (node.value !== undefined && node.value !== null && node.value !== "")
                        parts.push(`[${node.value}]`);
                    lines.push("  ".repeat(indent) + parts.join(" "));
                    if (node.children?.length) {
                        renderNode(node.children, indent + 1);
                    }
                }
            }
            renderNode(data.tree ?? [], 0);
            if (data.truncated) {
                lines.push(`\n(truncated — ${data.totalElements} elements visited, increase maxDepth or maxCount for more)`);
            }
            return {
                content: [{ type: "text", text: lines.join("\n") }],
                details: { totalElements: data.totalElements, truncated: data.truncated },
            };
        },
    });
    // -----------------------------------------------------------------
    // mac_click
    // -----------------------------------------------------------------
    pi.registerTool({
        name: "mac_click",
        label: "Click Element",
        description: "Click a UI element in a macOS application by performing AXPress. " +
            "Finds the first element matching the given criteria (role, title, value, identifier) and clicks it. " +
            "At least one criterion is required. Returns the clicked element's attributes.",
        promptGuidelines: [
            "Verify the click worked by reading the resulting state with mac_find or mac_read.",
            "Use mac_find first to discover the right role/title/value criteria before clicking.",
        ],
        parameters: Type.Object({
            app: Type.String({ description: "Application name or bundle identifier" }),
            role: Type.Optional(Type.String({ description: "AX role (e.g. 'AXButton', 'AXMenuItem')" })),
            title: Type.Optional(Type.String({ description: "AX title to match" })),
            value: Type.Optional(Type.String({ description: "AX value to match" })),
            identifier: Type.Optional(Type.String({ description: "AX identifier to match" })),
            matchType: Type.Optional(Type.String({ description: "'exact' (default) or 'contains'" })),
        }),
        async execute(_toolCallId, args) {
            if (!args.role && !args.title && !args.value && !args.identifier) {
                throw new Error("mac_click: provide at least one search criterion (role, title, value, or identifier)");
            }
            const params = { app: args.app };
            if (args.role)
                params.role = args.role;
            if (args.title)
                params.title = args.title;
            if (args.value)
                params.value = args.value;
            if (args.identifier)
                params.identifier = args.identifier;
            if (args.matchType)
                params.matchType = args.matchType;
            const result = execMacAgent("clickElement", params);
            if (!result.success) {
                throw new Error("mac_click: " + result.error);
            }
            const el = result.data?.element;
            const parts = [el?.role ?? "element"];
            if (el?.title)
                parts.push(`'${el.title}'`);
            return {
                content: [{ type: "text", text: `Clicked ${parts.join(" ")}` }],
                details: result.data,
            };
        },
    });
    // -----------------------------------------------------------------
    // mac_type
    // -----------------------------------------------------------------
    pi.registerTool({
        name: "mac_type",
        label: "Type Text",
        description: "Type text into a UI element in a macOS application by setting its AXValue attribute. " +
            "Finds the first element matching the given criteria and sets its value. " +
            "Returns the actual value after setting (read-back verification). " +
            "At least one criterion is required.",
        promptGuidelines: [
            "Read back the value after typing to verify — the return value includes actual content.",
            "Target text fields/areas by role (AXTextArea, AXTextField) for reliability.",
        ],
        parameters: Type.Object({
            app: Type.String({ description: "Application name or bundle identifier" }),
            text: Type.String({ description: "Text to type into the element" }),
            role: Type.Optional(Type.String({ description: "AX role (e.g. 'AXTextArea', 'AXTextField')" })),
            title: Type.Optional(Type.String({ description: "AX title to match" })),
            value: Type.Optional(Type.String({ description: "AX value to match" })),
            identifier: Type.Optional(Type.String({ description: "AX identifier to match" })),
            matchType: Type.Optional(Type.String({ description: "'exact' (default) or 'contains'" })),
        }),
        async execute(_toolCallId, args) {
            if (!args.role && !args.title && !args.value && !args.identifier) {
                throw new Error("mac_type: provide at least one search criterion (role, title, value, or identifier)");
            }
            const params = { app: args.app, text: args.text };
            if (args.role)
                params.role = args.role;
            if (args.title)
                params.title = args.title;
            if (args.value)
                params.value = args.value;
            if (args.identifier)
                params.identifier = args.identifier;
            if (args.matchType)
                params.matchType = args.matchType;
            const result = execMacAgent("typeText", params);
            if (!result.success) {
                throw new Error("mac_type: " + result.error);
            }
            const el = result.data?.element;
            const actualValue = result.data?.value;
            const parts = [el?.role ?? "element"];
            if (el?.title)
                parts.push(`'${el.title}'`);
            return {
                content: [{ type: "text", text: `Typed into ${parts.join(" ")} — value is now: ${actualValue}` }],
                details: result.data,
            };
        },
    });
    // -----------------------------------------------------------------
    // mac_screenshot
    // -----------------------------------------------------------------
    pi.registerTool({
        name: "mac_screenshot",
        label: "Screenshot Window",
        description: "Take a screenshot of a macOS application window by its window ID (from mac_list_windows). " +
            "Returns the screenshot as an image content block for visual analysis, alongside text metadata " +
            "(dimensions and format). Requires Screen Recording permission — use mac_check_permissions to verify.",
        promptGuidelines: [
            "Use for visual verification when accessibility attributes aren't sufficient.",
            "Prefer nominal resolution unless retina detail is needed — retina doubles payload size.",
            "Requires Screen Recording permission — run mac_check_permissions first if screenshot fails.",
        ],
        parameters: Type.Object({
            windowId: Type.Number({ description: "Window ID from mac_list_windows output" }),
            format: Type.Optional(StringEnum(["jpeg", "png"], { description: "'jpeg' (default) or 'png'" })),
            quality: Type.Optional(Type.Number({ description: "JPEG compression quality 0-1 (default: 0.8)" })),
            retina: Type.Optional(Type.Boolean({ description: "Capture at full pixel resolution (default: false)" })),
        }),
        async execute(_toolCallId, args) {
            const params = { windowId: args.windowId };
            if (args.format)
                params.format = args.format;
            if (args.quality !== undefined)
                params.quality = args.quality;
            if (args.retina !== undefined)
                params.retina = args.retina;
            const result = execMacAgent("screenshotWindow", params);
            if (!result.success) {
                throw new Error("mac_screenshot: " + result.error);
            }
            const data = result.data;
            const imageData = data.imageData;
            const format = data.format;
            const width = data.width;
            const height = data.height;
            const mimeType = format === "png" ? "image/png" : "image/jpeg";
            return {
                content: [
                    { type: "text", text: `Screenshot: ${width}x${height} ${format}` },
                    { type: "image", data: imageData, mimeType },
                ],
                details: { width, height, format, mimeType },
            };
        },
    });
    // -----------------------------------------------------------------
    // mac_read
    // -----------------------------------------------------------------
    pi.registerTool({
        name: "mac_read",
        label: "Read Attribute",
        description: "Read one or more accessibility attributes from a UI element in a macOS application. " +
            "Finds the first element matching the given criteria and reads the named attribute(s). " +
            "AXValue subtypes (CGPoint, CGSize, CGRect, CFRange) are automatically unpacked to structured dicts. " +
            "Use 'attribute' for a single attribute or 'attributes' for multiple. At least one search criterion is required.",
        promptGuidelines: [
            "Use to verify state after actions — read AXValue to confirm text was typed, AXEnabled to check if a button is active.",
        ],
        parameters: Type.Object({
            app: Type.String({ description: "Application name or bundle identifier" }),
            attribute: Type.Optional(Type.String({ description: "Single attribute name to read (e.g. 'AXValue', 'AXPosition', 'AXRole')" })),
            attributes: Type.Optional(Type.Array(Type.String(), { description: "Multiple attribute names to read" })),
            role: Type.Optional(Type.String({ description: "AX role (e.g. 'AXButton', 'AXTextArea')" })),
            title: Type.Optional(Type.String({ description: "AX title to match" })),
            value: Type.Optional(Type.String({ description: "AX value to match" })),
            identifier: Type.Optional(Type.String({ description: "AX identifier to match" })),
            matchType: Type.Optional(Type.String({ description: "'exact' (default) or 'contains'" })),
        }),
        async execute(_toolCallId, args) {
            if (!args.attribute && (!args.attributes || args.attributes.length === 0)) {
                throw new Error("mac_read: provide 'attribute' (single) or 'attributes' (array) parameter");
            }
            if (!args.role && !args.title && !args.value && !args.identifier) {
                throw new Error("mac_read: provide at least one search criterion (role, title, value, or identifier)");
            }
            const params = { app: args.app };
            if (args.attribute)
                params.attribute = args.attribute;
            if (args.attributes)
                params.attributes = args.attributes;
            if (args.role)
                params.role = args.role;
            if (args.title)
                params.title = args.title;
            if (args.value)
                params.value = args.value;
            if (args.identifier)
                params.identifier = args.identifier;
            if (args.matchType)
                params.matchType = args.matchType;
            const result = execMacAgent("readAttribute", params);
            if (!result.success) {
                throw new Error("mac_read: " + result.error);
            }
            // Format output based on single vs multi attribute
            if (args.attribute && !args.attributes) {
                const val = result.data?.value;
                const formatted = typeof val === "object" ? JSON.stringify(val) : String(val);
                return {
                    content: [{ type: "text", text: `${args.attribute}: ${formatted}` }],
                    details: result.data,
                };
            }
            // Multi-attribute: format as key: value lines
            const values = result.data?.values;
            if (values) {
                const lines = Object.entries(values).map(([k, v]) => {
                    const formatted = typeof v === "object" ? JSON.stringify(v) : String(v);
                    return `${k}: ${formatted}`;
                });
                return {
                    content: [{ type: "text", text: lines.join("\n") }],
                    details: result.data,
                };
            }
            // Fallback
            return {
                content: [{ type: "text", text: JSON.stringify(result.data) }],
                details: result.data,
            };
        },
    });
    // -----------------------------------------------------------------
    // System prompt injection — mac-tools usage guidelines
    // -----------------------------------------------------------------
    pi.on("before_agent_start", async (event) => {
        const guidelines = `

[SYSTEM CONTEXT — Mac Tools]

## Native macOS App Interaction

You have mac-tools for controlling native macOS applications (Finder, TextEdit, Safari, Xcode, etc.) via Accessibility APIs.

**Mac-tools vs browser-tools:** Use mac-tools for native macOS apps. Use browser-tools for web pages inside a browser. If you need to interact with a website in Safari or Chrome, use browser-tools — mac-tools controls the browser's native UI chrome (menus, tabs, address bar), not web page content.

**Permissions:** If any mac tool returns a permission error, run \`mac_check_permissions\` to diagnose. Accessibility and Screen Recording permissions are granted in System Settings > Privacy & Security.

**Interaction pattern — discover → act → verify:**
1. **Discover** the UI structure with \`mac_find\` (search for specific elements) or \`mac_get_tree\` (see overall layout)
2. **Act** with \`mac_click\` (press buttons/menus) or \`mac_type\` (enter text into fields)
3. **Verify** the result with \`mac_read\` (check attribute values) or \`mac_screenshot\` (visual confirmation)

**Tree queries:** Start with default limits (mac_get_tree: maxDepth:3, maxCount:50). Increase only if the element you need isn't visible in the output. Large trees waste context.

**Screenshots:** Use \`mac_screenshot\` only when visual verification is genuinely needed — the image payload is large. Prefer \`mac_read\` or \`mac_find\` for checking text values and element state.`;
        return { systemPrompt: event.systemPrompt + guidelines };
    });
}
