File size: 13,157 Bytes
ab6f137 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 | #!/usr/bin/env node
import { createInterface } from "node:readline/promises";
import { stdin as input, stdout as output } from "node:process";
import { readFile, writeFile } from "node:fs/promises";
import { actionNames, compareResults, investigate, runAction } from "./agent.js";
import { PROJECT, creditsBlock, creditsLine } from "./credits.js";
import { renderHtml, renderMarkdown, renderTerminal } from "./report.js";
import { startApiServer } from "./server.js";
import { startMcpServer } from "./mcp.js";
import { DEFAULT_BENCHMARK, runBenchmark } from "./benchmark.js";
import { listPlugins, loadPlugin } from "./plugins.js";
import { watchUrl } from "./watch.js";
import type { BenchmarkCase } from "./types.js";
function banner(): void {
console.log(`\nโญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ\nโ URL INTELLIGENCE AGENT v${PROJECT.version.padEnd(38)}โ\nโ URL in. Identity, evidence and intelligence out. โ\nโ โ\nโ ${PROJECT.company.padEnd(64)}โ\nโ Founder & Lead Engineer: ${PROJECT.creator.padEnd(39)}โ\nโ HORNO ecosystem: ${PROJECT.website.padEnd(46)}โ\nโฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ\n`);
}
type MenuItem = { n: string; label: string; action?: string; special?: string };
const menuActions: MenuItem[] = [
{ n: "1", label: "Full URL investigation", action: "investigate_url" },
{ n: "2", label: "Safe URL probe / redirects", action: "probe_url" },
{ n: "3", label: "Domain + DNS + mail + TLS intelligence", action: "domain_intelligence" },
{ n: "4", label: "Deep crawl + sitemap mapping", action: "deep_crawl" },
{ n: "5", label: "Render JavaScript page / browser fallback", action: "render_page" },
{ n: "6", label: "Resolve entity + evidence graph", action: "resolve_entity" },
{ n: "7", label: "Find social profiles", action: "find_social_profiles" },
{ n: "8", label: "Find public contacts + people", action: "find_contacts" },
{ n: "9", label: "Technology fingerprinting", action: "detect_technologies" },
{ n: "10", label: "Brand intelligence", action: "brand_intelligence" },
{ n: "11", label: "SEO audit", action: "audit_seo" },
{ n: "12", label: "Security-header posture audit", action: "audit_security" },
{ n: "13", label: "Quality/accessibility/performance audit", action: "audit_quality" },
{ n: "14", label: "Trust/transparency signals", action: "audit_trust" },
{ n: "15", label: "Entity relationship graph", action: "entity_graph" },
{ n: "16", label: "Competitor/comparison intelligence", action: "competitor_intelligence" },
{ n: "17", label: "Structured-data inventory", action: "structured_data" },
{ n: "18", label: "API / OpenAPI / GraphQL discovery", action: "api_discovery" },
{ n: "19", label: "Privacy/compliance public signals", action: "compliance_signals" },
{ n: "20", label: "Team / people extraction", action: "people_team" },
{ n: "21", label: "Commerce / pricing intelligence", action: "commerce_intelligence" },
{ n: "22", label: "Content freshness signals", action: "content_freshness" },
{ n: "23", label: "Link graph / external-domain intelligence", action: "link_intelligence" },
{ n: "24", label: "Broken-link / URL health check", action: "check_links" },
{ n: "25", label: "Generate marketplace/directory listing", action: "generate_listing" },
{ n: "26", label: "RAG-ready document export", action: "rag_export" },
{ n: "27", label: "Knowledge/fact export with provenance", action: "knowledge_export" },
{ n: "28", label: "Generate Markdown + HTML + JSON reports", special: "report" },
{ n: "29", label: "Compare two URLs", special: "compare" },
{ n: "30", label: "Batch enrichment worker", special: "batch" },
{ n: "31", label: "Create monitoring snapshot", action: "create_snapshot" },
{ n: "32", label: "Diff current URL against stored snapshot", action: "diff_snapshot" },
{ n: "33", label: "Continuous scheduled watch + webhook", special: "watch" },
{ n: "34", label: "Optional AI evidence reasoning", action: "ai_reason" },
{ n: "35", label: "Run reliability benchmark", special: "benchmark" },
{ n: "36", label: "Plugins / extension SDK", special: "plugins" },
{ n: "37", label: "HTTP API server", special: "serve" },
{ n: "38", label: "MCP server", special: "mcp" },
{ n: "39", label: "About & credits", special: "about" },
{ n: "0", label: "Exit", special: "exit" }
];
async function interactive(): Promise<void> {
banner(); console.log("Actions"); for (const item of menuActions) console.log(` ${item.n.padStart(2)}. ${item.label}`); console.log(`\n${creditsLine()}\n`);
const rl = createInterface({ input, output });
try {
const choice = await rl.question("Select action: "); const item = menuActions.find(x => x.n === choice); if (!item) throw new Error("Unknown menu choice");
if (item.special === "exit") return;
if (item.special === "about") { console.log("\n" + creditsBlock()); return; }
if (item.special === "serve") { const p = await rl.question("Port [8787]: "); await startApiServer(Number(p || 8787)); return; }
if (item.special === "mcp") { rl.close(); await startMcpServer(); return; }
if (item.special === "benchmark") { console.log(JSON.stringify(await runBenchmark(DEFAULT_BENCHMARK), null, 2)); return; }
if (item.special === "plugins") { console.log(JSON.stringify({ plugins: listPlugins(), load: "url-agent plugin-load <module>", credits: creditsBlock() }, null, 2)); return; }
if (item.special === "batch") { console.log("Use: url-agent batch <urls.txt|urls.json> [--concurrency 4]"); return; }
const url = await rl.question("Public URL: ");
if (item.special === "compare") { const url2 = await rl.question("Second public URL: "); console.log(JSON.stringify(compareResults(await investigate(url), await investigate(url2)), null, 2)); return; }
if (item.special === "report") { const result = await investigate(url); await writeReports(result); console.log(renderTerminal(result)); return; }
if (item.special === "watch") { const mins = Number(await rl.question("Check every N minutes [5]: ") || 5); const controller = new AbortController(); process.once("SIGINT", () => controller.abort()); console.log("Watching. Press Ctrl+C to stop."); await watchUrl(url, { intervalMs: Math.max(1, mins) * 60000, signal: controller.signal, onCheck: event => console.log(JSON.stringify({ ...event, credits: creditsLine() }, null, 2)) }); return; }
const args: Record<string, unknown> = { url };
if (item.action === "ai_reason") args.instruction = await rl.question("Instruction [Analyze evidence]: ") || "Analyze the evidence, summarize the entity, opportunities and uncertainties with source URLs.";
const result = await runAction(item.action || "investigate_url", args as any);
if (item.action === "investigate_url") console.log(renderTerminal(result as any) + "\n\n" + JSON.stringify(result, null, 2)); else console.log(JSON.stringify(result, null, 2));
} finally { rl.close(); }
}
async function writeReports(result: Awaited<ReturnType<typeof investigate>>, prefix = `url-intelligence-${Date.now()}`): Promise<void> {
await Promise.all([writeFile(`${prefix}.md`, renderMarkdown(result)), writeFile(`${prefix}.html`, renderHtml(result)), writeFile(`${prefix}.json`, JSON.stringify(result, null, 2))]);
console.log(`Reports written:\n ${prefix}.md\n ${prefix}.html\n ${prefix}.json\n${creditsLine()}`);
}
function argValue(args: string[], name: string): string | undefined { const i = args.indexOf(name); return i >= 0 ? args[i + 1] : undefined; }
function urlArg(args: string[]): string | undefined { return args.find(x => /^https?:\/\//i.test(x)); }
async function readBatch(path: string): Promise<string[]> { const text = await readFile(path, "utf8"); if (path.endsWith(".json")) { const parsed = JSON.parse(text); if (Array.isArray(parsed)) return parsed.map(String); if (Array.isArray(parsed.urls)) return parsed.urls.map(String); } return text.split(/\r?\n/).map(x => x.trim()).filter(x => /^https?:\/\//i.test(x)); }
async function main(args: string[]): Promise<void> {
const raw = args.includes("--raw"); const clean = args.filter(x => x !== "--raw"); const [cmd = "", ...rest] = clean;
if (!cmd) { await interactive(); return; }
if (cmd === "mcp") { await startMcpServer(); return; }
if (!raw) banner();
if (cmd === "about") { console.log(creditsBlock()); return; }
if (cmd === "actions") { console.log(JSON.stringify({ actions: actionNames(), credits: creditsBlock() }, null, 2)); return; }
if (cmd === "serve") { await startApiServer(Number(argValue(rest, "--port") || 8787), argValue(rest, "--host") || process.env.HOST || "127.0.0.1"); return; }
if (cmd === "plugin-load") { const module = rest[0]; if (!module) throw new Error("plugin-load requires a module path/package"); await loadPlugin(module); console.log(JSON.stringify({ loaded: module, plugins: listPlugins(), credits: creditsBlock() }, null, 2)); return; }
if (cmd === "plugins") { console.log(JSON.stringify({ plugins: listPlugins(), credits: creditsBlock() }, null, 2)); return; }
if (cmd === "benchmark") { const file = rest.find(x => x.endsWith(".json")); const cases = file ? JSON.parse(await readFile(file, "utf8")) as BenchmarkCase[] : DEFAULT_BENCHMARK; console.log(JSON.stringify(await runBenchmark(cases), null, 2)); return; }
if (cmd === "batch") { const file = rest[0]; if (!file) throw new Error("batch requires a text/JSON file containing URLs"); const urls = await readBatch(file); console.log(JSON.stringify(await runAction("batch_investigate", { urls, profile: argValue(rest, "--profile"), concurrency: Number(argValue(rest, "--concurrency") || 4) }), null, 2)); return; }
if (cmd === "compare") { const urls = rest.filter(x => /^https?:\/\//i.test(x)); if (urls.length < 2) throw new Error("compare requires two public URLs"); console.log(JSON.stringify(await runAction("compare_urls", { url: urls[0], url2: urls[1], profile: argValue(rest, "--profile") }), null, 2)); return; }
if (cmd === "watch") { const url = urlArg(rest); if (!url) throw new Error("watch requires a public URL"); const controller = new AbortController(); process.once("SIGINT", () => controller.abort()); const intervalMs = Math.max(60_000, Number(argValue(rest, "--interval") || 300000)); console.log(`Watching ${url} every ${Math.round(intervalMs / 60000)} minute(s). Press Ctrl+C to stop.`); await watchUrl(url, { intervalMs, profile: argValue(rest, "--profile"), webhook: !rest.includes("--no-webhook"), signal: controller.signal, onCheck: event => console.log(JSON.stringify({ ...event, credits: creditsLine() }, null, 2)) }); return; }
const url = urlArg(rest); if (!url) throw new Error("Provide a public http/https URL"); const profile = argValue(rest, "--profile");
if (cmd === "report") { const result = await investigate(url, profile); await writeReports(result, argValue(rest, "--out") || `url-intelligence-${Date.now()}`); console.log(renderTerminal(result)); return; }
const commandMap: Record<string, string> = { investigate: "investigate_url", probe: "probe_url", domain: "domain_intelligence", render: "render_page", "map-site": "map_site", crawl: "deep_crawl", entity: "resolve_entity", socials: "find_social_profiles", contacts: "find_contacts", technologies: "detect_technologies", brand: "brand_intelligence", seo: "audit_seo", security: "audit_security", quality: "audit_quality", trust: "audit_trust", graph: "entity_graph", competitors: "competitor_intelligence", structured: "structured_data", apis: "api_discovery", compliance: "compliance_signals", people: "people_team", commerce: "commerce_intelligence", freshness: "content_freshness", links: "link_intelligence", "check-links": "check_links", listing: "generate_listing", rag: "rag_export", knowledge: "knowledge_export", snapshot: "create_snapshot", diff: "diff_snapshot", reason: "ai_reason" };
const action = commandMap[cmd] || (actionNames().includes(cmd) ? cmd : undefined); if (!action) throw new Error(`Unknown command: ${cmd}. Run 'url-agent actions'.`);
const value = await runAction(action, { url, profile, force: rest.includes("--force"), snapshot: argValue(rest, "--snapshot"), webhook: !rest.includes("--no-webhook"), instruction: argValue(rest, "--instruction"), limit: Number(argValue(rest, "--limit") || 50), concurrency: Number(argValue(rest, "--concurrency") || 5), includeHtml: !rest.includes("--no-html") });
if (action === "investigate_url" && !raw) console.log(renderTerminal(value as any) + "\n"); console.log(JSON.stringify(value, null, 2)); if (!raw) console.log(`\n${creditsLine()}`);
}
main(process.argv.slice(2)).catch(error => { console.error(`ERROR: ${error instanceof Error ? error.message : String(error)}\n${creditsBlock()}`); process.exitCode = 1; });
|