@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
133 lines • 6.09 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
import * as cheerio from 'cheerio';
import { Box, Text } from 'ink';
import { fetch } from 'undici';
import { DEFAULT_WEB_SEARCH_RESULTS, MAX_WEB_SEARCH_QUERY_LENGTH, TIMEOUT_WEB_SEARCH_MS, } from '../constants.js';
import { useTheme } from '../hooks/useTheme.js';
import { jsonSchema, tool } from '../types/core.js';
import { calculateTokens } from '../utils/token-calculator.js';
const executeWebSearch = async (args) => {
const maxResults = args.max_results ?? DEFAULT_WEB_SEARCH_RESULTS;
const encodedQuery = encodeURIComponent(args.query);
try {
// Use Brave Search - scraper-friendly, no CAPTCHA
const searchUrl = `https://search.brave.com/search?q=${encodedQuery}`;
const response = await fetch(searchUrl, {
headers: {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
Accept: 'text/html',
},
signal: AbortSignal.timeout(TIMEOUT_WEB_SEARCH_MS),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const html = await response.text();
const $ = cheerio.load(html);
const results = [];
// Brave Search uses specific result containers
$('[data-type="web"]').each((_i, elem) => {
if (results.length >= maxResults)
return;
const $elem = $(elem);
// Extract title and URL
const titleLink = $elem.find('a[href^="http"]').first();
const url = titleLink.attr('href');
const title = titleLink.text().trim();
// Extract snippet
const snippet = $elem.find('.snippet-description').text().trim();
if (url && title) {
results.push({
title: title || 'No title',
url,
snippet: snippet || '',
});
}
});
if (results.length === 0) {
return `No results found for query: "${args.query}"`;
}
// Format results as markdown for easier LLM reading
let formattedResults = `# Web Search Results: "${args.query}"\n\n`;
for (let i = 0; i < results.length; i++) {
const result = results[i];
formattedResults += `## ${i + 1}. ${result.title}\n\n`;
formattedResults += `**URL:** ${result.url}\n\n`;
if (result.snippet) {
formattedResults += `${result.snippet}\n\n`;
}
formattedResults += '---\n\n';
}
return formattedResults;
}
catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
throw new Error('Search request timeout');
}
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
throw new Error(`Web search failed: ${errorMessage}`);
}
};
const webSearchCoreTool = tool({
description: 'Search the web for information (scrapes Brave Search, returns markdown)',
inputSchema: jsonSchema({
type: 'object',
properties: {
query: {
type: 'string',
description: 'The search query.',
},
max_results: {
type: 'number',
description: 'Maximum number of search results to return (default: 10).',
},
},
required: ['query'],
}),
// Low risk: read-only operation, never requires approval
needsApproval: false,
execute: async (args, _options) => {
return await executeWebSearch(args);
},
});
function WebSearchFormatterComponent({ query, maxResults, result, }) {
const { colors } = useTheme();
// Parse result to count actual results
let resultCount = 0;
let estimatedTokens = 0;
if (result) {
const matches = result.match(/^## \d+\./gm);
resultCount = matches ? matches.length : 0;
estimatedTokens = calculateTokens(result);
}
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { color: colors.tool, children: "\u2692 web_search" }), _jsxs(Box, { children: [_jsx(Text, { color: colors.secondary, children: "Query: " }), _jsx(Box, { marginLeft: 1, children: _jsx(Text, { color: colors.text, children: query }) })] }), _jsxs(Box, { children: [_jsx(Text, { color: colors.secondary, children: "Engine: " }), _jsx(Text, { color: colors.text, children: "Brave Search" })] }), result && (_jsxs(_Fragment, { children: [_jsxs(Box, { children: [_jsx(Text, { color: colors.secondary, children: "Results: " }), _jsxs(Text, { color: colors.text, children: [resultCount, " / ", maxResults, " results"] })] }), _jsxs(Box, { children: [_jsx(Text, { color: colors.secondary, children: "Tokens: " }), _jsxs(Text, { color: colors.text, children: ["~", estimatedTokens, " tokens"] })] })] }))] }));
}
const webSearchFormatter = (args, result) => {
return (_jsx(WebSearchFormatterComponent, { query: args.query || 'unknown', maxResults: args.max_results ?? DEFAULT_WEB_SEARCH_RESULTS, result: result }));
};
const webSearchValidator = (args) => {
const query = args.query?.trim();
// Check if query is empty
if (!query) {
return Promise.resolve({
valid: false,
error: '⚒ Search query cannot be empty',
});
}
// Check query length (reasonable limit)
if (query.length > MAX_WEB_SEARCH_QUERY_LENGTH) {
return Promise.resolve({
valid: false,
error: `⚒ Search query is too long (${query.length} characters). Maximum length is ${MAX_WEB_SEARCH_QUERY_LENGTH} characters.`,
});
}
return Promise.resolve({ valid: true });
};
export const webSearchTool = {
name: 'web_search',
tool: webSearchCoreTool,
formatter: webSearchFormatter,
validator: webSearchValidator,
readOnly: true,
};
//# sourceMappingURL=web-search.js.map