Forked from brdcastro/maestro
"use strict";
/**
* @file webSearch.ts
* Shared web search functions used by both primary tools and secondary agent.
* Single source of truth — no duplication.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.ensureSearXNG = ensureSearXNG;
exports.searchSearXNG = searchSearXNG;
exports.searchDDGLite = searchDDGLite;
const child_process_1 = require("child_process");
// ---------------------------------------------------------------------------
// Shell helper
// ---------------------------------------------------------------------------
function run(cmd, args, timeoutMs = 30000) {
return new Promise((resolve, reject) => {
(0, child_process_1.execFile)(cmd, args, { timeout: timeoutMs }, (err, stdout) => {
if (err)
reject(err);
else
resolve(stdout.trim());
});
});
}
// ---------------------------------------------------------------------------
// SearXNG auto-start (colima + Docker)
// ---------------------------------------------------------------------------
/**
* Singleton boot promise.
* Kept as a resolved/rejected promise so concurrent callers share the same
* result instead of each starting their own boot sequence.
* Reset to `null` only BEFORE the next boot attempt, never in `finally`.
*/
let _bootPromise = null;
let _bootDone = false;
async function ensureSearXNG() {
// If a previous boot completed, allow retrying (infrastructure may have stopped)
if (_bootDone) {
_bootPromise = null;
_bootDone = false;
}
if (_bootPromise)
return _bootPromise;
_bootPromise = (async () => {
try {
// 1. Ensure colima VM is running
const status = await run("colima", ["status"]).catch(() => "");
if (!status.includes("Running")) {
await run("colima", ["start", "--cpu", "2", "--memory", "2"], 90000);
}
// 2. Ensure searxng container exists and is started
const ps = await run("docker", ["ps", "-a", "--filter", "name=searxng", "--format", "{{.Status}}"]);
if (!ps)
return false; // container doesn't exist at all
if (!ps.startsWith("Up")) {
await run("docker", ["start", "searxng"], 15000);
}
// 3. Wait for SearXNG to be ready (always check — needs init time)
for (let i = 0; i < 15; i++) {
await new Promise(r => setTimeout(r, 2000));
try {
const resp = await fetch("http://localhost:8080/search?q=ping&format=json", {
signal: AbortSignal.timeout(3000),
});
if (resp.ok)
return true;
}
catch { }
}
return false;
}
catch {
return false;
}
finally {
_bootDone = true;
}
})();
return _bootPromise;
}
// ---------------------------------------------------------------------------
// SearXNG search
// ---------------------------------------------------------------------------
async function searchSearXNG(baseUrl, query, limit = 5) {
try {
const url = `${baseUrl.replace(/\/+$/, "")}/search?q=${encodeURIComponent(query)}&format=json&categories=general&language=auto`;
let resp = await fetch(url, { signal: AbortSignal.timeout(5000) }).catch(() => null);
// If SearXNG is not reachable, try to boot it
if (!resp || !resp.ok) {
const booted = await ensureSearXNG();
if (!booted)
return null;
resp = await fetch(url, { signal: AbortSignal.timeout(8000) }).catch(() => null);
if (!resp || !resp.ok)
return null;
}
const data = await resp.json();
if (!data.results || data.results.length === 0)
return null;
return data.results.slice(0, limit).map((r) => ({
title: r.title || "",
link: r.url || "",
snippet: (r.content || "").substring(0, 300),
}));
}
catch {
return null;
}
}
// ---------------------------------------------------------------------------
// DuckDuckGo Lite search
// ---------------------------------------------------------------------------
const DDG_HEADERS = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7",
};
function decodeHTML(s) {
return s
.replace(/<[^>]+>/g, "")
.replace(/'/g, "'")
.replace(/&/g, "&")
.replace(/"/g, '"')
.replace(/</g, "<")
.replace(/>/g, ">")
.trim();
}
function resolveRedirect(href) {
const m = href.match(/[?&]uddg=([^&]+)/);
return m ? decodeURIComponent(m[1]) : href;
}
async function searchDDGLite(query, limit = 5) {
try {
const resp = await fetch("https://lite.duckduckgo.com/lite/?q=" + encodeURIComponent(query), { headers: DDG_HEADERS });
if (!resp.ok)
return null;
const html = await resp.text();
if (html.includes("anomaly"))
return null;
const results = [];
const allLinks = [
...html.matchAll(/<a[^>]*href="([^"]+)"[^>]*class='result-link'[^>]*>([\s\S]*?)<\/a>/g),
];
const allSnippets = [
...html.matchAll(/<td\s+class='result-snippet'>\s*([\s\S]*?)\s*<\/td>/g),
];
for (let i = 0; i < allLinks.length && results.length < limit; i++) {
const href = allLinks[i][1];
// Filter ads
if (href.includes("y.js") || href.includes("ad_provider"))
continue;
results.push({
title: decodeHTML(allLinks[i][2]),
link: resolveRedirect(href),
snippet: decodeHTML(allSnippets[i]?.[1] || ""),
});
}
return results.length > 0 ? results : null;
}
catch {
return null;
}
}
//# sourceMappingURL=webSearch.js.map