UNPKG

@nanocollective/nanocoder

Version:

A local-first CLI coding agent that brings the power of agentic coding tools like Claude Code and Gemini CLI to local models or controlled APIs like OpenRouter

115 lines 4.93 kB
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; import { convertToMarkdown } from '@nanocollective/get-md'; import { Box, Text } from 'ink'; import { DEFAULT_TERMINAL_COLUMNS, MAX_URL_CONTENT_BYTES } from '../constants.js'; import { useTheme } from '../hooks/useTheme.js'; import { jsonSchema, tool } from '../types/core.js'; import { calculateTokens } from '../utils/token-calculator.js'; const executeFetchUrl = async (args) => { // Validate URL try { new URL(args.url); } catch { throw new Error(`Invalid URL: ${args.url}`); } try { // Use get-md to convert URL to LLM-friendly markdown const result = await convertToMarkdown(args.url); const content = result.markdown; if (!content || content.length === 0) { throw new Error('No content returned from URL'); } // Limit content size to prevent context overflow if (content.length > MAX_URL_CONTENT_BYTES) { const truncated = content.substring(0, MAX_URL_CONTENT_BYTES); return `${truncated}\n\n[Content truncated - original size was ${content.length} characters]`; } return content; } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error'; throw new Error(`Failed to fetch URL: ${message}`); } }; const fetchUrlCoreTool = tool({ description: 'Fetch and parse markdown content from a URL', inputSchema: jsonSchema({ type: 'object', properties: { url: { type: 'string', description: 'The URL to fetch content from.', }, }, required: ['url'], }), // Low risk: read-only operation, never requires approval needsApproval: false, execute: async (args, _options) => { return await executeFetchUrl(args); }, }); function FetchUrlFormatterComponent({ url, result, }) { const { colors } = useTheme(); // Calculate content stats from result let estimatedTokens = 0; let wasTruncated = false; if (result) { estimatedTokens = calculateTokens(result); wasTruncated = result.includes('[Content truncated'); } const terminalWidth = process.stdout.columns || DEFAULT_TERMINAL_COLUMNS; const urlLabelWidth = 6; // "URL: " + 1 margin const availableWidth = Math.max(terminalWidth - urlLabelWidth, 20); const truncatedUrl = url.length <= availableWidth ? url : url.slice(0, Math.floor(availableWidth / 2) - 1) + '…' + url.slice(-(Math.ceil(availableWidth / 2) - 1)); return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { color: colors.tool, children: "\u2692 fetch_url" }), _jsxs(Box, { children: [_jsx(Text, { color: colors.secondary, children: "URL: " }), _jsx(Box, { marginLeft: 1, children: _jsx(Text, { color: colors.text, children: truncatedUrl }) })] }), result && (_jsxs(_Fragment, { children: [_jsxs(Box, { children: [_jsx(Text, { color: colors.secondary, children: "Tokens: " }), _jsxs(Text, { color: colors.text, children: ["~", estimatedTokens, " tokens"] })] }), wasTruncated && (_jsx(Box, { children: _jsx(Text, { color: colors.warning, children: "\u26A0 Content was truncated to 100KB" }) }))] }))] })); } const fetchUrlFormatter = (args, result) => { return (_jsx(FetchUrlFormatterComponent, { url: args.url || 'unknown', result: result })); }; const fetchUrlValidator = (args) => { // Validate URL format try { const parsedUrl = new URL(args.url); // Check for valid protocol if (!['http:', 'https:'].includes(parsedUrl.protocol)) { return Promise.resolve({ valid: false, error: `Invalid URL protocol "${parsedUrl.protocol}". Only http: and https: are supported.`, }); } // Check for localhost/internal IPs (security consideration) const hostname = parsedUrl.hostname.toLowerCase(); if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '0.0.0.0' || hostname.startsWith('192.168.') || hostname.startsWith('10.') || hostname.match(/^172\.(1[6-9]|2[0-9]|3[0-1])\./)) { return Promise.resolve({ valid: false, error: `⚒ Cannot fetch from internal/private network address: ${hostname}`, }); } return Promise.resolve({ valid: true }); } catch { return Promise.resolve({ valid: false, error: `⚒ Invalid URL format: ${args.url}`, }); } }; export const fetchUrlTool = { name: 'fetch_url', tool: fetchUrlCoreTool, formatter: fetchUrlFormatter, validator: fetchUrlValidator, readOnly: true, }; //# sourceMappingURL=fetch-url.js.map