"use strict";
// =============================================================================
// Pure Agent - Vision Tools
// Ported from Hermes tools/vision_tools.py
// Analyze images and videos using multimodal LLM capabilities
// =============================================================================
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 });
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const tool_registry_1 = require("../tool-registry");
const result_types_1 = require("../result-types");
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
const MAX_BASE64_BYTES = 20 * 1024 * 1024; // 20 MB
const MAX_VIDEO_BASE64_BYTES = 50 * 1024 * 1024; // 50 MB
const DOWNLOAD_TIMEOUT = 30_000;
const IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp', '.svg']);
const VIDEO_EXTENSIONS = new Set(['.mp4', '.webm', '.mov', '.avi', '.mkv', '.mpeg', '.mpg']);
// ---------------------------------------------------------------------------
// SSRF protection
// ---------------------------------------------------------------------------
const BLOCKED_URL_PATTERNS = [
    /^https?:\/\/(localhost|127\.|0\.0\.0\.0|::1)/i,
    /^https?:\/\/10\./i,
    /^https?:\/\/172\.(1[6-9]|2[0-9]|3[01])\./i,
    /^https?:\/\/192\.168\./i,
    /^https?:\/\/169\.254\./i,
    /^file:\/\//i,
];
function isSafeUrl(url) {
    try {
        const parsed = new URL(url);
        if (!['http:', 'https:'].includes(parsed.protocol))
            return false;
        for (const pattern of BLOCKED_URL_PATTERNS) {
            if (pattern.test(url))
                return false;
        }
        return true;
    }
    catch {
        return false;
    }
}
// ---------------------------------------------------------------------------
// MIME type detection
// ---------------------------------------------------------------------------
function detectImageMime(filePath) {
    const ext = path.extname(filePath).toLowerCase();
    const mimeMap = {
        '.png': 'image/png',
        '.jpg': 'image/jpeg',
        '.jpeg': 'image/jpeg',
        '.gif': 'image/gif',
        '.bmp': 'image/bmp',
        '.webp': 'image/webp',
        '.svg': 'image/svg+xml',
    };
    return mimeMap[ext] || 'image/jpeg';
}
function detectVideoMime(filePath) {
    const ext = path.extname(filePath).toLowerCase();
    const mimeMap = {
        '.mp4': 'video/mp4',
        '.webm': 'video/webm',
        '.mov': 'video/quicktime',
        '.avi': 'video/x-msvideo',
        '.mkv': 'video/x-matroska',
        '.mpeg': 'video/mpeg',
        '.mpg': 'video/mpeg',
    };
    return mimeMap[ext] || 'video/mp4';
}
// ---------------------------------------------------------------------------
// Image file loading
// ---------------------------------------------------------------------------
async function loadImageSource(imageUrl) {
    // Local file
    if (!imageUrl.startsWith('http://') && !imageUrl.startsWith('https://') && !imageUrl.startsWith('data:')) {
        const filePath = imageUrl.replace(/^file:\/\//, '');
        if (!fs.existsSync(filePath))
            throw new Error(`File not found: ${filePath}`);
        if (!IMAGE_EXTENSIONS.has(path.extname(filePath).toLowerCase())) {
            throw new Error(`Not an image file: ${filePath}`);
        }
        const data = fs.readFileSync(filePath);
        if (data.length > MAX_BASE64_BYTES)
            throw new Error(`Image too large: ${data.length} bytes (max ${MAX_BASE64_BYTES})`);
        return {
            base64: data.toString('base64'),
            mimeType: detectImageMime(filePath),
        };
    }
    // Data URL
    if (imageUrl.startsWith('data:')) {
        const match = imageUrl.match(/^data:(image\/\w+);base64,(.+)$/);
        if (match)
            return { base64: match[2], mimeType: match[1] };
        throw new Error('Invalid data URL format');
    }
    // HTTP URL - download
    if (!isSafeUrl(imageUrl))
        throw new Error(`Blocked unsafe URL: ${imageUrl}`);
    const response = await fetch(imageUrl, {
        signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT),
        headers: { 'User-Agent': 'PureAgent/1.0' },
    });
    if (!response.ok)
        throw new Error(`Download failed: HTTP ${response.status}`);
    const buffer = Buffer.from(await response.arrayBuffer());
    if (buffer.length > MAX_BASE64_BYTES) {
        throw new Error(`Image too large: ${buffer.length} bytes (max ${MAX_BASE64_BYTES})`);
    }
    // Detect MIME from response header or URL
    const contentType = response.headers.get('content-type') || '';
    let mimeType = 'image/jpeg';
    if (contentType.startsWith('image/')) {
        mimeType = contentType.split(';')[0];
    }
    else {
        mimeType = detectImageMime(imageUrl);
    }
    return {
        base64: buffer.toString('base64'),
        mimeType,
    };
}
// ---------------------------------------------------------------------------
// Video file loading
// ---------------------------------------------------------------------------
async function loadVideoSource(videoUrl) {
    // Local file
    if (!videoUrl.startsWith('http://') && !videoUrl.startsWith('https://')) {
        const filePath = videoUrl.replace(/^file:\/\//, '');
        if (!fs.existsSync(filePath))
            throw new Error(`File not found: ${filePath}`);
        if (!VIDEO_EXTENSIONS.has(path.extname(filePath).toLowerCase())) {
            throw new Error(`Not a video file: ${filePath}`);
        }
        const data = fs.readFileSync(filePath);
        if (data.length > MAX_VIDEO_BASE64_BYTES) {
            throw new Error(`Video too large: ${data.length} bytes (max ${MAX_VIDEO_BASE64_BYTES})`);
        }
        return {
            base64: data.toString('base64'),
            mimeType: detectVideoMime(filePath),
        };
    }
    // HTTP URL - download
    if (!isSafeUrl(videoUrl))
        throw new Error(`Blocked unsafe URL: ${videoUrl}`);
    const response = await fetch(videoUrl, {
        signal: AbortSignal.timeout(60_000),
        headers: { 'User-Agent': 'PureAgent/1.0' },
    });
    if (!response.ok)
        throw new Error(`Download failed: HTTP ${response.status}`);
    const buffer = Buffer.from(await response.arrayBuffer());
    if (buffer.length > MAX_VIDEO_BASE64_BYTES) {
        throw new Error(`Video too large: ${buffer.length} bytes (max ${MAX_VIDEO_BASE64_BYTES})`);
    }
    const contentType = response.headers.get('content-type') || '';
    let mimeType = 'video/mp4';
    if (contentType.startsWith('video/')) {
        mimeType = contentType.split(';')[0];
    }
    else {
        mimeType = detectVideoMime(videoUrl);
    }
    return { base64: buffer.toString('base64'), mimeType };
}
// ---------------------------------------------------------------------------
// vision_analyze handler
// ---------------------------------------------------------------------------
async function visionAnalyzeHandler(args, context) {
    const { image_url, question } = args;
    if (!image_url)
        return (0, result_types_1.toolError)('Missing required parameter: image_url');
    if (!question)
        return (0, result_types_1.toolError)('Missing required parameter: question');
    try {
        const { base64, mimeType } = await loadImageSource(image_url);
        // Return multimodal content that the main model can see directly
        // The conversation loop will handle this as a tool result with image content
        const dataUrl = `data:${mimeType};base64,${base64}`;
        return JSON.stringify({
            _multimodal: true,
            content: [
                { type: 'text', text: `Image analysis request: ${question}\n\nThe image has been loaded into context. Please analyze it and answer the question.` },
                { type: 'image_url', image_url: { url: dataUrl } },
            ],
            text_summary: `Image loaded (${Math.round(base64.length * 0.75 / 1024)} KB) for analysis: ${question}`,
            meta: {
                image_url,
                size_bytes: Math.round(base64.length * 0.75),
                native_vision: true,
            },
        });
    }
    catch (err) {
        return (0, result_types_1.toolError)(`Vision analysis failed: ${err.message}`);
    }
}
// ---------------------------------------------------------------------------
// video_analyze handler
// ---------------------------------------------------------------------------
async function videoAnalyzeHandler(args, context) {
    const { video_url, question } = args;
    if (!video_url)
        return (0, result_types_1.toolError)('Missing required parameter: video_url');
    if (!question)
        return (0, result_types_1.toolError)('Missing required parameter: question');
    try {
        const { base64, mimeType } = await loadVideoSource(video_url);
        const dataUrl = `data:${mimeType};base64,${base64}`;
        return JSON.stringify({
            _multimodal: true,
            content: [
                { type: 'text', text: `Video analysis request: ${question}\n\nThe video has been loaded. Please analyze it.` },
                { type: 'video_url', video_url: { url: dataUrl } },
            ],
            text_summary: `Video loaded (${Math.round(base64.length * 0.75 / 1024)} KB) for analysis: ${question}`,
            meta: {
                video_url,
                size_bytes: Math.round(base64.length * 0.75),
            },
        });
    }
    catch (err) {
        return (0, result_types_1.toolError)(`Video analysis failed: ${err.message}`);
    }
}
// ---------------------------------------------------------------------------
// Registration
// ---------------------------------------------------------------------------
function registerVisionTools() {
    tool_registry_1.registry.register({
        name: 'vision_analyze',
        toolset: 'vision',
        schema: {
            description: 'Analyze an image. Provide a URL or local file path and a question about the image. The image will be loaded into the model context for direct visual analysis.',
            parameters: {
                type: 'object',
                properties: {
                    image_url: {
                        type: 'string',
                        description: 'Image URL (http/https), local file path, or data: URL',
                    },
                    question: {
                        type: 'string',
                        description: 'Question or instruction about the image',
                    },
                },
                required: ['image_url', 'question'],
            },
        },
        handler: visionAnalyzeHandler,
        isAsync: true,
        description: 'Analyze an image',
        emoji: '👁️',
    });
    tool_registry_1.registry.register({
        name: 'video_analyze',
        toolset: 'video',
        schema: {
            description: 'Analyze a video. Provide a URL or local file path and a question about the video.',
            parameters: {
                type: 'object',
                properties: {
                    video_url: {
                        type: 'string',
                        description: 'Video URL or local file path (mp4, webm, mov, avi, mkv)',
                    },
                    question: {
                        type: 'string',
                        description: 'Question or instruction about the video',
                    },
                },
                required: ['video_url', 'question'],
            },
        },
        handler: videoAnalyzeHandler,
        isAsync: true,
        description: 'Analyze a video',
        emoji: '🎬',
    });
}
// Auto-register
registerVisionTools();
//# sourceMappingURL=vision.js.map