"use strict";
// =============================================================================
// Pure Agent - Web Tools
// Ported from Hermes tools/web_tools.py
// web_search: search the web via DuckDuckGo (default) or configured API
// web_extract: fetch and extract content from URLs
// =============================================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.registerWebTools = registerWebTools;
const tool_registry_1 = require("./tool-registry");
const result_types_1 = require("./result-types");
const logger_1 = require("./logger");
const utils_1 = require("./utils");
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
const MAX_CONTENT_SIZE = 2_000_000; // 2MB
const MAX_OUTPUT_SIZE = 50_000;
const WEB_SEARCH_TIMEOUT = 15000;
const WEB_EXTRACT_TIMEOUT = 30000;
const MAX_EXTRACT_URLS = 5;
// SSRF protection: blocked URL patterns
const BLOCKED_URL_PATTERNS = [
    /^https?:\/\/(localhost|127\.|0\.0\.0\.0|::1|0x7f)/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,
    /^ftp:\/\//i,
];
// ---------------------------------------------------------------------------
// URL safety check
// ---------------------------------------------------------------------------
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;
    }
}
function getSearchBackend() {
    if (process.env.TAVILY_API_KEY)
        return 'tavily';
    if (process.env.EXA_API_KEY)
        return 'exa';
    if (process.env.BRAVE_API_KEY)
        return 'brave-free';
    if (process.env.SEARXNG_URL)
        return 'searxng';
    if (process.env.FIRECRAWL_API_KEY)
        return 'firecrawl';
    return 'ddgs'; // DuckDuckGo as default (no API key needed)
}
async function searchDuckDuckGo(query, limit) {
    const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), WEB_SEARCH_TIMEOUT);
    try {
        const response = await fetch(url, {
            signal: controller.signal,
            headers: {
                'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
            },
        });
        clearTimeout(timeout);
        if (!response.ok)
            throw new Error(`DuckDuckGo returned ${response.status}`);
        const html = await response.text();
        const results = [];
        // Parse result blocks
        const resultRegex = /<a[^>]+class="result__a"[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>[\s\S]*?<a[^>]+class="result__snippet"[^>]*>([\s\S]*?)<\/a>/gi;
        let match;
        while ((match = resultRegex.exec(html)) !== null && results.length < limit) {
            const href = match[1];
            const title = stripHtml(match[2]).trim();
            const description = stripHtml(match[3]).trim();
            // Decode DuckDuckGo redirect URL
            let realUrl = href;
            try {
                const urlParam = new URL(href, 'https://duckduckgo.com').searchParams.get('uddg');
                if (urlParam)
                    realUrl = decodeURIComponent(urlParam);
            }
            catch { }
            if (title && realUrl && !realUrl.includes('duckduckgo.com')) {
                results.push({ title, url: realUrl, description });
            }
        }
        return results;
    }
    catch (err) {
        clearTimeout(timeout);
        throw new Error(`DuckDuckGo search failed: ${err.message}`);
    }
}
// ---------------------------------------------------------------------------
// Tavily search (API key required)
// ---------------------------------------------------------------------------
async function searchTavily(query, limit) {
    const apiKey = process.env.TAVILY_API_KEY;
    if (!apiKey)
        throw new Error('TAVILY_API_KEY not set');
    const response = await fetch('https://api.tavily.com/search', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
            api_key: apiKey,
            query,
            max_results: limit,
            include_answer: false,
        }),
        signal: AbortSignal.timeout(WEB_SEARCH_TIMEOUT),
    });
    if (!response.ok)
        throw new Error(`Tavily returned ${response.status}`);
    const data = await response.json();
    return (data.results || []).map((r, i) => ({
        title: r.title || '',
        url: r.url || '',
        description: r.content || '',
    }));
}
// ---------------------------------------------------------------------------
// Brave Search (API key required)
// ---------------------------------------------------------------------------
async function searchBrave(query, limit) {
    const apiKey = process.env.BRAVE_API_KEY;
    if (!apiKey)
        throw new Error('BRAVE_API_KEY not set');
    const url = `https://api.search.brave.com/res/v1/web/search?q=${encodeURIComponent(query)}&count=${limit}`;
    const response = await fetch(url, {
        headers: {
            'Accept': 'application/json',
            'Accept-Encoding': 'gzip',
            'X-Subscription-Token': apiKey,
        },
        signal: AbortSignal.timeout(WEB_SEARCH_TIMEOUT),
    });
    if (!response.ok)
        throw new Error(`Brave returned ${response.status}`);
    const data = await response.json();
    return (data.web?.results || []).map((r) => ({
        title: r.title || '',
        url: r.url || '',
        description: r.description || '',
    }));
}
// ---------------------------------------------------------------------------
// Web search tool handler
// ---------------------------------------------------------------------------
async function webSearchHandler(args, _context) {
    const { query, limit = 5 } = args;
    if (!query)
        return (0, result_types_1.toolError)('Missing required parameter: query');
    const backend = getSearchBackend();
    (0, logger_1.getLogger)().info(`Web search [${backend}]: "${query}" (limit: ${limit})`);
    try {
        let results;
        switch (backend) {
            case 'tavily':
                results = await searchTavily(query, limit);
                break;
            case 'brave-free':
                results = await searchBrave(query, limit);
                break;
            case 'ddgs':
            default:
                results = await searchDuckDuckGo(query, limit);
                break;
        }
        return (0, result_types_1.toolResult)({
            success: true,
            query,
            backend,
            results: results.map((r, i) => ({
                position: i + 1,
                title: r.title,
                url: r.url,
                description: r.description,
            })),
            count: results.length,
        });
    }
    catch (err) {
        return (0, result_types_1.toolError)(`Web search failed: ${err.message}`);
    }
}
// ---------------------------------------------------------------------------
// Web extract tool handler
// ---------------------------------------------------------------------------
async function webExtractHandler(args, _context) {
    const { urls } = args;
    if (!urls || !Array.isArray(urls) || urls.length === 0) {
        return (0, result_types_1.toolError)('Missing required parameter: urls (array of URLs)');
    }
    if (urls.length > MAX_EXTRACT_URLS) {
        return (0, result_types_1.toolError)(`Maximum ${MAX_EXTRACT_URLS} URLs per request`);
    }
    // SSRF check
    for (const url of urls) {
        if (!isSafeUrl(url)) {
            return (0, result_types_1.toolError)(`Blocked unsafe URL: ${url}`);
        }
    }
    (0, logger_1.getLogger)().info(`Web extract: ${urls.length} URLs`);
    const results = await Promise.all(urls.map(async (url) => {
        try {
            const controller = new AbortController();
            const timeout = setTimeout(() => controller.abort(), WEB_EXTRACT_TIMEOUT);
            const response = await fetch(url, {
                signal: controller.signal,
                headers: {
                    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
                    'Accept': 'text/html,application/xhtml+xml,text/plain,*/*',
                },
            });
            clearTimeout(timeout);
            if (!response.ok) {
                return { url, title: '', content: '', error: `HTTP ${response.status}` };
            }
            const contentType = response.headers.get('content-type') || '';
            const rawText = await response.text();
            // Check size
            if (rawText.length > MAX_CONTENT_SIZE) {
                return {
                    url,
                    title: '',
                    content: (0, utils_1.truncate)(rawText, MAX_OUTPUT_SIZE).text,
                    error: `Content too large (${rawText.length} chars), truncated`,
                };
            }
            let title = '';
            let content = '';
            if (contentType.includes('text/html')) {
                // Extract title
                const titleMatch = rawText.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
                if (titleMatch)
                    title = stripHtml(titleMatch[1]).trim();
                // Extract main content
                content = extractHtmlContent(rawText);
            }
            else {
                // Plain text
                content = rawText;
            }
            // Truncate if needed
            if (content.length > MAX_OUTPUT_SIZE) {
                content = (0, utils_1.truncate)(content, MAX_OUTPUT_SIZE).text;
            }
            return { url, title, content };
        }
        catch (err) {
            return { url, title: '', content: '', error: err.message };
        }
    }));
    return (0, result_types_1.toolResult)({
        success: true,
        results: results.map(r => ({
            url: r.url,
            title: r.title,
            content: r.content,
            error: r.error,
        })),
    });
}
// ---------------------------------------------------------------------------
// HTML content extraction
// ---------------------------------------------------------------------------
function stripHtml(html) {
    return html
        .replace(/<script[\s\S]*?<\/script>/gi, '')
        .replace(/<style[\s\S]*?<\/style>/gi, '')
        .replace(/<[^>]+>/g, ' ')
        .replace(/&nbsp;/g, ' ')
        .replace(/&amp;/g, '&')
        .replace(/&lt;/g, '<')
        .replace(/&gt;/g, '>')
        .replace(/&quot;/g, '"')
        .replace(/&#39;/g, "'")
        .replace(/&[a-zA-Z]+;/g, '')
        .replace(/\s+/g, ' ');
}
function extractHtmlContent(html) {
    // Remove script, style, nav, header, footer, aside
    let cleaned = html
        .replace(/<script[\s\S]*?<\/script>/gi, '')
        .replace(/<style[\s\S]*?<\/style>/gi, '')
        .replace(/<nav[\s\S]*?<\/nav>/gi, '')
        .replace(/<header[\s\S]*?<\/header>/gi, '')
        .replace(/<footer[\s\S]*?<\/footer>/gi, '')
        .replace(/<aside[\s\S]*?<\/aside>/gi, '')
        .replace(/<!--[\s\S]*?-->/g, '');
    // Try to find main content area
    const mainMatch = cleaned.match(/<main[\s\S]*?>([\s\S]*?)<\/main>/i)
        || cleaned.match(/<article[\s\S]*?>([\s\S]*?)<\/article>/i)
        || cleaned.match(/<div[^>]*class="[^"]*content[^"]*"[^>]*>([\s\S]*?)<\/div>/i);
    if (mainMatch) {
        cleaned = mainMatch[1];
    }
    // Convert block elements to newlines
    cleaned = cleaned
        .replace(/<\/?(p|div|h[1-6]|li|tr|br\s*\/?)>/gi, '\n')
        .replace(/<[^>]+>/g, ' ');
    // Decode entities and clean whitespace
    cleaned = cleaned
        .replace(/&nbsp;/g, ' ')
        .replace(/&amp;/g, '&')
        .replace(/&lt;/g, '<')
        .replace(/&gt;/g, '>')
        .replace(/&quot;/g, '"')
        .replace(/&#39;/g, "'")
        .replace(/&[a-zA-Z]+;/g, '')
        .replace(/[ \t]+/g, ' ')
        .replace(/\n\s*\n\s*\n/g, '\n\n')
        .trim();
    return cleaned;
}
// ---------------------------------------------------------------------------
// Registration
// ---------------------------------------------------------------------------
function registerWebTools() {
    tool_registry_1.registry.register({
        name: 'web_search',
        toolset: 'web',
        schema: {
            description: 'Search the web for information. Returns titles, URLs, and descriptions. Supports DuckDuckGo (default, no API key), Tavily, and Brave search backends.',
            parameters: {
                type: 'object',
                properties: {
                    query: {
                        type: 'string',
                        description: 'The search query',
                    },
                    limit: {
                        type: 'integer',
                        description: 'Number of results (1-20). Default: 5',
                        minimum: 1,
                        maximum: 20,
                        default: 5,
                    },
                },
                required: ['query'],
            },
        },
        handler: webSearchHandler,
        isAsync: true,
        description: 'Search the web',
        emoji: '🔍',
        maxResultSizeChars: MAX_OUTPUT_SIZE,
    });
    tool_registry_1.registry.register({
        name: 'web_extract',
        toolset: 'web',
        schema: {
            description: 'Fetch and extract text content from web pages. Supports HTML pages. Strips scripts, styles, and navigation elements to return clean content.',
            parameters: {
                type: 'object',
                properties: {
                    urls: {
                        type: 'array',
                        items: { type: 'string' },
                        description: `Array of URLs to extract content from (max ${MAX_EXTRACT_URLS})`,
                        maxItems: MAX_EXTRACT_URLS,
                    },
                },
                required: ['urls'],
            },
        },
        handler: webExtractHandler,
        isAsync: true,
        description: 'Extract web page content',
        emoji: '📄',
        maxResultSizeChars: MAX_OUTPUT_SIZE,
    });
}
// Auto-register
registerWebTools();
//# sourceMappingURL=web-tools.js.map