From 58e0e488e4c056c389737cd999a0c458068c0c93 Mon Sep 17 00:00:00 2001 From: ztimson Date: Wed, 29 Jul 2026 22:34:51 -0400 Subject: [PATCH] Added Geo, FS and flarescraperr tools --- src/tools.ts | 515 ++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 472 insertions(+), 43 deletions(-) diff --git a/src/tools.ts b/src/tools.ts index 2dedb14..69aaeec 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -91,20 +91,33 @@ export function convertSchema(schema: any): any { }; } -export const CliTool: AiTool = { +export const ExecCliTool: AiTool = { name: 'cli', description: 'Use the command line interface, returns any output', args: {command: {type: 'string', description: 'Command to run', required: true}}, fn: (args: {command: string}) => $Sync`${args.command}` } -export const DateTimeTool: AiTool = { - name: 'get_datetime', - description: 'Get local/UTC date/time', +export const ExecJSTool: AiTool = { + name: 'exec_javascript', + description: 'Execute commonjs javascript', args: { - timezone: {type: 'string', description: 'Which timezone to return, defaults to local', enum: ['local', 'utc'], default: 'local'} + code: {type: 'string', description: 'CommonJS javascript', required: true} }, - fn: ({timezone}) => new Date()[timezone === 'local' ? 'toString' : 'toUTCString']() + fn: async (args: {code: string}) => { + const c = consoleInterceptor(null); + const resp = await Fn({console: c}, args.code, true).catch((err: any) => c.output.error.push(err)); + return {...c.output, return: resp, stdout: undefined, stderr: undefined}; + } +} + +export const ExecPythonTool: AiTool = { + name: 'exec_python', + description: 'Execute commonjs javascript', + args: { + code: {type: 'string', description: 'CommonJS javascript', required: true} + }, + fn: async (args: {code: string}) => ({result: $Sync`python -c "${args.code}"`}) } export const ExecTool: AiTool = { @@ -118,11 +131,11 @@ export const ExecTool: AiTool = { try { switch(args.language) { case 'cli': - return await CliTool.fn({command: args.code}, stream, ai); + return await ExecCliTool.fn({command: args.code}, stream, ai); case 'node': - return await JSTool.fn({code: args.code}, stream, ai); + return await ExecJSTool.fn({code: args.code}, stream, ai); case 'python': - return await PythonTool.fn({code: args.code}, stream, ai); + return await ExecPythonTool.fn({code: args.code}, stream, ai); default: throw new Error(`Unsupported language: ${args.language}`); } @@ -132,7 +145,389 @@ export const ExecTool: AiTool = { } } -export const FetchTool: AiTool = { +export const FsDeleteTool = (whitelist: null | string[] = null): AiTool => { + return { + name: 'fs_delete', + description: 'Delete a file or directory', + args: { + path: {type: 'string', description: 'Path to file or directory', required: true}, + recursive: {type: 'boolean', description: 'Delete all children', required: false} + }, + fn: async ({path, recursive = false}) => { + const {existsSync, rmSync} = await import('fs'); + const normalizePath = p => p.replace(/\\/g, '/'); + + path = normalizePath(path); + if(whitelist && !whitelist.some(p => path.startsWith(p))) return {error: 'Permission denied'}; + if(!existsSync(path)) return {error: 'Path does not exist'}; + + rmSync(path, {recursive, force: true}); + return {success: true, path}; + } + } +} + +export const FsMoveTool = (whitelist: null | string[] = null): AiTool => { + return { + name: 'fs_move', + description: 'Move or rename a file or directory', + args: { + source: {type: 'string', description: 'Path to source file or directory', required: true}, + destination: {type: 'string', description: 'Path to destination file or directory', required: true} + }, + fn: async ({source, destination}) => { + const {existsSync, renameSync} = await import('fs'); + const normalizePath = p => p.replace(/\\/g, '/'); + + source = normalizePath(source); + destination = normalizePath(destination); + if(whitelist && !whitelist.some(p => source.startsWith(p) && destination.startsWith(p))) return {error: 'Permission denied'}; + + if(!existsSync(source)) return {error: 'Source path does not exist'}; + if(existsSync(destination)) return {error: 'Destination path already exists'}; + + renameSync(source, destination); + return {success: true, source, destination}; + } + } +} + +export const FsReadTool = (whitelist: null | string[] = null): AiTool => { + return { + name: 'fs_read', + description: 'Read the contents of a provided path. Works with files and directories', + args: {path: {type: 'string', description: 'Path to file or directory', required: true}}, + fn: async ({path}) => { + const {existsSync, lstatSync, readdirSync, readFileSync} = await import('fs'); + const {join} = await import('path'); + const normalizePath = p => p.replace(/\\/g, '/'); + + path = normalizePath(path); + if(whitelist && !whitelist.some(p => path.startsWith(p))) return {error: 'Permission denied'}; + + if(!existsSync(path)) return {error: 'Path does not exist'}; + const stats = lstatSync(path); + if(stats.isDirectory()) { + const children = readdirSync(path).map(name => { + const childPath = normalizePath(join(path, name)); + const childStats = lstatSync(childPath); + return {name, type: childStats.isDirectory() ? 'directory' : 'file', size: childStats.size}; + }); + return {type: 'directory', children}; + } + const content = readFileSync(path, 'utf-8'); + return {type: 'file', content}; + } + } +} + +export const FsSearchTool = (whitelist: null | string[] = null): AiTool => { + return { + name: 'fs_search', + description: 'Scan a directory for matching glob patterns (e.g. "**/*.js", "src/**/*.test.ts")', + args: { + pattern: {type: 'string', description: 'Glob pattern to match against paths', required: true}, + root: {type: 'string', description: 'Directory to search from', required: false, default: '.'} + }, + fn: async ({pattern, root = '.'}) => { + const {existsSync, lstatSync, readdirSync} = await import('fs'); + const {join, relative} = await import('path'); + const normalizePath = p => p.replace(/\\/g, '/'); + + root = normalizePath(root); + if(!existsSync(root)) return {error: 'Root path does not exist'}; + if(!lstatSync(root).isDirectory()) return {error: 'Root path is not a directory'}; + + if(whitelist && !whitelist.some(p => root.startsWith(p))) return {error: 'Permission denied'}; + + const globToRegex = (glob) => { + let re = ''; + for(let i = 0; i < glob.length; i++) { + const c = glob[i]; + if(c === '*') { + if(glob[i + 1] === '*') { + const isSlash = glob[i + 2] === '/'; + re += '.*'; + i += isSlash ? 2 : 1; + } else { + re += '[^/]*'; + } + } else if(c === '?') { + re += '[^/]'; + } else if('.+^$(){}|[]\\'.includes(c)) { + re += '\\' + c; + } else { + re += c; + } + } + return new RegExp('^' + re + '$'); + }; + const regex = globToRegex(pattern); + + const results: any = []; + const walk = (dir) => { + for(const name of readdirSync(dir)) { + const fullPath = normalizePath(join(dir, name)); + const stats = lstatSync(fullPath); + const relPath = normalizePath(relative(root, fullPath)); + if(regex.test(relPath)) { + results.push({path: relPath, type: stats.isDirectory() ? 'directory' : 'file', size: stats.size}); + } + if(stats.isDirectory()) walk(fullPath); + } + }; + walk(root); + + return results; + } + } +} + +export const FsWriteTool = (whitelist: null | string[] = null): AiTool => { + return { + name: 'fs_write', + description: 'Create a directory, write content to a file or preform a find & replace', + args: { + path: {type: 'string', description: 'Path to file or directory', required: true}, + content: {type: 'string', description: 'Content to write or replace (Omit to create a directory)'}, + find: {type: 'string', description: 'Text or regex pattern to match (regex must match pattern: "/pattern/g")'} + }, + fn: async ({path, content, find}) => { + const {existsSync, mkdirSync, readFileSync, writeFileSync} = await import('fs'); + const {dirname} = await import('path'); + const normalizePath = p => p.replace(/\\/g, '/'); + + path = normalizePath(path); + if(whitelist && !whitelist.some(p => path.startsWith(p))) return {error: 'Permission denied'}; + + if(content === undefined) { + mkdirSync(path, {recursive: true}); + return {success: true, type: 'directory', path}; + } + + const dir = normalizePath(dirname(path)); + if(!existsSync(dir)) mkdirSync(dir, {recursive: true}); + + if(find && existsSync(path)) { + const existing = readFileSync(path, 'utf-8'); + const regexMatch = find.match(/^\/(.+)\/([gimuy]*)$/); + const pattern = regexMatch ? new RegExp(regexMatch[1], regexMatch[2]) : find; + + if(!existing.match(pattern)) return {error: 'Find pattern not found in file'}; + + const updated = existing.replace(pattern, content); + writeFileSync(path, updated, 'utf-8'); + return {success: true, type: 'file', path, replaced: true, content: updated}; + } + + writeFileSync(path, content, 'utf-8'); + return {success: true, type: 'file', path, content}; + } + } +} + +export const GetPathsTool: AiTool = { + name: 'get_paths', + description: 'Get the current working directory, and paths to the users home directory', + fn: async () => { + return { + home: os.homedir(), + cwd: process.cwd() + }; + } +} + +export const GetDatetimeTool: AiTool = { + name: 'get_datetime', + description: 'Get local/UTC timestamp', + args: { + timezone: {type: 'string', description: 'Which timezone to return, defaults to local', enum: ['local', 'utc'], default: 'local'} + }, + fn: ({timezone}) => new Date()[timezone === 'local' ? 'toString' : 'toUTCString']() +} + +export const GetDevice: AiTool = { + name: 'get_device', + description: 'Get comprehensive system information including hostname, specs, load, storage, and network status', + args: {}, + fn: async () => { + const platform = os.platform(); + const hostname = os.hostname(); + + // CPU Info + const cpus = os.cpus(); + const cpuModel = cpus[0].model; + const cpuCores = cpus.length; + + // Memory Info + const totalMem: any = (os.totalmem() / 1024 / 1024 / 1024).toFixed(2); + const freeMem: any = (os.freemem() / 1024 / 1024 / 1024).toFixed(2); + const usedMem: any = (totalMem - freeMem).toFixed(2); + const memUsage: any = ((usedMem / totalMem) * 100).toFixed(1); + + // Load Average (not available on Windows) + const loadAvg = platform === 'win32' ? ['N/A', 'N/A', 'N/A'] : os.loadavg().map(l => l.toFixed(2)); + + // Storage Usage + let storage = {}; + if(platform === 'win32') { + const ps = $Sync`powershell "Get-PSDrive C | Select-Object Used,Free | ConvertTo-Json"`.trim(); + const drive = JSON.parse(ps); + const used: any = (drive.Used / 1024 / 1024 / 1024).toFixed(2); + const free: any = (drive.Free / 1024 / 1024 / 1024).toFixed(2); + const total: any = (parseFloat(used) + parseFloat(free)).toFixed(2); + const usage: any = ((used / total) * 100).toFixed(1); + storage = { + filesystem: 'C:', + size: `${total} GB`, + used: `${used} GB`, + available: `${free} GB`, + usage: `${usage}%` + }; + } else { + const df = $Sync`df -h / | tail -1`.trim(); + const s = df.split(/\s+/); + storage = { + filesystem: s[0], + size: s[1], + used: s[2], + available: s[3], + usage: s[4] + }; + } + + // Network Status + const interfaces = os.networkInterfaces(); + const activeIfaces = Object.entries(interfaces) + .filter(([name]) => name !== 'lo' && !name.includes('Loopback')) + .map(([name, addrs]) => { + const ipv4 = addrs?.find(a => a.family === 'IPv4'); + return ipv4 ? {name, ip: ipv4.address} : null; + }) + .filter(Boolean); + + // Internet connectivity check + let internet = false; + try { + if(platform === 'win32') { + $Sync`powershell "Test-Connection -ComputerName 8.8.8.8 -Count 1 -Quiet"`; + } else { + $Sync`ping -c 1 -W 2 8.8.8.8 > /dev/null 2>&1`; + } + internet = true; + } catch {} + + // Uptime + const uptime = os.uptime(); + const days = Math.floor(uptime / 86400); + const hours = Math.floor((uptime % 86400) / 3600); + const minutes = Math.floor((uptime % 3600) / 60); + + return { + hostname, + cpu: { + model: cpuModel, + cores: cpuCores + }, + memory: { + total: `${totalMem} GB`, + used: `${usedMem} GB`, + free: `${freeMem} GB`, + usage: `${memUsage}%` + }, + load: { + '1min': loadAvg[0], + '5min': loadAvg[1], + '15min': loadAvg[2] + }, + storage, + network: { + interfaces: activeIfaces, + internet: internet ? 'connected' : 'disconnected' + }, + uptime: `${days}d ${hours}h ${minutes}m`, + platform: `${os.type()} ${os.release()}` + }; + } +} + +export const GeoCodeTool: AiTool = { + name: 'geo_code', + description: 'Converts coordinates to address OR vice versa', + args: { + query: {type: 'string', description: 'Search query - coordinates (lat,lon) or address string', required: true}, + }, + fn: async ({query}) => { + const coordinates = /(-?\d+(?:\.\d+)?).*?,.*?(-?\d+(?:\.\d+)?)/.exec(query); + if(coordinates) { // Geolocate + const url = `https://nominatim.openstreetmap.org/reverse?format=json&lat=${encodeURIComponent(coordinates[1])}&lon=${encodeURIComponent(coordinates[2])}`; + const response = await fetch(url, {headers: {'User-Agent': 'OpenSight/1.0', 'Accept-Language': 'en'}}); + const data = await response.json(); + if(data.display_name) return {address: data.display_name, mode: 'geolocate'}; + } else { // Geocode + const url = `https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(query)}`; + const response = await fetch(url, {headers: {'User-Agent': 'OpenSight/1.0'}}); + const data = await response.json(); + if(data[0]) return {latitude: parseFloat(data[0].lat), longitude: parseFloat(data[0].lon), mode: 'geocode'}; + } + return {error: 'Not found'}; + }, +} + +export const GeoWeatherTool: AiTool = { + name: 'geo_weather', + description: 'Gets weather and air quality info for a location and time', + args: { + query: {type: 'string', description: 'Location - address or place name', required: true}, + day: {type: 'string', description: 'Date to retrieve (YYYY-MM-DD), defaults to today'}, + }, + fn: async ({query, day}) => { + day = day || new Date().toISOString().slice(0, 10); + + const geoUrl = `https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(query)}`; + const geoResponse = await fetch(geoUrl, {headers: {'User-Agent': 'OpenSight/1.0'}}); + const geoData = await geoResponse.json(); + if(!geoData[0]) return {error: 'Location not found'}; + + const lat = parseFloat(geoData[0].lat); + const lon = parseFloat(geoData[0].lon); + + const weatherUrl = `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}&start_date=${day}&end_date=${day}&daily=weathercode,temperature_2m_max,temperature_2m_min,apparent_temperature_max,apparent_temperature_min,precipitation_sum,precipitation_probability_max,windspeed_10m_max,winddirection_10m_dominant,uv_index_max,sunrise,sunset&timezone=auto`; + const airUrl = `https://air-quality-api.open-meteo.com/v1/air-quality?latitude=${lat}&longitude=${lon}&start_date=${day}&end_date=${day}&hourly=us_aqi,european_aqi,pm10,pm2_5&timezone=auto`; + + const [weatherResponse, airResponse] = await Promise.all([fetch(weatherUrl), fetch(airUrl)]); + const weatherData = await weatherResponse.json(); + const airData = await airResponse.json(); + + const avg = arr => (arr && arr.length) ? arr.reduce((a, b) => a + b, 0) / arr.length : null; + + return { + location: geoData[0].display_name, + latitude: lat, + longitude: lon, + elevation: weatherData.elevation, + date: day, + weatherCode: weatherData.daily?.weathercode?.[0], + tempMax: weatherData.daily?.temperature_2m_max?.[0], + tempMin: weatherData.daily?.temperature_2m_min?.[0], + feelsLikeMax: weatherData.daily?.apparent_temperature_max?.[0], + feelsLikeMin: weatherData.daily?.apparent_temperature_min?.[0], + precipitation: weatherData.daily?.precipitation_sum?.[0], + precipitationChance: weatherData.daily?.precipitation_probability_max?.[0], + windSpeedMax: weatherData.daily?.windspeed_10m_max?.[0], + windDirection: weatherData.daily?.winddirection_10m_dominant?.[0], + uvIndexMax: weatherData.daily?.uv_index_max?.[0], + sunrise: weatherData.daily?.sunrise?.[0], + sunset: weatherData.daily?.sunset?.[0], + usAqi: avg(airData.hourly?.us_aqi), + europeanAqi: avg(airData.hourly?.european_aqi), + pm10: avg(airData.hourly?.pm10), + pm2_5: avg(airData.hourly?.pm2_5), + }; + }, +} + +export const NetFetchTool: AiTool = { name: 'net_fetch', description: 'Make HTTP request to URL', args: { @@ -149,29 +544,58 @@ export const FetchTool: AiTool = { }) => new Http({url: args.url, headers: args.headers}).request({method: args.method || 'GET', body: args.body}) } -export const JSTool: AiTool = { - name: 'exec_javascript', - description: 'Execute commonjs javascript', - args: { - code: {type: 'string', description: 'CommonJS javascript', required: true} - }, - fn: async (args: {code: string}) => { - const c = consoleInterceptor(null); - const resp = await Fn({console: c}, args.code, true).catch((err: any) => c.output.error.push(err)); - return {...c.output, return: resp, stdout: undefined, stderr: undefined}; +export const NetFlareSolverTool = (host: string) => { + return { + name: 'net_flaresolverr', + description: 'Use a flaresolverr proxy to bypass cloudflare bot detection', + args: { + url: {type: 'string', description: 'URL to fetch', required: true}, + cmd: {type: 'string', description: 'Flaresolverr cmd', enum: ['request.get', 'request.post'], default: 'request.get'}, + maxTimeout: {type: 'number', description: 'Fetch time limit', default: 60_000}, + postData: {type: 'object', description: 'Data to send during request.post requests'}, + }, + fn: async ({url, cmd, maxTimeout, postData}) => { + function toFormUrlEncoded(obj, prefix = '') { + const pairs: any = []; + for (const key in obj) { + if (!obj.hasOwnProperty(key)) continue; + + const value = obj[key]; + const encodedKey = prefix + ? `${prefix}[${encodeURIComponent(key)}]` + : encodeURIComponent(key); + + if (value === null || value === undefined) { + pairs.push(`${encodedKey}=`); + } else if (typeof value === 'object' && !Array.isArray(value)) { + pairs.push(toFormUrlEncoded(value, encodedKey)); + } else if (Array.isArray(value)) { + value.forEach(item => { + pairs.push(`${encodedKey}[]=${encodeURIComponent(item)}`); + }); + } else { + pairs.push(`${encodedKey}=${encodeURIComponent(value)}`); + } + } + + return pairs.join('&'); + } + + const res = await fetch(host + '/v1', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({cmd, url, maxTimeout, postData: postData ? toFormUrlEncoded(postData) : undefined}), + }); + + if(!res.ok) throw new Error(`FlareSolverr HTTP error: ${res.status} ${res.statusText}`); + const data = await res.json(); + if(data.status !== 'ok') throw new Error(`FlareSolverr error: ${data.message ?? data.status}`); + return data.solution.response; + } } } -export const PythonTool: AiTool = { - name: 'exec_python', - description: 'Execute commonjs javascript', - args: { - code: {type: 'string', description: 'CommonJS javascript', required: true} - }, - fn: async (args: {code: string}) => ({result: $Sync`python -c "${args.code}"`}) -} - -export const ReadWebpageTool: AiTool = { +export const NetReadTool: AiTool = { name: 'net_read', description: 'Extract clean content from webpages, or convert media/documents to accessible formats', args: { @@ -275,7 +699,7 @@ export const ReadWebpageTool: AiTool = { } }; -export const WebSearchTool: AiTool = { +export const NetSearchTool: AiTool = { name: 'net_search', description: 'Use duckduckgo (anonymous) to find find relevant online resources. Returns a list of URLs that works great with the `read_webpage` tool', args: { @@ -306,23 +730,28 @@ export const WikipediaTool: AiTool = { description: 'Search Wikipedia for matching articles', args: { query: {type: 'string', description: 'Search term or article title', required: true}, - mode: {type: 'string', description: 'search - look for articles, summary - intro of first found article (default), full - complete first found article', enum: ['search', 'summary', 'full'], default: 'summary'} + mode: {type: 'string', description: 'search - look for articles, summary - intro of first found article (default), full - complete first found article', enum: ['search', 'summary', 'full'], default: 'summary'}, + ua: {type: 'string', description: 'User Agent'}, }, - fn: async (args: {query: string, mode: 'search' | 'summary' | 'full'}) => { - const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'; - + fn: async ({query, mode, ua}) => { class WikipediaClient { - async get(url: string) { - const resp = await fetch(url, {headers: {'User-Agent': UA}}); + useragent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'; + + constructor(useragent: string) { + this.useragent = useragent; + } + + async get(url) { + const resp = await fetch(url, {headers: {'User-Agent': this.useragent}}); return resp.json(); } - api(params: any) { + api(params) { const qs = new URLSearchParams({...params, format: 'json', utf8: '1'}).toString(); return this.get(`https://en.wikipedia.org/w/api.php?${qs}`); } - clean(text: string) { + clean(text) { const cutoffs = ['== See also ==', '== References ==', '== Bibliography ==', '== External links ==']; for (const marker of cutoffs) { const idx = text.indexOf(marker); @@ -335,7 +764,7 @@ export const WikipediaTool: AiTool = { .replace(/^={2}\s*(.+?)\s*={2}$/gm, '## $1') .replace(/\n{3,}/g, '\n\n') .replace(/ {2,}/g, ' ') - .replace(/\[\d+\]/g, '') + .replace(/\[\d+]/g, '') .trim(); } @@ -383,8 +812,8 @@ export const WikipediaTool: AiTool = { } } - const wiki = new WikipediaClient(); - if(args.mode == 'search') return wiki.search(args.query); - return wiki.lookup(args.query, args.mode || 'summary'); + const wiki = new WikipediaClient(ua); + if(mode === 'search') return wiki.search(query); + return wiki.lookup(query, mode || 'summary'); } };