@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
JavaScript
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