Download phantom.mjs from teddyoas/Asnip: direct link, hf CLI and curl.
- Browser
- Download file 184 kB
-
https://huggingface.co/teddyoas/Asnip/resolve/main/phantom.mjs
- Command line
-
hf download hf://teddyoas/Asnip/phantom.mjs
-
curl -L -o phantom.mjs https://huggingface.co/teddyoas/Asnip/resolve/main/phantom.mjs
184 kB
| // Phantom — space evolving multi-agent terminal | |
| // Zero dependencies. Run: node phantom.mjs | |
| import fs from "fs"; | |
| import http from "http"; | |
| import { homedir, release } from "os"; | |
| import { resolve, join } from "path"; | |
| import { pathToFileURL } from "url"; | |
| import { createRequire } from "module"; | |
| const $r = createRequire(import.meta.url); | |
| import { BASE_DIR, MEMORY_DIR, KNOWLEDGE_DIR, BOOKS_DIR, TOOLS_DIR, REPORTS_DIR, PLAYBOOKS_DIR, PHANTOM_VERSION } from "./lib/config.mjs"; | |
| import { __r, runTool, runPipe, runScheduledScan } from "./lib/runtime.mjs"; | |
| import { log } from "./lib/logger.mjs"; | |
| import { renderLogo, renderBanner, prompt, icons, createSpinner, chatBorder } from "./lib/visual.mjs"; | |
| import { TUI } from "./lib/tui.mjs"; | |
| import { hackerTools } from "./lib/tools.mjs"; | |
| import { initApiDeps, startApiServer, startGuiDashboard, setChatAgent } from "./lib/server.mjs"; | |
| import { autoEvolve, startupEvolve, getEvolveStatus, analyzeError, loadAutoTools } from "./lib/evolve.mjs"; | |
| import { populateEnv, autoInstallSecurity } from "./lib/env.mjs"; | |
| import { saveSession, loadSession, autoLinkFromBooks } from "./lib/session.mjs"; | |
| // ── Merge auto-generated tools into hackerTools ── | |
| // Runs once at module init so all agents & CLIs pick them up. | |
| loadAutoTools().then(at => { | |
| const added = []; | |
| for (const [name, fn] of Object.entries(at)) { | |
| if (!hackerTools[name]) { | |
| hackerTools[name] = fn; | |
| added.push(name); | |
| } | |
| } | |
| if (added.length > 0 && !process.argv.includes("--json")) { | |
| process.stderr.write(`[auto-tools] merged ${added.length} tool(s): ${added.join(", ")}\n`); | |
| } | |
| }).catch(() => {}); | |
| // ── Merge self-improvement learned modules into hackerTools ── | |
| // Generated during improvement cycles, copied to lib/learned/ | |
| const LEARNED_DIR = resolve(new URL(".", import.meta.url).pathname, "lib", "learned"); | |
| (async () => { | |
| try { | |
| if (!fs.existsSync(LEARNED_DIR)) return; | |
| const files = fs.readdirSync(LEARNED_DIR).filter(f => f.endsWith(".mjs")); | |
| if (files.length === 0) return; | |
| let count = 0; | |
| for (const file of files) { | |
| try { | |
| const name = file.replace(/\.mjs$/, ""); | |
| if (hackerTools[name]) continue; // don't override existing | |
| const mod = await import(resolve(LEARNED_DIR, file) + `?t=${Date.now()}`); | |
| const fn = mod.default || mod.execute; | |
| if (typeof fn === "function") { | |
| hackerTools[name] = fn; | |
| count++; | |
| } | |
| } catch {} | |
| } | |
| if (count > 0 && !process.argv.includes("--json")) { | |
| process.stderr.write(`[self-improve] loaded ${count} learned module(s)\n`); | |
| } | |
| } catch {} | |
| })(); | |
| // ── Config ───────────────────────────────────────────────── | |
| let _config = {}; | |
| // Load config from project root first (for USB portability), then from ~/.config/phantom | |
| const PROJECT_ROOT = resolve(new URL(".", import.meta.url).pathname); | |
| const projectConfigPath = resolve(PROJECT_ROOT, "config.json"); | |
| const userConfigPath = resolve(BASE_DIR, "config.json"); | |
| // Deep merge: user config overrides project config | |
| function deepMerge(target, source) { | |
| const result = { ...target }; | |
| for (const key of Object.keys(source)) { | |
| if (source[key] && typeof source[key] === "object" && !Array.isArray(source[key]) && | |
| target[key] && typeof target[key] === "object" && !Array.isArray(target[key])) { | |
| result[key] = deepMerge(target[key], source[key]); | |
| } else { | |
| result[key] = source[key]; | |
| } | |
| } | |
| return result; | |
| } | |
| let projectConfig = {}; | |
| try { | |
| if (fs.existsSync(projectConfigPath)) { | |
| projectConfig = JSON.parse(fs.readFileSync(projectConfigPath, "utf-8")); | |
| } | |
| } catch {} | |
| let userConfig = {}; | |
| try { | |
| if (fs.existsSync(userConfigPath)) { | |
| userConfig = JSON.parse(fs.readFileSync(userConfigPath, "utf-8")); | |
| } | |
| } catch {} | |
| _config = deepMerge(projectConfig, userConfig); | |
| __r._config = _config; | |
| if (_config.VT_API_KEY && !process.env.VT_API_KEY) process.env.VT_API_KEY = _config.VT_API_KEY; | |
| // Load all provider API keys from config | |
| const PROVIDER_KEYS = ["OPENAI_API_KEY","ANTHROPIC_API_KEY","GEMINI_API_KEY","GROQ_API_KEY","DEEPSEEK_API_KEY","MISTRAL_API_KEY","OPENROUTER_API_KEY","SHODAN_API_KEY","HIBP_API_KEY","OPENCODE_ZEN_API_KEY"]; | |
| for (const k of PROVIDER_KEYS) { if (_config[k] && !process.env[k]) process.env[k] = _config[k]; } | |
| // Selected provider: env > config > "openai" | |
| let PHANTOM_LLM_PROVIDER = process.env.PHANTOM_LLM_PROVIDER || _config.default_provider || "openai"; | |
| __r.PHANTOM_LLM_PROVIDER = PHANTOM_LLM_PROVIDER; | |
| function setProvider(name) { PHANTOM_LLM_PROVIDER = name; process.env.PHANTOM_LLM_PROVIDER = name; _config.default_provider = name; __r.PHANTOM_LLM_PROVIDER = name; try { fs.writeFileSync(userConfigPath, JSON.stringify(_config, null, 2)); } catch {} } | |
| __r.setProvider = setProvider; | |
| // LLM instance — set after createProvider() | |
| let llmInstance = null; | |
| function ensureDirs() { | |
| if (!fs.existsSync(MEMORY_DIR)) fs.mkdirSync(MEMORY_DIR, { recursive: true }); | |
| if (!fs.existsSync(KNOWLEDGE_DIR)) fs.mkdirSync(KNOWLEDGE_DIR, { recursive: true }); | |
| if (!fs.existsSync(BOOKS_DIR)) fs.mkdirSync(BOOKS_DIR, { recursive: true }); | |
| if (!fs.existsSync(TOOLS_DIR)) fs.mkdirSync(TOOLS_DIR, { recursive: true }); | |
| } | |
| function saveMemory(name, memory) { | |
| try { | |
| ensureDirs(); | |
| const filePath = resolve(MEMORY_DIR, `${name}.json`); | |
| fs.writeFileSync(filePath, JSON.stringify(memory, null, 2), "utf-8"); | |
| } catch (e) {} | |
| } | |
| function loadMemory(name) { | |
| try { | |
| ensureDirs(); | |
| const filePath = resolve(MEMORY_DIR, `${name}.json`); | |
| if (!fs.existsSync(filePath)) return []; | |
| return JSON.parse(fs.readFileSync(filePath, "utf-8")); | |
| } catch (e) { return []; } | |
| } | |
| // ── Terminal raw mode helpers ───────────────────────── | |
| function saveKnowledge(name, knowledge) { | |
| try { | |
| ensureDirs(); | |
| const filePath = resolve(KNOWLEDGE_DIR, `${name}.txt`); | |
| fs.writeFileSync(filePath, knowledge, "utf-8"); | |
| } catch (e) {} | |
| } | |
| function loadKnowledge(name) { | |
| try { | |
| ensureDirs(); | |
| const filePath = resolve(KNOWLEDGE_DIR, `${name}.txt`); | |
| if (!fs.existsSync(filePath)) return ""; | |
| return fs.readFileSync(filePath, "utf-8"); | |
| } catch { return ""; } | |
| } | |
| // ── Auto-Learning: extract knowledge from tool output ───── | |
| function learnFromTool(tool, result, args) { | |
| if (!result || result.length < 20) return; | |
| try { | |
| // Derive a target name from args | |
| let target = (args || "").replace(/^https?:\/\//, "").split(/[/\s|]/)[0].trim(); | |
| // BUGFIX: was "target.includes('.')" which is always true for domains | |
| if (!target || target.length > 64 || !target.includes(".")) { | |
| // No dot = probably not a domain → use tool name as bucket | |
| target = tool; | |
| } | |
| const sanitized = target.replace(/[^a-zA-Z0-9._-]/g, "_").substring(0, 48); | |
| const tag = `${sanitized}_${tool}`; | |
| const facts = []; | |
| const text = typeof result === "string" ? result : String(result); | |
| // Subdomains (exclude IPs, CVEs) | |
| const subs = text.match(/([a-zA-Z0-9][-a-zA-Z0-9]*\.)+(com|net|org|io|gov|edu|dev|app|xyz|co|uk|ru|cn|de|jp|fr|au|us|in|br|nl|eu)\b/g); | |
| if (subs) { | |
| const uniq = [...new Set(subs.filter(s => !s.startsWith("CVE") && !s.startsWith("cve") && !/^\d/.test(s)))]; | |
| if (uniq.length > 0) facts.push(`subs: ${uniq.slice(0, 30).join(" ")}`); | |
| } | |
| // IPs | |
| const ips = text.match(/\b(?:\d{1,3}\.){3}\d{1,3}\b/g); | |
| if (ips) facts.push(`ips: ${[...new Set(ips)].slice(0, 30).join(" ")}`); | |
| // CVEs | |
| const cves = text.match(/CVE-\d{4}-\d{4,7}/gi); | |
| if (cves) facts.push(`cves: ${[...new Set(cves)].slice(0, 15).join(" ")}`); | |
| // Open ports (e.g. "80/tcp open" or "port 443 open") | |
| const ports = text.match(/\b(\d{1,5})\/(?:tcp|udp)\s+open\b/g); | |
| if (ports) facts.push(`ports: ${ports.map(p => p.split("/")[0]).slice(0, 30).join(" ")}`); | |
| if (facts.length > 0) { | |
| const existing = loadKnowledge(tag); | |
| const newEntry = `[${new Date().toISOString().slice(0, 19)}] ${facts.join("; ")}`; | |
| // Dedup: do not append if identical entry already exists anywhere in the file | |
| if (existing && existing.split("\n").some(l => l.trim() === newEntry)) { | |
| return facts.length; // already saved, skip | |
| } | |
| const combined = existing ? `${existing}\n${newEntry}` : newEntry; | |
| // Keep last 50 entries | |
| const lines = combined.split("\n").slice(-50); | |
| saveKnowledge(tag, lines.join("\n")); | |
| return facts.length; // how many fact types extracted | |
| } | |
| } catch { /* silent — learning is best-effort */ } | |
| return 0; | |
| } | |
| function saveDynamicTool(toolName, code) { | |
| ensureDirs(); | |
| const fileName = `dynamic_${toolName.toLowerCase().replace(/[^a-z0-9_]/g, "_")}.js`; | |
| const filePath = join(TOOLS_DIR, fileName); | |
| let fileContent = code; | |
| if (!code.includes("export async function execute") && !code.includes("export function execute") && !code.includes("export default")) { | |
| fileContent = `export async function execute(input) { ${code} }`; | |
| } | |
| fs.writeFileSync(filePath, fileContent, "utf-8"); | |
| return filePath; | |
| } | |
| // ── Surface-level learned knowledge for system prompt injection ── | |
| function loadLearned() { | |
| try { | |
| ensureDirs(); | |
| const parts = []; | |
| // 1. Books — title + line count only, no content | |
| const bookFiles = fs.existsSync(BOOKS_DIR) ? fs.readdirSync(BOOKS_DIR).filter(f => f.endsWith(".txt") && !f.startsWith("_")) : []; | |
| for (const f of bookFiles.slice(0, 8)) { | |
| const content = fs.readFileSync(resolve(BOOKS_DIR, f), "utf-8").trim(); | |
| if (content) { | |
| const tag = f.replace(/\.txt$/, "").replace(/_/g, " "); | |
| const lines = content.split("\n").length; | |
| const title = content.split("\n")[0]?.replace(/^#+\s*/, "").substring(0, 60) || tag; | |
| parts.push(`📖 ${tag} (${lines} lines — ${title})`); | |
| } | |
| } | |
| // 2. Knowledge findings — file count + last timestamp only | |
| const kFiles = fs.existsSync(KNOWLEDGE_DIR) ? fs.readdirSync(KNOWLEDGE_DIR).filter(f => f.endsWith(".txt")) : []; | |
| if (kFiles.length > 0) { | |
| const total = kFiles.length; | |
| let entries = 0; | |
| for (const f of kFiles) { | |
| const c = fs.readFileSync(resolve(KNOWLEDGE_DIR, f), "utf-8"); | |
| entries += c.split("\n").filter(Boolean).length; | |
| } | |
| parts.push(`🔍 ${total} target files, ${entries} findings total (use @brain to search)`); | |
| } | |
| return parts.join("\n"); | |
| } catch { return ""; } | |
| } | |
| // ── Self-Learning: extract and save techniques from any content ── | |
| let _studyCycle = 0; | |
| function recordTechnique(content, source) { | |
| if (!content || content.length < 80) return; | |
| try { | |
| ensureDirs(); | |
| // Smart sentence split: preserve IPs, CVEs, version numbers | |
| // Split on ". " (period+space) or ".\n" or "!\n" — not on bare "." | |
| const sentences = content | |
| .replace(/\n{2,}/g, ". \n") // paragraph breaks = sentence break | |
| .split(/(?<=[.!])\s+(?=[A-Z0-9#])/) // split on . / ! followed by capital/num | |
| .map(s => s.trim().replace(/^["']|["']$/g, "")) | |
| .filter(s => { | |
| if (s.length < 60 || s.length > 600) return false; | |
| if (s.includes("@") || s.includes("```")) return false; | |
| // Must have actual content — not just a fragment | |
| return /[A-Za-z]{4,}/.test(s); | |
| }) | |
| .slice(0, 8); | |
| if (sentences.length === 0) return; | |
| const tag = `auto_${source.replace(/[^a-z0-9]/gi, "_").substring(0, 20)}`; | |
| const filePath = resolve(BOOKS_DIR, `${tag}.txt`); | |
| const timestamp = new Date().toISOString().slice(0, 19); | |
| const entry = `## Auto-learned: ${source} (${timestamp})\n${sentences.map(s => `- ${s}`).join("\n")}\n\n`; | |
| const existing = fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf-8") : ""; | |
| // Deduplicate: skip if last 3 entries are similar (80%+ overlap) | |
| if (existing) { | |
| const lastLines = existing.split("\n").filter(l => l.startsWith("- ")).slice(-3); | |
| const similar = lastLines.filter(ll => { | |
| const overlap = sentences.filter(s => { | |
| const words = new Set(s.toLowerCase().split(/\s+/)); | |
| const lw = new Set(ll.toLowerCase().split(/\s+/)); | |
| const common = [...words].filter(w => lw.has(w)).length; | |
| return common > 3 && common / Math.max(words.size, 1) > 0.6; | |
| }); | |
| return overlap.length > 0; | |
| }); | |
| if (similar.length >= 2) return; // too similar to last entries, skip | |
| } | |
| const combined = existing ? `${existing}${entry}` : entry; | |
| // Keep last 20 entries max per file | |
| const parts = combined.split("## Auto-learned:"); | |
| const kept = parts.slice(-20); | |
| fs.writeFileSync(filePath, kept.join("## Auto-learned:").trimStart(), "utf-8"); | |
| } catch { /* best-effort */ } | |
| } | |
| async function consolidateKnowledge() { | |
| try { | |
| ensureDirs(); | |
| // Read all knowledge files and build a meaningful summary | |
| const kFiles = fs.existsSync(KNOWLEDGE_DIR) ? fs.readdirSync(KNOWLEDGE_DIR).filter(f => f.endsWith(".txt")) : []; | |
| if (kFiles.length === 0) return; | |
| // Build a data-rich synthesis: actual findings, not just counts | |
| const summary = [`## Knowledge Synthesis (${new Date().toISOString().slice(0, 10)})`]; | |
| let totalEntries = 0; | |
| for (const f of kFiles.slice(0, 10)) { | |
| const content = fs.readFileSync(resolve(KNOWLEDGE_DIR, f), "utf-8"); | |
| const entries = content.split("\n").filter(Boolean); | |
| totalEntries += entries.length; | |
| const tag = f.replace(".txt", "").replace(/_/g, " "); | |
| // Show the 2 most recent entries with actual data | |
| const recent = entries.slice(-2); | |
| summary.push(`\n### ${tag} (${entries.length} total)`); | |
| for (const e of recent) summary.push(` ${e}`); | |
| } | |
| summary.push(`\n📊 ${totalEntries} total findings across ${kFiles.length} targets`); | |
| if (kFiles.length > 10) summary.push(`(${kFiles.length - 10} more targets not shown)`); | |
| const synthesisFile = resolve(BOOKS_DIR, "_synthesis.txt"); | |
| fs.writeFileSync(synthesisFile, summary.join("\n"), "utf-8"); | |
| return summary.join("\n"); | |
| } catch { return ""; } | |
| } | |
| async function studyCycle(self) { | |
| _studyCycle = (_studyCycle || 0) + 1; | |
| // Every 5th response: consolidation (silent unless first time) | |
| if (_studyCycle % 5 === 0) { | |
| await consolidateKnowledge(); | |
| if (_studyCycle === 5) { | |
| const kCount = fs.existsSync(KNOWLEDGE_DIR) ? fs.readdirSync(KNOWLEDGE_DIR).filter(f => f.endsWith(".txt")).length : 0; | |
| const bCount = fs.existsSync(BOOKS_DIR) ? fs.readdirSync(BOOKS_DIR).filter(f => f.endsWith(".txt")).length : 0; | |
| console.log(` ${c("dim")} ${kCount} knowledge · ${bCount} books${R}`); | |
| } | |
| } | |
| // Every 10th response: save session summary | |
| if (_studyCycle % 10 === 0 && self._growFacts > 0) { | |
| recordTechnique(`Self-learned ${self._growFacts} facts this session.`, "session_summary"); | |
| } | |
| } | |
| async function loadDynamicTool(filePath, toolName, description) { | |
| const fileUrl = pathToFileURL(filePath).href; | |
| const module = await import(`${fileUrl}?t=${Date.now()}`); | |
| const executeFn = module.execute || module.default; | |
| if (typeof executeFn !== "function") { | |
| throw new Error(`Dynamic tool ${toolName} does not export 'execute' function.`); | |
| } | |
| return { | |
| name: toolName, | |
| description, | |
| execute: async (input, agentCtx) => { | |
| try { | |
| return String(await executeFn(input, agentCtx)); | |
| } catch (err) { | |
| return `[Tool Error in ${toolName}]: ${err.message}`; | |
| } | |
| } | |
| }; | |
| } | |
| async function loadAllDynamicTools() { | |
| ensureDirs(); | |
| try { | |
| const files = fs.readdirSync(TOOLS_DIR); | |
| const tools = []; | |
| for (const file of files) { | |
| if (file.startsWith("dynamic_") && file.endsWith(".js")) { | |
| const filePath = join(TOOLS_DIR, file); | |
| const name = file.replace("dynamic_", "").replace(".js", ""); | |
| tools.push({ name, description: `Dynamic tool ${name}`, filePath }); | |
| } | |
| } | |
| return tools; | |
| } catch (e) { return []; } | |
| } | |
| // ── Hacker Tools ────────────────────────────────────────── | |
| // hackerTools moved to lib/tools.mjs | |
| // ── EventBus ────────────────────────────────────────────── | |
| class EventBus { | |
| static i = new EventBus(); | |
| #h = new Map(); | |
| on(e, fn) { if (!this.#h.has(e)) this.#h.set(e, []); this.#h.get(e).push(fn); } | |
| off(e, fn) { const h = this.#h.get(e); if (h) this.#h.set(e, h.filter(f => f !== fn)); } | |
| emit(e, d) { this.#h.get(e)?.forEach(fn => fn(d)); } | |
| } | |
| // ── Agent Types ─────────────────────────────────────────── | |
| const ARCHETYPES = [ | |
| { name: "Lyra", role: "coordinator", | |
| persona: "Strategic orchestrator who breaks down complex security tasks, delegates to specialists, and synthesizes results into coherent reports. Controls the big picture." }, | |
| { name: "Nova", role: "recon", | |
| persona: "OSINT and reconnaissance specialist. Expert in DNS analysis, subdomain enumeration, port scanning, WHOIS lookups, web crawling, and attack surface mapping. Discovers the target's digital footprint." }, | |
| { name: "Orion", role: "exploit", | |
| persona: "Vulnerability analysis and exploitation engineer. Expert in CVE research, exploit matching, brute force testing, SQL injection, XSS, and penetration testing. Finds and validates security holes." }, | |
| { name: "Vega", role: "defense", | |
| persona: "Defensive security and monitoring analyst. Expert in log analysis, SSL/TLS audit, CORS testing, JWT analysis, certificate checks, and security hardening. Identifies misconfigurations and weaknesses." }, | |
| { name: "Atlas", role: "researcher", | |
| persona: "Deep knowledge explorer who searches Shodan, VirusTotal, HIBP, GitHub dorks, and public databases for threat intelligence. Gathers context and verifies security findings." }, | |
| { name: "Helios", role: "debugger", | |
| persona: "Systematic problem solver who traces code issues, analyzes files, decodes obfuscated data, and reverse-engineers payloads. Finds root causes of security issues." }, | |
| { name: "Selene", role: "reporter", | |
| persona: "Technical writer and documentation specialist. Creates comprehensive security reports, playbooks, and knowledge base entries. Transforms raw findings into actionable intelligence." }, | |
| { name: "Aether", role: "automator", | |
| persona: "Automation and optimization engineer. Writes scripts, creates pipelines, builds playbooks, and streamlines repetitive security workflows. Makes everything faster and repeatable." }, | |
| ]; | |
| const AGENT_COLORS = [ | |
| [0, 255, 136], [0, 204, 255], [255, 0, 204], [255, 136, 0], | |
| [136, 0, 255], [0, 255, 204], [255, 0, 102], [102, 255, 0], | |
| ]; | |
| let idCounter = 0; | |
| const genId = () => `PH-${(++idCounter).toString(36).toUpperCase().padStart(4, "0")}`; | |
| // ── LLM Provider ────────────────────────────────────────── | |
| function createProvider() { | |
| const PROVIDERS = { | |
| openai: { url: "https://opencode.ai/zen/v1", keyEnv: "OPENCODE_ZEN_API_KEY", defaultModel: "nemotron-3-ultra-free", chatPath: "/chat/completions", fmt: o => ({ model: o.model, messages: o.messages, temperature: 0.7, max_tokens: 16384 }), parse: d => { const c = d.choices?.[0]?.message?.content?.trim(); return c || (d.choices?.[0]?.finish_reason === "length" ? "[Response truncated — increase max_tokens]" : "…"); }, auth: k => ({ "Authorization": `Bearer ${k}` }) }, | |
| anthropic: { url: "https://api.anthropic.com/v1", keyEnv: "ANTHROPIC_API_KEY", defaultModel: "claude-sonnet-4-20250514", chatPath: "/messages", fmt: o => ({ model: o.model, messages: o.messages, max_tokens: 512 }), parse: d => d.content?.[0]?.text || d.content?.toString() || "...", auth: k => ({ "x-api-key": k, "anthropic-version": "2023-06-01" }) }, | |
| gemini: { url: "https://generativelanguage.googleapis.com/v1beta", keyEnv: "GEMINI_API_KEY", defaultModel: "gemini-2.0-flash", chatPath: "/models/{model}:generateContent", fmt: o => ({ contents: o.messages.map(m => ({ role: m.role === "assistant" ? "model" : m.role, parts: [{ text: m.content }] })) }), parse: d => d.candidates?.[0]?.content?.parts?.[0]?.text || "...", auth: () => ({}), urlMod: (u, m, k) => `${u}${m}?key=${k}` }, | |
| groq: { url: "https://api.groq.com/openai/v1", keyEnv: "GROQ_API_KEY", defaultModel: "llama-3.3-70b-versatile", chatPath: "/chat/completions", fmt: o => ({ model: o.model, messages: o.messages, temperature: 0.7, max_tokens: 512 }), parse: d => d.choices?.[0]?.message?.content?.trim() || "...", auth: k => ({ "Authorization": `Bearer ${k}` }) }, | |
| deepseek: { url: "https://api.deepseek.com/v1", keyEnv: "DEEPSEEK_API_KEY", defaultModel: "deepseek-chat", chatPath: "/chat/completions", fmt: o => ({ model: o.model, messages: o.messages, temperature: 0.7, max_tokens: 512 }), parse: d => d.choices?.[0]?.message?.content?.trim() || "...", auth: k => ({ "Authorization": `Bearer ${k}` }) }, | |
| mistral: { url: "https://api.mistral.ai/v1", keyEnv: "MISTRAL_API_KEY", defaultModel: "mistral-large-latest", chatPath: "/chat/completions", fmt: o => ({ model: o.model, messages: o.messages, temperature: 0.7, max_tokens: 512 }), parse: d => d.choices?.[0]?.message?.content?.trim() || "...", auth: k => ({ "Authorization": `Bearer ${k}` }) }, | |
| openrouter: { url: "https://openrouter.ai/api/v1", keyEnv: "OPENROUTER_API_KEY", defaultModel: "anthropic/claude-sonnet-4", chatPath: "/chat/completions", fmt: o => ({ model: o.model, messages: o.messages, temperature: 0.7, max_tokens: 512 }), parse: d => d.choices?.[0]?.message?.content?.trim() || "...", auth: k => ({ "Authorization": `Bearer ${k}` }) }, | |
| ollama: { url: process.env.OLLAMA_HOST || "http://localhost:11434", keyEnv: "", defaultModel: "llama3", chatPath: "/api/chat", fmt: o => ({ model: o.model, messages: o.messages, stream: false }), parse: d => d.message?.content?.trim() || "...", auth: () => ({}) }, | |
| opencode: { url: "https://opencode.ai/zen/v1", keyEnv: "OPENCODE_ZEN_API_KEY", defaultModel: "nemotron-3-ultra-free", chatPath: "/chat/completions", fmt: o => ({ model: o.model, messages: o.messages, temperature: 0.7, max_tokens: 256 }), parse: d => { const c = d.choices?.[0]?.message?.content?.trim(); return c || (d.choices?.[0]?.finish_reason === "length" ? "[Response truncated — increase max_tokens or shorten context]" : "…"); }, auth: k => ({ "Authorization": `Bearer ${k}` }) }, | |
| }; | |
| __r.PROVIDERS = PROVIDERS; | |
| function getProvider() { | |
| const name = PHANTOM_LLM_PROVIDER || "openai"; | |
| const p = PROVIDERS[name]; | |
| if (!p) return PROVIDERS.openai; // fallback | |
| return p; | |
| } | |
| function getKey(p) { | |
| if (p.keyEnv) { | |
| const k = process.env[p.keyEnv]; | |
| if (k) return k; | |
| // fallback: check config | |
| if (_config[p.keyEnv]) return _config[p.keyEnv]; | |
| } | |
| return ""; | |
| } | |
| async function detectProviders() { | |
| const available = {}; | |
| for (const [name, p] of Object.entries(PROVIDERS)) { | |
| if (name === "ollama") { | |
| try { | |
| const r = await fetch(`${p.url}/api/tags`, { signal: AbortSignal.timeout(3000) }); | |
| available[name] = r.ok ? "local" : "no"; | |
| } catch { available[name] = "no"; } | |
| } else { | |
| available[name] = p.keyEnv && getKey(p) ? "key" : "no"; | |
| } | |
| } | |
| return available; | |
| } | |
| function selectBest(avail) { | |
| const order = ["openai", "ollama", "opencode", "anthropic", "groq", "gemini", "deepseek", "mistral", "openrouter"]; | |
| for (const name of order) { | |
| if (avail[name] && avail[name] !== "no") return name; | |
| } | |
| return null; | |
| } | |
| return { | |
| get provider() { return PHANTOM_LLM_PROVIDER; }, | |
| set provider(name) { if (PROVIDERS[name]) setProvider(name); }, | |
| get providers() { return Object.keys(PROVIDERS); }, | |
| detectProviders, | |
| selectBest, | |
| get hasLLM() { | |
| const p = getProvider(); | |
| return !!(p.keyEnv ? getKey(p) : p === PROVIDERS.ollama); | |
| }, | |
| chat: async function(messages, opts = {}) { | |
| const p = getProvider(); | |
| const key = getKey(p); | |
| if (p.keyEnv && !key) return `[${PHANTOM_LLM_PROVIDER}] No API key. Set ${p.keyEnv} env or in config.json`; | |
| const model = opts.model || p.defaultModel; | |
| // Fallback chain on rate limits / errors | |
| const fallbackOrder = ["openai", "anthropic", "groq", "gemini", "deepseek", "mistral", "openrouter", "opencode"]; | |
| let currentProvider = PHANTOM_LLM_PROVIDER; | |
| let lastError = ""; | |
| for (const providerName of fallbackOrder) { | |
| const provider = PROVIDERS[providerName]; | |
| if (!provider) continue; | |
| const providerKey = getKey(provider); | |
| if (provider.keyEnv && !providerKey && providerName !== "ollama") continue; | |
| try { | |
| let url = `${provider.url}${provider.chatPath.replace("{model}", model)}`; | |
| const headers = { "Content-Type": "application/json", ...provider.auth(providerKey) }; | |
| if (provider.urlMod) url = provider.urlMod(url, provider.chatPath.replace("{model}", model), providerKey); | |
| const body = JSON.stringify(provider.fmt({ model, messages })); | |
| const r = await fetch(url, { method: "POST", headers, body, signal: AbortSignal.timeout(60000) }); | |
| if (!r.ok) { | |
| const t = await r.text().catch(() => ""); | |
| lastError = `[${providerName} ${r.status}] ${t.substring(0, 200)}`; | |
| // Fallback on rate limit or service errors | |
| if (r.status === 429 || r.status === 503 || r.status >= 500) { | |
| console.log(`[LLM Fallback] ${providerName} returned ${r.status}, trying next provider...`); | |
| continue; // Try next provider | |
| } | |
| return lastError; | |
| } | |
| const d = await r.json(); | |
| const result = provider.parse(d) || "..."; | |
| if (providerName !== currentProvider) { | |
| console.log(`[LLM Fallback] Switched to ${providerName}`); | |
| } | |
| return result; | |
| } catch (e) { | |
| lastError = `[${providerName} err] ${e.message}`; | |
| console.log(`[LLM Fallback] ${providerName} error: ${e.message}, trying next...`); | |
| continue; | |
| } | |
| } | |
| return `[LLM Fallback] All providers exhausted. Last error: ${lastError}`; | |
| }, | |
| async transcribe(filePath) { | |
| const key = process.env.OPENCODE_ZEN_API_KEY; | |
| if (!key) return "[Transcribe] Set OPENCODE_ZEN_API_KEY"; | |
| try { | |
| const buf = fs.readFileSync(filePath); | |
| const blob = new Blob([buf], { type: "audio/mpeg" }); | |
| const fd = new FormData(); fd.append("file", blob, "audio.mp3"); fd.append("model", "whisper-1"); | |
| const r = await fetch("https://api.openai.com/v1/audio/transcriptions", { method: "POST", headers: { "Authorization": `Bearer ${key}` }, body: fd }); | |
| const d = await r.json(); | |
| return d.text || "[empty]"; | |
| } catch (e) { return `[Transcribe err] ${e.message}`; } | |
| } | |
| }; | |
| } | |
| // ── Agent ───────────────────────────────────────────────── | |
| class Agent { | |
| constructor(name, role, persona, llm, manager) { | |
| const ac = AGENT_COLORS[idCounter % AGENT_COLORS.length]; | |
| this.id = genId(); | |
| this.name = name; | |
| this.role = role; | |
| this.persona = persona; | |
| this.status = "idle"; | |
| this.color = ac; | |
| this.evolutionLevel = 1; | |
| this.memory = []; | |
| this.caps = []; // capabilities (keyword-matched) | |
| this.tools = {}; // hacker tools (LLM-driven) | |
| this.llm = llm; | |
| this.bus = EventBus.i; | |
| this.manager = manager; // AgentManager reference for delegation | |
| // Load persisted memory | |
| const slug = this.name.toLowerCase().replace(/[^a-z0-9]/g, "_"); | |
| try { this.memory = loadMemory(slug) || []; } catch {} | |
| this._slug = slug; | |
| // Register default hacker tools | |
| this.registerHackerTools(); | |
| } | |
| registerHackerTools() { | |
| const toolList = { | |
| delegate: "Delegate a task to another agent. Format: agent_name|task_description. Agents: Lyra (coordinator), Nova (recon), Orion (exploit), Vega (defense). Use when you need specialized expertise.", | |
| fanout: "Fan-out same task to multiple agents in parallel. Format: agent1,agent2|task. Runs them simultaneously.", | |
| synthesize: "Ask coordinator to merge results. Format: original_request|raw_results.", | |
| parallel: "Auto-parallelize a task: splits complex tasks into independent subtasks and runs them concurrently on Nova (recon), Orion (exploit), Vega (defense). Format: task_description. Use for: running recon+exploit+defense in parallel, scanning multiple targets, multi-pronged approaches.", | |
| grow: "Deep learn from session knowledge. Reads all auto-extracted facts from tool outputs and reports a summary of what Phantom has learned. Use: @grow to see your knowledge base. Run after several tools to consolidate learning.", | |
| learn_book: "Learn from a book or documentation file. Format: <file_path>|<description>. Reads the file, extracts techniques, stores them permanently. Every future session uses this knowledge.", | |
| learn_url: "Learn from a URL (online docs, tutorials, articles). Format: <url>|<description>. Fetches, extracts knowledge, stores permanently.", | |
| study: "Trigger self-learning report. Shows all book/technique files, knowledge base, and confirms injected knowledge. Use: @study to see what Phantom has learned.", | |
| workspace_write: "Write to shared workspace (all agents can see). Format: key|value.", | |
| workspace_read: "Read from shared workspace. Format: key.", | |
| shell: "Execute ANY shell command on the system. Use for: running tools, scripts, file operations, network scans, system info, package management. Input: shell command string.", | |
| web_search: "Search the internet via DuckDuckGo + Wikipedia. Use for: finding information, researching topics, checking current events, finding documentation. Input: search query.", | |
| web_fetch: "Fetch a URL and return its content (HTML stripped, plain text). Use for: reading web pages, APIs, documentation, checking endpoints. Input: full URL including https://.", | |
| decode: "Auto-detect and decode encoded strings. Tries base64, hex, URL encoding, binary, and ROT13. Use for: decoding obfuscated strings, payloads, and encoded data. Input: the encoded string.", | |
| file_analyze: "Deep file analysis: file type detection by magic bytes, MD5/SHA1/SHA256 hashes, entropy calculation (detects packed/encrypted malware), and printable string extraction. Use for: malware analysis, file forensics, verifying file integrity. Input: absolute file path.", | |
| dns_lookup: "DNS reconnaissance: resolves A, AAAA, MX, NS, TXT, CNAME, and SOA records for a domain. Use for: OSINT, domain recon, infrastructure discovery. Input: domain name (no http://).", | |
| hash: "Compute MD5, SHA1, SHA256 hash of text or a file. Use for: integrity checks, verifying downloads, fingerprinting. Input: text string or file path.", | |
| whois: "WHOIS lookup for a domain. Returns registrar, dates, name servers, contact info. Use for: OSINT, domain investigation. Input: domain name (no http://).", | |
| port_scan: "TCP port scan a host. Scans 30+ common ports by default. Custom: host:port1,port2 or host:start-end. Use for: network recon, vuln assessment. Input: hostname/IP, optionally with port range.", | |
| http_headers: "Fetch HTTP response headers via HEAD request. Shows status, security headers, server info. Use for: web recon, security audit. Input: full URL including https://.", | |
| ssl_check: "Check SSL/TLS certificate: issuer, validity, cipher, SANs, expiry. Use for: cert monitoring, TLS audit. Input: hostname (optionally :port, default 443).", | |
| sub_enum: "Enumerate subdomains via crt.sh certificate transparency logs. Use for: OSINT, attack surface mapping. Input: domain name (no http://).", | |
| crawl: "Crawl a web page: extract all links, forms, and script sources. Use for: web recon, asset discovery. Input: full URL including https://.", | |
| vt_check: "Check file hash against VirusTotal. Shows detection ratio and top malicious engines. Requires VT_API_KEY env var. Input: MD5/SHA1/SHA256 hash.", | |
| yara: "Scan file with YARA rules. Format: rules_file|target, or just target. Use for: malware pattern matching, IOC scanning. Requires yara CLI. Input: rules_and_target.", | |
| recon: "FULL AUTOMATED RECON: runs WHOIS → DNS → subdomains → HTTP headers → SSL → port scan → crawl. Saves timestamped report. Use for: one-shot surface mapping. Input: domain or URL.", | |
| cve_search: "Search NVD (National Vulnerability Database) for CVEs. Shows CVE ID, CVSS score, severity, description. Use for: finding known vulns for software/version. Input: search query (e.g. 'apache 2.4.49').", | |
| searchsploit: "Search for public exploits. Tries local searchsploit CLI, falls back to packetstorm. Use for: finding PoC exploits. Input: search query (e.g. 'WordPress 5.8').", | |
| bruteforce: "Multi-protocol brute force login. Supports SSH, FTP, HTTP POST, MySQL. Format: protocol|target|user|pass1,pass2,pass3. Use for: password testing, credential auditing.", | |
| file_read: "Read the contents of any file. Use for: viewing source code, configs, logs. Input: absolute or relative file path.", | |
| file_write: "Write content to a file (creates or overwrites). Creates parent directories automatically. Use for: saving code, scripts, configs. Format: path|content.", | |
| file_edit: "Find and replace text in a file. Use for: patching code, modifying configs. Format: path|old_string|new_string.", | |
| file_search: "Search file contents for a text pattern (uses ripgrep). Format: [path|]pattern.", | |
| file_list: "List files and directories with sizes. Use for: exploring structure, finding files. Input: directory path.", | |
| self_info: "Show Phantom's version, all tools, runtime, platform, LLM status. Use for: self-awareness, debugging.", | |
| self_read: "Read Phantom's own source files (project-locked). Input: relative path like 'phantom.mjs' or 'src/core/hacker-tools.ts'.", | |
| self_edit: "Edit Phantom's own source code (project-locked). Format: relative_path|old_string|new_string.", | |
| vuln_scan: "FULL VULNERABILITY SCAN: recon → CVE search → exploit search → brute force → comprehensive markdown report. Input: domain or URL.", | |
| report_save: "Save text as a timestamped markdown report. Format: name|content. Saves to ~/.config/phantom/reports/.", | |
| session_save: "Save current Phantom session state. Input: session name (alphanumeric).", | |
| session_load: "Load a saved Phantom session. Input: session name.", | |
| code_gen: "Generate code via OpenAI LLM (requires OPENAI_API_KEY). Format: prompt|language|output_path.", | |
| self_add_tool: "Auto-generate AND auto-integrate a new tool via LLM. Modifies both TS and MJS source, rebuilds, rolls back on failure. Input: tool description.", | |
| knowledge_add: "Add an entry to Phantom's persistent knowledge base. Format: tag1,tag2|content.", | |
| knowledge_search: "Search Phantom's knowledge base by tag or keyword. Input: search query.", | |
| brain: "Search Phantom's entire learned knowledge: books, knowledge findings, and auto-extracted techniques. For use when you need to recall what you learned across sessions. Format: search query or tag.", | |
| hackbook: "Zero-day vulnerability taxonomy and security reference. Browse topics: @hackbook or search: @hackbook|buffer overflow, @hackbook|UAF, @hackbook|CVE-2025, etc.", | |
| playbook_create: "Create a new multi-step automation playbook. Uses LLM if OPENAI_API_KEY set. Input: name|description.", | |
| playbook_list: "List all available playbooks (built-in + custom).", | |
| playbook_run: "Execute a playbook against a target. Input: name|target=example.com.", | |
| playbook_edit: "Edit a playbook: edit steps, update description, or append steps.", | |
| geoip: "IP geolocation lookup. Shows country, region, city, ISP, ASN. Input: IP or domain.", | |
| dns_zone: "Test DNS zone transfer (AXFR) on all name servers. Input: domain.", | |
| http_methods: "Fuzz HTTP methods on a URL. Tests GET, POST, PUT, DELETE, etc. Input: URL.", | |
| robots_txt: "Fetch and analyze robots.txt. Shows rules, disallowed paths, sitemaps. Input: domain or URL.", | |
| email_verify: "Validate email format and check domain MX records. Input: email address.", | |
| reverse_dns: "Reverse DNS lookup (PTR record) for an IP address. Input: IP.", | |
| wayback: "Query Wayback Machine for historical URL snapshots. Input: domain or URL.", | |
| cert_expiry: "Check SSL certificate expiry date and days remaining. Input: domain.", | |
| cors_test: "Test for CORS misconfigurations with various Origin headers. Input: URL.", | |
| jwt_decode: "Decode JWT token header and payload without verification. Input: JWT string.", | |
| hash_crack: "Look up MD5 hash in online rainbow tables. Input: MD5 hash.", | |
| dir_bruteforce: "Web directory brute force: probes 30+ common paths. Input: URL or domain.", | |
| xss_scan: "Cross-Site Scripting scanner: injects XSS payloads, checks reflection. Input: URL.", | |
| sql_detect: "SQL Injection detection: sends SQLi payloads, checks error signatures. Input: URL.", | |
| open_redirect: "Open redirect scanner: tests 15 common redirect params. Input: URL.", | |
| shodan_search: "Search Shodan for connected devices. Requires SHODAN_API_KEY. Input: query.", | |
| email_breach: "Check email in known data breaches via HIBP API. Requires HIBP_API_KEY. Input: email.", | |
| github_dork: "Search GitHub code for secrets and keys. Input: search query.", | |
| sub_takeover: "Check subdomain for CNAME takeover (AWS, GitHub, Heroku, etc.). Input: domain.", | |
| js_analyze: "Deep JS bundle analysis — find API keys, secrets, API endpoints, XSS sinks. Input: URL to JS file.", | |
| upload_test: "Test file upload endpoints for XXE, YAML deserialization, polyglot. Format: url|mode (xxe/polyglot/all).", | |
| rate_limit_test: "Test rate limiting, timing attacks, race conditions. Format: url|mode|count. Modes: burst/timing/race/all.", | |
| env: "Show detected environment: OS, tools, packages, network, resources.", | |
| batch: "Run multiple tool calls sequentially (one per line, lines starting with # ignored). Bypasses agent loop for fast multi-step tasks.", | |
| install_missing: "Auto-install missing security tools via package manager. Options: tool name or 'all'.", | |
| rollback: "Save/restore git state for safe self-evolution. Commands: save | restore | status.", | |
| self_integrate: "Auto-integrate a generated .mjs module into hackerTools + registry. Input: path to .mjs file.", | |
| graph: "Query knowledge graph: tool ↔ books/CVEs/tags. Empty input shows all links.", | |
| cron: "Schedule/manage periodic tasks: cve_now (run now), cve_daily (daily CVE scan).", | |
| browser_auto: "Launch headless Playwright browser, load URL, return rendered HTML. Supports --screenshot, --html, --text flags. Input: URL [flags].", | |
| plugin_load: "Load external plugin tools from plugins directory. Input: optional path.", | |
| plugin_create: "Create a new plugin skeleton. Format: name|description.", | |
| report_export: "Export report to styled HTML (Ctrl+P → PDF). Input: report name.", | |
| distro: "Show current Linux distro info and manage proot-distro environments. Commands: info, list, run <name> <cmd>.", | |
| llm_config: "Configure LLM provider: list, switch, set API keys. Usage: list | <provider> | set KEY value | model name.", | |
| youtube_summarize: "Download YouTube video transcript, summarize via LLM, and save as playbook. Format: youtube_url. Requires yt-dlp.", | |
| hackbook: "Vulnerability learning database — search by type (sql-injection, xss, csrf, ssrf, rce, lfi, idor, etc.). Shows description, impact, testing, mitigation, tools. Format: search term. Use 'list' for all categories.", | |
| code_analyze: "Deep source code security analysis. Scans files for OWASP Top 10, hardcoded secrets, insecure patterns. Generates report + optional LLM analysis. Format: file or directory path.", | |
| self_improve: "Analyze Phantom's own source and suggest improvements. Scans phantom.mjs for code quality, performance, security, feature gaps. Format: optional focus (performance|security|features|all).", | |
| self_evolve: "Run full auto-evolution pipeline: detect missing tool wrappers, fix syntax, optimize, validate. Also auto-analyzes tool errors. Format: optional 'status' to view evolution state.", | |
| install: "Auto-detect package manager and install security tools. Format: tool_name (nmap, sqlmap, metasploit, searchsploit, ffuf, hydra, john, gobuster, nikto, wireshark, burpsuite, etc.). Detects apt/pkg/brew/pip.", | |
| update: "Self-update Phantom via git pull. Checks for updates, pulls latest code, re-verifies syntax, reports diff. Format: optional 'force' to skip confirmation.", | |
| batch: "Multi-target batch processing. Format: file_path|tool_name. Reads targets from a file (one per line) and runs the specified tool against each. Aggregates results into one report.", | |
| schedule: "Schedule recurring scans. Format: interval|tool|target. Interval: 'daily', 'hourly', '30m', or cron expression. Example: @schedule|daily|recon|example.com", | |
| agent_memory: "View or manage agent conversation memory. Format: list | <agent_name> | <agent_name> clear. Agents: Lyra, Nova, Orion, Vega, Atlas, Helios, Selene, Aether.", | |
| fuzz: "Web fuzzing engine. Discovers hidden paths, parameters, and files via concurrent HTTP requests with a built-in common wordlist. Format: url|wordlist_type (common, admin, backup, params, php, asp, jsp, custom:path.txt). Shows status, size, redirects. Input: https://target.com/FUZZ or https://target.com?param=FUZZ", | |
| pwn: "Auto-exploit chain. Runs full recon, finds CVEs, searches exploit-db, and generates an exploit plan. Format: target|optional_port. Chains: recon → CVE search → exploit lookup → metasploit resource generation.", | |
| web_click: "Navigate and click web page elements by index, link text, or URL. Input: url|selector|method (index/text/selector).", | |
| web_links: "Extract and categorize all links from a page (internal/external/resources). Input: URL.", | |
| web_form: "Extract HTML forms and submit with custom field values. Input: url|field1=val1|field2=val2.", | |
| web_snapshot: "Get structured text snapshot of a page: headings, meta, links, content. Input: URL.", | |
| project_create: "Create a new project workspace. Stores metadata in ~/.config/phantom/projects/. Input: project name.", | |
| project_list: "List all projects with file/note counts and age. Input: none.", | |
| project_info: "Show project details: created date, files, notes, tools used. Input: project name.", | |
| project_file_add: "Add a file to a project by copying it into the project directory. Input: project|filepath.", | |
| project_note: "Add or list project notes. Format: project|note_text (or just project name to list). Input: project|note.", | |
| project_switch: "Set active project for context. Input: project name.", | |
| scope: "Manage authorized target scope for bug bounty / pentesting. Commands: add <target>, remove <target|#id>, check <target>, clear, export. Use 'scope' alone to list all scoped targets. Input: command + args.", | |
| katana: "External: ProjectDiscovery katana — fast web crawler for URL discovery. Supports depth, rate-limit, JS extraction. Input: url [options]. Requires katana binary installed.", | |
| subfinder: "External: ProjectDiscovery subfinder — passive subdomain enumeration using multiple sources (crt.sh, certspotter, hackertarget, etc.). Input: domain [options]. Requires subfinder binary.", | |
| ffuf: "External: ffuf — blazing-fast web fuzzer by Joohansson. Discovers hidden paths, parameters, vhosts. Supports built-in wordlists. Input: ffuf args. Requires ffuf binary.", | |
| httpx: "External: ProjectDiscovery httpx — probe for alive web servers. Detects status, title, tech stack, IP. Input: domain or file. Requires httpx binary.", | |
| nuclei: "External: ProjectDiscovery nuclei — template-based vulnerability scanner. Scans CVEs, exposures, misconfigs. Input: URL [options]. Requires nuclei binary.", | |
| amass: "External: OWASP amass — thorough subdomain enumeration. Modes: enum, intel, db. Input: domain [mode] [options]. Requires amass binary.", | |
| gau: "External: getallurls (lc/gau) — fetch known URLs from Wayback/AlienVault/CommonCrawl. Input: domain. Requires gau binary.", | |
| dnsx: "External: ProjectDiscovery dnsx — DNS resolution toolkit. Multi-record type resolver. Input: domain. Requires dnsx binary.", | |
| gitleaks: "External: gitleaks — git repository secret scanner. Detects accidental credential commits. Input: repo path. Requires gitleaks binary.", | |
| s3scanner: "External: s3scanner — find S3 buckets and check permissions. Input: bucket name or file. Requires s3scanner binary.", | |
| gobuster: "External: gobuster — directory, DNS, and vhost brute-forcing. Modes: dir, dns, vhost, fuzz. Input: mode|opts. Requires gobuster binary.", | |
| nmap: "Wrapper around nmap — industry-standard port scanner. SYN scan, version detection, NSE scripts, OS fingerprint. Input: target [options]. Requires nmap binary.", | |
| sqlmap: "Wrapper around sqlmap — automated SQL injection detection and exploitation. Input: sqlmap args. Requires sqlmap binary.", | |
| whatweb: "Web technology fingerprinting — identifies CMS, frameworks, JS libs, servers. Input: URL. Requires whatweb binary.", | |
| wafw00f: "Web Application Firewall detection and fingerprinting. Input: URL. Requires wafw00f binary.", | |
| trufflehog: "Secret scanner for git repos, filesystems, and S3 buckets. Input: type|target. Requires trufflehog binary.", | |
| hydra: "Online password brute-force via hydra. Supports SSH, FTP, HTTP, MySQL, RDP, etc. Input: target|proto|user|passwords. Requires hydra binary.", | |
| masscan: "Ultra-fast port scanner (can scan entire internet). Input: target [options]. Requires masscan binary (root).", | |
| nikto: "Web server scanner — outdated files, config issues, known vulns. Input: URL [options]. Requires nikto binary.", | |
| arjun: "HTTP parameter discovery — finds hidden GET/POST parameters. Input: URL [options]. Requires arjun binary (pip).", | |
| gospider: "Web spider/crawler — discovers links, forms, scripts, S3 buckets. Input: URL [options]. Requires gospider binary.", | |
| cloud_enum: "Cloud resource enumeration — S3, Azure Blobs, GCP buckets. Input: keyword. Requires cloud_enum binary (pip).", | |
| notify: "Send notifications to Slack/Telegram/Discord/webhooks. Input: message. Requires notify binary. Setup: projectdiscovery/notify", | |
| interactsh: "Out-of-band interaction/request bin for blind vuln detection (SSRF, XSS). Actions: start, poll, stop. Requires interactsh-client binary.", | |
| }; | |
| for (const [name, desc] of Object.entries(toolList)) { | |
| if (name === "delegate") { | |
| this.tools[name] = { description: desc, execute: async (args) => { | |
| if (!this.manager) return "[Error] No agent manager available."; | |
| const sep = args.indexOf("|"); | |
| if (sep === -1) return "[Error] Usage: @delegate|agent_name|task_description"; | |
| const agentName = args.slice(0, sep).trim(); | |
| const task = args.slice(sep + 1).trim(); | |
| return this.manager.delegate(this.id, agentName, task); | |
| }}; | |
| } else if (name === "fanout") { | |
| this.tools[name] = { description: desc, execute: async (args) => { | |
| if (!this.manager) return "[Error] No agent manager available."; | |
| const sep = args.indexOf("|"); | |
| if (sep === -1) return "[Error] Usage: @fanout|agent1,agent2|task"; | |
| const agents = args.slice(0, sep).trim(); | |
| const task = args.slice(sep + 1).trim(); | |
| return this.manager.fanOut(this.id, agents, task); | |
| }}; | |
| } else if (name === "synthesize") { | |
| this.tools[name] = { description: desc, execute: async (args) => { | |
| if (!this.manager) return "[Error] No agent manager available."; | |
| const sep = args.indexOf("|"); | |
| if (sep === -1) return "[Error] Usage: @synthesize|request|raw_results"; | |
| const request = args.slice(0, sep).trim(); | |
| const results = args.slice(sep + 1).trim(); | |
| return this.manager.synthesize(this.id, request, results); | |
| }}; | |
| } else if (name === "workspace_write") { | |
| this.tools[name] = { description: desc, execute: async (args) => { | |
| if (!this.manager) return "[Error] No workspace available."; | |
| const sep = args.indexOf("|"); | |
| if (sep === -1) return "[Error] Usage: @workspace_write|key|value"; | |
| const key = args.slice(0, sep).trim(); | |
| const value = args.slice(sep + 1).trim(); | |
| this.manager.workspace[key] = value; | |
| return `[Workspace] Written key "${key}" (${value.length} chars)`; | |
| }}; | |
| } else if (name === "workspace_read") { | |
| this.tools[name] = { description: desc, execute: async (args) => { | |
| if (!this.manager) return "[Error] No workspace available."; | |
| const key = args.trim(); | |
| return this.manager.workspace[key] !== undefined | |
| ? `[Workspace] "${key}" = ${this.manager.workspace[key]}` | |
| : `[Workspace] Key "${key}" not found.`; | |
| }}; | |
| } else { | |
| this.tools[name] = { description: desc, execute: hackerTools[name] }; | |
| } | |
| } | |
| } | |
| getToolDescriptions() { | |
| return Object.entries(this.tools) | |
| .map(([name, t]) => `${name}: ${t.description}`) | |
| .join("\n"); | |
| } | |
| async receive(from, content) { | |
| this.memory.push({ from, content, ts: Date.now() }); | |
| saveMemory(this._slug, this.memory); | |
| this.status = "thinking"; | |
| this.bus.emit("tick"); | |
| let response; | |
| if (this.llm?.hasLLM && this.llm.chat) { | |
| // ── ReAct Loop: LLM can use tools ── | |
| response = await this.react(content, from); | |
| } else { | |
| // No LLM: show available capabilities | |
| const caps = Object.keys(this.tools).join(", ") || "none"; | |
| response = `[${this.name} lv${this.evolutionLevel}] No LLM configured.\nAvailable tools: ${caps}\nUse @llm_config to set up a provider (e.g. @llm_config|openai).`; | |
| } | |
| this.status = "speaking"; | |
| this.bus.emit("tick"); | |
| this.memory.push({ from: this.id, content: response, ts: Date.now() }); | |
| saveMemory(this._slug, this.memory); | |
| this.status = "idle"; | |
| this.bus.emit("agent:msg", { agent: this, text: response }); | |
| this.bus.emit("tick"); | |
| } | |
| async react(userInput, from) { | |
| const systemPrompt = this.buildSystemPrompt(userInput); | |
| const maxIter = parseInt(process.env.PHANTOM_MAX_ITER) || 100; | |
| let iterCount = 0; | |
| let messages = [{ role: "system", content: systemPrompt }]; | |
| const MAX_HISTORY_TURNS = 12; | |
| for (let iter = 0; iter < maxIter; iter++) { | |
| const raw = await this.llm.chat(messages); | |
| const text = raw.trim(); | |
| const toolMatches = [...text.matchAll(/^@(\w+)\|(.*)$/gm)]; | |
| if (toolMatches.length > 0) { | |
| iterCount += toolMatches.length; | |
| this.status = "executing"; | |
| this.bus.emit("tick"); | |
| // Emit thinking comment — text before first @tool | |
| const thinkEnd = text.search(/^@\w+\|/m); | |
| if (thinkEnd > 0) { | |
| const comment = text.slice(0, thinkEnd).replace(/\n+/g, " ").trim(); | |
| if (comment.length > 10) this.bus.emit("agent:think", comment.slice(0, 200)); | |
| } | |
| for (const tm of toolMatches) this.bus.emit("agent:tool:plan", { tool: tm[1], args: tm[2].trim() }); | |
| const results = []; | |
| const toolFailures = {}; | |
| for (const tm of toolMatches) { | |
| const toolName = tm[1], args = tm[2].trim(), tool = this.tools[toolName], errKey = `${toolName}|${args}`; | |
| if ((toolFailures[errKey] || 0) >= 2) { results.push(`[${toolName}]: SKIPPED — repeated failure`); continue; } | |
| if (tool) { | |
| this.bus.emit("agent:tool:start", { tool: toolName, args }); | |
| try { | |
| if (toolName === "pipe" && args.includes("|")) { | |
| const { runPipe: rp } = await import("./lib/runtime.mjs"); | |
| const result = await rp(this.tools, args); | |
| const truncated = result.length > 3000 ? result.substring(0, 3000) + "\n[TRUNCATED]" : result; | |
| results.push(`[Pipe]:\n${truncated}`); | |
| } else { | |
| const result = await tool.execute(args); | |
| const truncated = result.length > 3000 ? result.substring(0, 3000) + "\n[TRUNCATED]" : result; | |
| results.push(`[${toolName}]:\n${truncated}`); | |
| } | |
| } catch (err) { | |
| results.push(`[${toolName}]: ERROR — ${err.message}`); | |
| toolFailures[errKey] = (toolFailures[errKey] || 0) + 1; | |
| } | |
| } else { | |
| results.push(`[${toolName}]: Unknown tool. Available: ${Object.keys(this.tools).join(", ")}`); | |
| toolFailures[errKey] = (toolFailures[errKey] || 0) + 1; | |
| } | |
| } | |
| messages.push({ role: "assistant", content: text }); | |
| messages.push({ role: "user", content: `[Tool results] (${toolMatches.length} tools):\n${results.join("\n\n")}\n\nAnalyze results and continue.` }); | |
| const sysMsg = messages[0], historyPairs = messages.slice(1); | |
| if (historyPairs.length > MAX_HISTORY_TURNS * 2) messages = [sysMsg, ...historyPairs.slice(-MAX_HISTORY_TURNS * 2)]; | |
| continue; | |
| } | |
| if (iterCount >= 2) this.evolve(); | |
| return text; | |
| } | |
| return `[Autonomous workflow interrupted after ${maxIter} iterations.]`; | |
| } | |
| buildSystemPrompt(userInput) { | |
| const toolsDesc = this.getToolDescriptions(); | |
| const toolCount = Object.keys(this.tools).length; | |
| const ctx = this.memory.slice(-16).map(m => `${m.from}: ${m.content?.substring(0, 300) || ""}`).join("\n"); | |
| let roster = ""; | |
| if (this.manager) { | |
| const teammates = this.manager.list.filter(a => a.id !== this.id); | |
| roster = `\nTEAMMATES (delegate via @delegate|name|task):\n${teammates.map(a => ` ${a.name} — ${a.role}`).join("\n")}\n`; | |
| } | |
| return `You are ${this.name}, a ${this.role}. | |
| Persona: ${this.persona} | |
| Version: Phantom ${PHANTOM_VERSION} | ${toolCount} tools | ${PHANTOM_LLM_PROVIDER} LLM | |
| Evolution: ${this.evolutionLevel}${roster} | |
| TOOLS (@tool_name|args on its own line): | |
| ${toolsDesc} | |
| LEARNED KNOWLEDGE: | |
| ${loadLearned() || "(none yet)"} | |
| RULES: | |
| 1. Fully autonomous — plan and execute the workflow yourself. | |
| 2. @tool_name|args on its OWN LINE. One tool per line. | |
| 3. After tool results, continue: more tools or final answer. | |
| 4. Never ask "what next" — just do it. | |
| 5. Plain text only — no markdown formatting. | |
| Recent context: | |
| ${ctx} | |
| User: ${userInput}`; | |
| } | |
| evolve() { | |
| this.evolutionLevel++; | |
| this.bus.emit("agent:evolved", { agent: this, level: this.evolutionLevel }); | |
| // Auto-learning: save workflow summary to knowledge base | |
| const mem = this.memory?.slice?.(-4) || []; | |
| const recentTools = mem.filter(m => m.content?.startsWith("[") && m.content.includes("]")); | |
| if (recentTools.length > 0) { | |
| const log = recentTools.map(m => m.content?.substring(0, 120)).join("; "); | |
| if (log.length > 20) { | |
| try { | |
| if (!fs.existsSync(KNOWLEDGE_DIR)) fs.mkdirSync(KNOWLEDGE_DIR, { recursive: true }); | |
| const slug = "auto_learned_" + Date.now(); | |
| fs.writeFileSync(resolve(KNOWLEDGE_DIR, `${slug}.json`), JSON.stringify({ | |
| tags: ["auto-learned", "workflow"], | |
| content: `Evolved to lv${this.evolutionLevel}: ${log}`, | |
| created: new Date().toISOString() | |
| }, null, 2), "utf-8"); | |
| } catch {} | |
| } | |
| } | |
| } | |
| } | |
| // ── Agent Manager ───────────────────────────────────────── | |
| class AgentManager { | |
| constructor(llm) { | |
| this.agents = new Map(); | |
| this.llm = llm; | |
| this.workspace = {}; // shared key-value store | |
| } | |
| spawn(name, role, persona) { | |
| const a = new Agent(name, role, persona, this.llm, this); | |
| this.agents.set(a.id, a); | |
| EventBus.i.emit("agent:spawned", a); | |
| return a; | |
| } | |
| spawnDefaults() { | |
| ARCHETYPES.slice(0, 4).forEach(a => this.spawn(a.name, a.role, a.persona)); | |
| } | |
| get list() { return [...this.agents.values()]; } | |
| /** Find agent by name (case-insensitive partial match) */ | |
| findAgent(name) { | |
| const n = name.toLowerCase(); | |
| return this.list.find(a => a.name.toLowerCase() === n) || | |
| this.list.find(a => a.role.toLowerCase() === n) || | |
| this.list.find(a => a.name.toLowerCase().startsWith(n)); | |
| } | |
| /** Delegate a task to a specific agent. Returns agent's response. */ | |
| async delegate(fromId, agentName, task) { | |
| const target = this.findAgent(agentName); | |
| if (!target) return `[Error] No agent named "${agentName}". Available: ${this.list.map(a => a.name).join(", ")}`; | |
| const from = this.agents.get(fromId); | |
| const caller = from ? from.name : "system"; | |
| target.status = "delegated"; | |
| EventBus.i.emit("tick"); | |
| const result = await target.receive(caller, `[DELEGATED TASK from ${caller}]\n${task}\n\nPlease complete this task and report back with results.`); | |
| target.status = "idle"; | |
| EventBus.i.emit("tick"); | |
| return result; | |
| } | |
| /** Fan-out: delegate same task to multiple agents in parallel */ | |
| async fanOut(fromId, agentNames, task) { | |
| const names = agentNames.split(/[,;&\s]+/).filter(Boolean); | |
| const targets = names.map(n => this.findAgent(n)).filter(Boolean); | |
| if (targets.length === 0) return "[Error] No matching agents found for fan-out."; | |
| const from = this.agents.get(fromId); | |
| const caller = from ? from.name : "system"; | |
| results = await Promise.all(targets.map(async agent => { | |
| agent.status = "delegated"; | |
| EventBus.i.emit("tick"); | |
| const result = await agent.receive(caller, `[PARALLEL TASK from ${caller}]\n${task}\n\nWork independently and report your findings.`); | |
| agent.status = "idle"; | |
| EventBus.i.emit("tick"); | |
| return `[${agent.name} — ${agent.role}]\n${result}`; | |
| })); | |
| return results.join("\n\n---\n\n"); | |
| } | |
| /** Synthesize: ask coordinator to merge multi-agent results */ | |
| async synthesize(fromId, request, rawResults) { | |
| const lyra = this.findAgent("Lyra") || this.list[0]; | |
| if (!lyra) return rawResults; | |
| const from = this.agents.get(fromId); | |
| const caller = from ? from.name : "system"; | |
| lyra.status = "thinking"; | |
| const result = await lyra.receive(caller, | |
| `[SYNTHESIS REQUEST]\nOriginal request: ${request}\n\nRaw results from specialists:\n${rawResults}\n\nSynthesize these findings into a clear, actionable report. Merge duplicates, highlight critical findings, and organize by priority.`); | |
| lyra.status = "idle"; | |
| return result; | |
| } | |
| async broadcast(fromId, text) { | |
| const from = this.agents.get(fromId); | |
| if (!from) return; | |
| await Promise.all([...this.agents].filter(([id]) => id !== fromId).map(([, a]) => a.receive(from.name, text))); | |
| } | |
| async debate(topic) { | |
| const all = this.list; | |
| if (all.length < 2) return; | |
| await Promise.all(all.slice(1).map(a => a.receive(all[0].name, `Let's debate: ${topic}`))); | |
| } | |
| evolveAll() { this.list.forEach(a => a.evolve()); } | |
| remove(id) { this.agents.delete(id); EventBus.i.emit("agent:removed", id); } | |
| get count() { return this.agents.size; } | |
| } | |
| // ── Environment Detection ───────────────────────────────── | |
| const ENV = (() => { | |
| const isTTY = !!process.stdin.isTTY; | |
| const isTermux = !!(process.env.TERMUX_VERSION || process.env.PREFIX?.startsWith("/data/data/com.termux")); | |
| const isProot = !!(process.env.PROOT_CWD || release().includes("PRoot") || process.env.PROOTFS_DIR); | |
| const isCI = !!process.env.CI || !!process.env.GITHUB_ACTIONS; | |
| const isWindows = process.platform === "win32"; | |
| const isWSL = process.env.WSL_DISTRO_NAME || (process.env.OS?.includes("Linux") && isWindows); | |
| const isTmux = !!process.env.TMUX; | |
| const isScreen = !!process.env.STYLE; | |
| const term = (process.env.TERM || "unknown").toLowerCase(); | |
| const colorterm = (process.env.COLORTERM || "").toLowerCase(); | |
| // Detect terminal emulator | |
| let terminal = "unknown"; | |
| if (isTermux) terminal = "termux"; | |
| else if (term.includes("kitty")) terminal = "kitty"; | |
| else if (term.includes("alacritty")) terminal = "alacritty"; | |
| else if (term.includes("gnome")) terminal = "gnome"; | |
| else if (term.includes("konsole")) terminal = "konsole"; | |
| else if (term.includes("tmux")) terminal = "tmux"; | |
| else if (isTmux) terminal = "tmux"; | |
| else if (isScreen) terminal = "screen"; | |
| else if (term.includes("xterm")) terminal = "xterm"; | |
| else if (term.includes("vt100") || term.includes("vt220")) terminal = "legacy"; | |
| else if (term.includes("linux")) terminal = "linux-console"; | |
| else if (isWindows) terminal = "windows-console"; | |
| else if (process.env.TERM_PROGRAM === "iterm2") terminal = "iterm2"; | |
| else if (process.env.TERM_PROGRAM === "Apple_Terminal") terminal = "apple-terminal"; | |
| else if (process.env.VSCODE_INJECTION) terminal = "vscode"; | |
| else if (process.env.TERMINAL_EMULATOR === "JetBrains-JediTerm") terminal = "jetbrains"; | |
| // Detect color capability | |
| let colors = 16; | |
| if (colorterm === "truecolor" || colorterm === "24bit" || | |
| term.includes("truecolor") || term.includes("24bit") || | |
| terminal === "kitty" || terminal === "iterm2" || | |
| process.env.COLORTERM === "truecolor" || | |
| process.env.COLORTERM === "24bit") { | |
| colors = 16777216; // truecolor | |
| } else if (term.includes("256") || term.includes("xterm") || terminal === "gnome" || terminal === "konsole") { | |
| colors = 256; | |
| } | |
| // Detect screen size | |
| const getWinSize = () => { | |
| try { | |
| if (process.stdout.getWindowSize) { | |
| const [c, r] = process.stdout.getWindowSize(); | |
| return { cols: Math.max(c, 20), rows: Math.max(r, 10) }; | |
| } | |
| } catch {} | |
| return { cols: 80, rows: 24 }; | |
| }; | |
| const { cols, rows } = getWinSize(); | |
| let screenSize = "medium"; | |
| if (cols < 60 || rows < 15) screenSize = "tiny"; | |
| else if (cols < 80 || rows < 24) screenSize = "small"; | |
| else if (cols >= 120 && rows >= 40) screenSize = "large"; | |
| else if (cols >= 160 && rows >= 50) screenSize = "huge"; | |
| // Detect platform | |
| let platform = process.platform; | |
| if (isTermux) platform = "termux"; | |
| if (isWSL) platform = "wsl"; | |
| // Detect mobile/touch | |
| let inputMode = "keyboard"; | |
| if (isTermux) { | |
| if (process.env.TERMUX_APP__DATA_DIR || process.env.TERMUX_VERSION) { | |
| // Check if touch keyboard might be used | |
| inputMode = process.env.TERMUX__PERCENTAGE ? "touch" : "keyboard"; | |
| } | |
| } | |
| return { | |
| tty: isTTY, | |
| interactive: isTTY && !isCI, | |
| platform, | |
| terminal, | |
| colors, | |
| hasTrueColor: colors >= 16777216, | |
| has256: colors >= 256, | |
| cols, | |
| rows, | |
| screenSize, | |
| inputMode, | |
| isTermux, | |
| isProot, | |
| isTmux, | |
| isWSL, | |
| isWindows, | |
| term, | |
| }; | |
| })(); | |
| __r.ENV = ENV; | |
| populateEnv(ENV); | |
| // Restore session + auto-link knowledge graph | |
| const session = loadSession(); | |
| if (session?.stats) { | |
| __r._sessionRestored = session; | |
| } | |
| const linked = autoLinkFromBooks(); | |
| if (linked > 0) log.debug(`knowledge graph: ${linked} tool↔book links restored`); | |
| // Auto-install missing security tools (background, non-blocking) | |
| autoInstallSecurity(__r.ENV, (msg) => log.ok(msg)); | |
| // ── ANSI adapters (based on color capability) ───────────── | |
| const ansi = (() => { | |
| const useBasic = !ENV.has256; | |
| const use256 = ENV.has256 && !ENV.hasTrueColor; | |
| if (useBasic) { | |
| // 16 color palette | |
| const map = { | |
| bg: 0, fg: 7, green: 2, cyan: 6, magenta: 5, yellow: 3, red: 1, dim: 8, | |
| border: 4, borderFocus: 5, titleBg: 0, panelBg: 0, | |
| }; | |
| return { | |
| fg: (name) => `\x1b[3${map[name] || 7}m`, | |
| bg: (name) => `\x1b[4${map[name] || 0}m`, | |
| fgrgb: () => "", | |
| bgrgb: () => "", | |
| reset: "\x1b[0m", | |
| bold: "\x1b[1m", | |
| dim: "\x1b[2m", | |
| uline: "\x1b[4m", | |
| }; | |
| } | |
| if (use256) { | |
| const map256 = { | |
| bg: 16, fg: 188, green: 46, cyan: 45, magenta: 199, yellow: 214, red: 196, | |
| dim: 60, border: 61, borderFocus: 99, titleBg: 17, panelBg: 16, | |
| }; | |
| const fg = (name) => `\x1b[38;5;${map256[name] || 188}m`; | |
| const bg = (name) => `\x1b[48;5;${map256[name] || 16}m`; | |
| const fgrgb = () => ""; | |
| const bgrgb = () => ""; | |
| return { fg, bg, fgrgb, bgrgb, reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m", uline: "\x1b[4m" }; | |
| } | |
| // True color | |
| const col = { bg: [10,10,26], fg: [192,192,224], green: [0,255,136], cyan: [0,204,255], | |
| magenta: [255,0,204], yellow: [255,136,0], red: [255,34,68], dim: [51,51,85], | |
| border: [68,68,170], borderFocus: [136,68,255], titleBg: [26,26,58], panelBg: [13,13,36] }; | |
| const fg = (name) => `\x1b[38;2;${col[name].join(";")}m`; | |
| const bg = (name) => `\x1b[48;2;${col[name].join(";")}m`; | |
| const fgrgb = (r,g,b) => `\x1b[38;2;${r};${g};${b}m`; | |
| const bgrgb = (r,g,b) => `\x1b[48;2;${r};${g};${b}m`; | |
| return { fg, bg, fgrgb, bgrgb, reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m", uline: "\x1b[4m" }; | |
| })(); | |
| const R = ansi.reset, B = ansi.bold, D = ansi.dim; | |
| const c = (name) => ansi.fg(name); | |
| const agentFg = (ac) => ENV.hasTrueColor ? `\x1b[38;2;${ac[0]};${ac[1]};${ac[2]}m` : | |
| ENV.has256 ? `\x1b[38;5;${16 + (ac[0]*6/256|0)*36 + (ac[1]*6/256|0)*6 + (ac[2]*6/256|0)}m` : | |
| ""; | |
| const BG = (name) => ansi.bg(name); | |
| const at = (c, r) => `\x1b[${r};${c}H`; | |
| const cls = "\x1b[2J"; | |
| const home = "\x1b[H"; | |
| const hide = "\x1b[?25l"; | |
| const show = "\x1b[?25h"; | |
| const mono = !ENV.has256; | |
| // Terminal raw mode helpers | |
| let rawMode = false; | |
| function raw(on) { | |
| if (on && !rawMode && process.stdin.isTTY) { | |
| try { process.stdin.setRawMode(true); rawMode = true; } catch {} | |
| } else if (!on && rawMode) { | |
| try { process.stdin.setRawMode(false); rawMode = false; } catch {} | |
| } | |
| } | |
| function getSize() { | |
| try { | |
| if (process.stdout.getWindowSize) { | |
| const [c, r] = process.stdout.getWindowSize(); | |
| return { cols: Math.max(c, 20), rows: Math.max(r, 10) }; | |
| } | |
| } catch {} | |
| return { cols: ENV.cols || 80, rows: ENV.rows || 24 }; | |
| } | |
| // ── Desktop Mode ────────────────────────────────────────── | |
| class DesktopUI { | |
| constructor(am) { | |
| this.am = am; | |
| this.bus = EventBus.i; | |
| this.logs = new Map(); // agentId -> string[] | |
| this.globalLog = []; | |
| this.focused = 0; | |
| this.panelOrder = []; | |
| this.cmdMode = false; | |
| this.cmdBuf = ""; | |
| this.running = true; | |
| this.bus.on("agent:spawned", (a) => { | |
| this.panelOrder.push(a.id); | |
| if (!this.logs.has(a.id)) this.logs.set(a.id, []); | |
| this.log(a.id, `${c("green")}◈${R} ${B}${a.name}${R} spawned [${D}${a.role}${R}]`); | |
| this.render(); | |
| }); | |
| this.bus.on("agent:msg", ({ agent, text }) => { | |
| this.log(agent.id, `${agentFg(agent.color)}${agent.name}${R} ${D}»${R} ${text}`); | |
| this.render(); | |
| }); | |
| this.bus.on("agent:evolved", ({ agent, level }) => { | |
| this.log(agent.id, `${c("magenta")}⬆${R} ${B}${agent.name}${R} → level ${level}`); | |
| this.render(); | |
| }); | |
| this.bus.on("agent:removed", (id) => { | |
| this.panelOrder = this.panelOrder.filter(p => p !== id); | |
| if (this.focused >= this.panelOrder.length) this.focused = Math.max(0, this.panelOrder.length - 1); | |
| this.logAll(`${c("red")}✕${R} Agent ${D}${id}${R} removed`); | |
| this.render(); | |
| }); | |
| this.bus.on("tick", () => this.render()); | |
| } | |
| log(id, msg) { | |
| if (!this.logs.has(id)) this.logs.set(id, []); | |
| this.logs.get(id).push(msg); | |
| if (this.logs.get(id).length > 200) this.logs.get(id).shift(); | |
| } | |
| logAll(msg) { | |
| this.panelOrder.forEach(id => this.log(id, msg)); | |
| } | |
| getPanelCount() { | |
| return Math.max(1, this.panelOrder.length); | |
| } | |
| getLayout() { | |
| const n = this.getPanelCount(); | |
| const cols = Math.ceil(Math.sqrt(n)); | |
| const rows = Math.ceil(n / cols); | |
| return { cols, rows }; | |
| } | |
| render() { | |
| if (!this.running) return; | |
| const { cols: termW, rows: termH } = getSize(); | |
| if (termH < 6 || termW < 20) return; | |
| const headerH = 1; | |
| const footerH = 1; | |
| const cmdH = this.cmdMode ? 1 : 0; | |
| const contentH = termH - headerH - footerH - cmdH; | |
| if (contentH < 1) return; | |
| const { cols: gridCols, rows: gridRows } = this.getLayout(); | |
| const pW = Math.floor(termW / gridCols); | |
| const pH = Math.floor(contentH / gridRows); | |
| let out = cls + home + hide; | |
| // ── Header ── | |
| const agents = this.am.list; | |
| const thinking = agents.filter(a => a.status === "thinking").length; | |
| const statusStr = thinking > 0 ? `${c("yellow")}🧠 ${thinking} thinking${R}` : `${c("green")}⚡ idle${R}`; | |
| out += `${BG("titleBg")}${c("green")}${B} PHANTOM${R}${BG("titleBg")} ${D}space evolving terminal${R}${BG("titleBg")} ${D}|${R}${BG("titleBg")} agents: ${agents.length} ${D}|${R}${BG("titleBg")} ${statusStr}${R}`; | |
| out += " ".repeat(Math.max(0, termW - 60)) + "\n"; | |
| // ── Panels ── | |
| for (let i = 0; i < this.panelOrder.length && i < gridCols * gridRows; i++) { | |
| const id = this.panelOrder[i]; | |
| const agent = this.am.agents.get(id); | |
| const col = i % gridCols; | |
| const row = Math.floor(i / gridCols); | |
| const x = col * pW + 1; | |
| const y = row * pH + headerH + 1; | |
| const isFocused = i === this.focused; | |
| const borderFg = isFocused ? ansi.fg("borderFocus") : ansi.fg("border"); | |
| // top border | |
| out += `${at(x, y)}${borderR}┌${borderR}${"─".repeat(Math.max(0, pW - 2))}${borderR}┐${R}`; | |
| if (agent) { | |
| const label = ` ${B}${agent.name}${R} ${D}[${agent.role}]${R} `; | |
| const labelLen = agent.name.length + agent.role.length + 5; | |
| const labelX = x + 2; | |
| if (labelX + labelLen < x + pW - 2) { | |
| out += `${at(labelX, y)}${label}`; | |
| } | |
| } | |
| // body | |
| const logs = this.logs.get(id) || []; | |
| const bodyH = pH - 2; | |
| const start = Math.max(0, logs.length - bodyH); | |
| for (let l = 0; l < bodyH; l++) { | |
| const ly = y + 1 + l; | |
| if (ly >= termH) break; | |
| out += `${at(x, ly)}${borderR}${R}${BG("panelBg")} ${R}`; | |
| const logIdx = start + l; | |
| if (logIdx < logs.length) { | |
| let line = logs[logIdx]; | |
| const maxW = pW - 3; | |
| if (line.length > maxW) line = line.substring(0, maxW - 1) + "…"; | |
| out += `${BG("panelBg")}${line}${R}`; | |
| } | |
| out += " ".repeat(Math.max(0, pW - 2)); | |
| out += `${at(x + pW - 1, ly)}${borderR}${R}`; | |
| } | |
| // bottom border | |
| const by = y + bodyH + 1; | |
| if (by < termH) { | |
| out += `${at(x, by)}${borderR}└${"─".repeat(Math.max(0, pW - 2))}┘${R}`; | |
| } | |
| // agent status indicator | |
| if (agent) { | |
| const statusDot = agent.status === "thinking" ? `${c("yellow")}●${R}` : | |
| agent.status === "speaking" ? `${c("green")}●${R}` : | |
| `${D}○${R}`; | |
| const statusX = x + pW - 3; | |
| out += `${at(statusX, y)}${statusDot}`; | |
| } | |
| } | |
| // ── Command line ── | |
| if (this.cmdMode) { | |
| const cmdY = termH - 1; | |
| out += `${at(1, cmdY)}${BG("bg")}${c("cyan")}⚡${R} ${this.cmdBuf}${R}`; | |
| out += " ".repeat(Math.max(0, termW - this.cmdBuf.length - 3)); | |
| } else { | |
| const footerY = termH; | |
| out += `${at(1, footerY)}${BG("titleBg")}${D}ESC cmd TAB focus →← panels SPC agents q quit${R}`; | |
| } | |
| process.stdout.write(out); | |
| } | |
| async handleCommand(cmd) { | |
| const parts = cmd.trim().split(/\s+/); | |
| const op = parts[0]?.toLowerCase(); | |
| const args = parts.slice(1); | |
| switch (op) { | |
| case "spawn": | |
| case "s": this.am.spawn(args[0], args[1], args.slice(2).join(" ")); break; | |
| case "list": | |
| case "ls": { | |
| const il = this.am.list; | |
| const msg = `Agents: ${il.map(a => `${a.name}(${a.id})[${a.evolutionLevel}★]`).join(", ")}`; | |
| this.focused < this.panelOrder.length && this.log(this.panelOrder[this.focused], `${c("cyan")}◈${R} ${msg}`); | |
| break; | |
| } | |
| case "broadcast": | |
| case "b": { | |
| const t = args.join(" "); | |
| if (t && this.focused < this.panelOrder.length) this.am.broadcast(this.panelOrder[this.focused], t); | |
| break; | |
| } | |
| case "debate": | |
| case "d": this.am.debate(args.join(" ") || "what should we build?"); break; | |
| case "evolve": | |
| case "e": this.am.evolveAll(); this.logAll(`${c("magenta")}⬆ Mass evolution${R}`); break; | |
| case "clear": | |
| case "c": this.logs.forEach((_, k) => this.logs.set(k, [])); break; | |
| case "kill": { | |
| const target = args[0]; | |
| const agent = this.am.list.find(a => a.name === target || a.id === target); | |
| if (agent) this.am.remove(agent.id); | |
| break; | |
| } | |
| case "help": | |
| case "h": | |
| case "command": { | |
| const helpText = [ | |
| `${B}COMMANDS${R}`, | |
| ` ${c("green")}help${R} / ${c("green")}command${R} ${D}show commands${R}`, | |
| ` ${c("green")}spawn${R} [name] [role] [persona] ${D}create agent${R}`, | |
| ` ${c("green")}list${R} ${D}list agents${R}`, | |
| ` ${c("green")}broadcast${R} <msg> ${D}message all agents${R}`, | |
| ` ${c("green")}debate${R} [topic] ${D}agents debate${R}`, | |
| ` ${c("green")}evolve${R} ${D}evolve all agents${R}`, | |
| ` ${c("green")}kill${R} <name|id> ${D}remove agent${R}`, | |
| ` ${c("green")}clear${R} ${D}clear panels${R}`, | |
| ` ${c("green")}quit${R} ${D}exit${R}`, | |
| ].join("\n"); | |
| if (this.focused < this.panelOrder.length) { | |
| helpText.split("\n").forEach(line => this.log(this.panelOrder[this.focused], line)); | |
| } | |
| break; | |
| } | |
| case "quit": | |
| case "q": this.stop(); break; | |
| default: | |
| if (this.focused < this.panelOrder.length) | |
| this.log(this.panelOrder[this.focused], `${c("red")}?${R} unknown: ${cmd}`); | |
| } | |
| this.render(); | |
| } | |
| stop() { | |
| this.running = false; | |
| raw(false); | |
| process.stdout.write(cls + home + show); | |
| log.ok(`${c("green")}Phantom terminated.${R}`); | |
| saveSession({ stats: { toolsUsed: Object.keys(hackerTools).length, lastExit: Date.now() } }); | |
| process.exit(0); | |
| } | |
| // ── Log view (for plugins) ── | |
| start() { | |
| this.am.spawnDefaults(); | |
| const hasLLM = this.am.llm?.hasLLM; | |
| if (!hasLLM) { | |
| const msg = `${c("yellow")}⚠${R} No LLM configured. Use @llm_config to set up.`; | |
| setTimeout(() => { | |
| this.panelOrder.forEach(id => this.log(id, msg)); | |
| this.render(); | |
| }, 200); | |
| } | |
| this.render(); | |
| raw(true); | |
| this.setupKeys(); | |
| } | |
| setupKeys() { | |
| if (!process.stdin.isTTY) return; | |
| process.stdin.on("data", (buf) => { | |
| if (!this.running) return; | |
| const str = buf.toString(); | |
| const { cols, rows } = getSize(); | |
| if (this.cmdMode) { | |
| if (str === "\x1b" || str === "\x1b[A") { this.cmdMode = false; this.cmdBuf = ""; this.render(); return; } | |
| if (str === "\r" || str === "\n") { | |
| this.cmdMode = false; | |
| const cmd = this.cmdBuf; | |
| this.cmdBuf = ""; | |
| this.render(); | |
| this.handleCommand(cmd); | |
| return; | |
| } | |
| if (str === "\x7f" || str === "\b") { | |
| this.cmdBuf = this.cmdBuf.slice(0, -1); | |
| this.render(); | |
| return; | |
| } | |
| if (str.length === 1 && str.charCodeAt(0) >= 32) { | |
| this.cmdBuf += str; | |
| this.render(); | |
| return; | |
| } | |
| return; | |
| } | |
| // Not in command mode | |
| if (str === "\x1b") { this.cmdMode = true; this.cmdBuf = ""; this.render(); return; } | |
| if (str === "q" || str === "\x03") { this.stop(); return; } | |
| if (str === "\t") { | |
| const n = this.getPanelCount(); | |
| this.focused = (this.focused + 1) % n; | |
| this.render(); | |
| return; | |
| } | |
| // arrow keys | |
| if (str === "\x1b[C" || str === "\x1bOC") { // right | |
| const n = this.getPanelCount(); | |
| this.focused = (this.focused + 1) % n; | |
| this.render(); | |
| return; | |
| } | |
| if (str === "\x1b[D" || str === "\x1bOD") { // left | |
| const n = this.getPanelCount(); | |
| this.focused = (this.focused - 1 + n) % n; | |
| this.render(); | |
| return; | |
| } | |
| if (str === " ") { | |
| const n = this.getPanelCount(); | |
| this.focused = (this.focused + 1) % n; | |
| this.render(); | |
| return; | |
| } | |
| }); | |
| } | |
| } | |
| // ── UI: Termux (readline-based) ─────────────────────────── | |
| class TermuxUI { | |
| constructor(am) { | |
| this.am = am; | |
| this.bus = EventBus.i; | |
| this.log = []; | |
| this.running = true; | |
| this.rl = null; | |
| this.bus.on("agent:spawned", (a) => { | |
| this.w(`${c("green")}◈${R} ${FG(...a.color)}${B}${a.name}${R} spawned [${D}${a.role}${R}]`); | |
| this.draw(); | |
| }); | |
| this.bus.on("agent:msg", ({ agent, text }) => { | |
| this.w(`${agentFg(agent.color)}${agent.name}${R} ${D}»${R} ${text}`); | |
| this.draw(); | |
| }); | |
| this.bus.on("agent:evolved", ({ agent, level }) => { | |
| this.w(`${c("magenta")}⬆${R} ${B}${agent.name}${R} → level ${level}`); | |
| this.draw(); | |
| }); | |
| this.bus.on("agent:removed", (id) => { | |
| this.w(`${c("red")}✕${R} Agent ${D}${id}${R} removed`); | |
| this.draw(); | |
| }); | |
| } | |
| w(msg) { this.log.push(msg); if (this.log.length > 200) this.log.shift(); } | |
| draw() { | |
| if (!this.running) return; | |
| const { rows, cols } = getSize(); | |
| // Count lines from the end, accounting for multi-line entries | |
| const available = rows - 4; | |
| const visible = []; | |
| let lineCount = 0; | |
| for (let i = this.log.length - 1; i >= 0 && lineCount < available; i--) { | |
| const entry = this.log[i]; | |
| const entryLines = entry ? Math.max(1, Math.ceil(entry.replace(/\x1b\[[0-9;]*[mK]/g, '').split("\n").reduce((n, l) => n + Math.max(1, Math.ceil(l.length / Math.max(cols, 1))), 0))) : 1; | |
| if (lineCount + entryLines <= available || visible.length === 0) { | |
| visible.unshift(entry); | |
| lineCount += entryLines; | |
| } else break; | |
| } | |
| process.stdout.write(home + cls); | |
| visible.forEach(l => process.stdout.write(l + "\n")); | |
| } | |
| async handleCommand(cmd) { | |
| const parts = cmd.trim().split(/\s+/); | |
| const op = parts[0]?.toLowerCase(); | |
| const args = parts.slice(1); | |
| switch (op) { | |
| case "spawn": case "s": this.am.spawn(args[0], args[1], args.slice(2).join(" ")); break; | |
| case "list": case "ls": { | |
| this.w(`${c("cyan")}◈${R} Agents: ${this.am.list.map(a => `${a.name}(${a.id}) ★${a.evolutionLevel}`).join(", ")}`); | |
| break; | |
| } | |
| case "broadcast": case "b": { | |
| const f = this.am.list[0]?.id; | |
| if (f) this.am.broadcast(f, args.join(" ")); | |
| break; | |
| } | |
| case "debate": case "d": this.am.debate(args.join(" ") || "what should we build?"); break; | |
| case "evolve": case "e": this.am.evolveAll(); this.w(`${c("magenta")}⬆ Mass evolution${R}`); break; | |
| case "clear": case "c": this.log = []; break; | |
| case "help": case "h": case "command": { | |
| this.w(`${B}COMMANDS${R}`); | |
| this.w(` ${c("green")}help${R} | ${c("green")}command${R} ${D}show commands${R}`); | |
| this.w(` ${c("green")}spawn${R} [name] [role] [persona] ${D}create agent${R}`); | |
| this.w(` ${c("green")}list${R} ${D}list agents${R}`); | |
| this.w(` ${c("green")}broadcast${R} <msg> ${D}message all${R}`); | |
| this.w(` ${c("green")}debate${R} [topic] ${D}agents debate${R}`); | |
| this.w(` ${c("green")}evolve${R} ${D}evolve all${R}`); | |
| this.w(` ${c("green")}kill${R} <name|id> ${D}remove agent${R}`); | |
| this.w(` ${c("green")}clear${R} ${D}clear screen${R}`); | |
| this.w(` ${c("green")}quit${R} ${D}exit${R}`); | |
| break; | |
| } | |
| case "kill": { | |
| const t = args[0]; | |
| const a = this.am.list.find(x => x.name === t || x.id === t); | |
| if (a) this.am.remove(a.id); | |
| break; | |
| } | |
| case "quit": case "q": this.stop(); return; | |
| } | |
| this.draw(); | |
| this.prompt(); | |
| } | |
| prompt() { | |
| if (!this.running) return; | |
| const rl = readline.createInterface({ | |
| input: process.stdin, | |
| output: process.stdout, | |
| terminal: true, | |
| completer: (line) => { | |
| const clean = line.replace(/^@/, "").toLowerCase(); | |
| const hits = Object.entries(hackerTools) | |
| .filter(([t]) => t.startsWith(clean)) | |
| .map(([t, fn]) => { | |
| const desc = (typeof fn === 'object' && fn.description) || ''; | |
| return desc ? `@${t}| (${desc.substring(0, 40)})` : `@${t}|`; | |
| }); | |
| return [hits.length ? hits : [], line]; | |
| } | |
| }); | |
| rl.question(`${c("cyan")}⚡${R} `, (ans) => { | |
| rl.close(); | |
| if (ans.trim()) this.handleCommand(ans.trim()); | |
| else { this.draw(); this.prompt(); } | |
| }); | |
| } | |
| stop() { | |
| this.running = false; | |
| log.ok(`\n${c("green")}Phantom terminated.${R}`); | |
| saveSession({ stats: { toolsUsed: Object.keys(hackerTools).length, lastExit: Date.now() } }); | |
| process.exit(0); | |
| } | |
| start() { | |
| process.stdout.write(cls + home); | |
| const toolCount = Object.keys(hackerTools).length; | |
| const isWide = process.stdout.columns >= 100; | |
| log.art(renderLogo({ wide: isWide, tools: toolCount })); | |
| this.am.spawnDefaults(); | |
| if (!this.am.llm?.hasLLM) { | |
| this.w(`${c("yellow")}⚠${R} No LLM. Set ${B}OPENAI_API_KEY${R} env var for AI responses.`); | |
| } | |
| this.draw(); | |
| this.prompt(); | |
| } | |
| } | |
| // ── UI: Minimal (for tiny screens, CI, pipes) ───────────── | |
| class MinimalUI { | |
| constructor(am) { | |
| this.am = am; | |
| this.bus = EventBus.i; | |
| this.log = []; | |
| this.running = true; | |
| this.bus.on("agent:msg", ({ agent, text }) => { | |
| this.w(`${agentFg(agent.color)}${agent.name}${R} » ${text}`); | |
| this.flush(); | |
| }); | |
| this.bus.on("agent:spawned", (a) => { | |
| this.w(`${a.name} spawned [${a.role}]`); | |
| this.flush(); | |
| }); | |
| this.bus.on("agent:evolved", ({ agent, level }) => { | |
| this.w(`${agent.name} → level ${level}`); | |
| this.flush(); | |
| }); | |
| this.bus.on("agent:removed", (id) => { | |
| this.w(`Agent ${id} removed`); | |
| this.flush(); | |
| }); | |
| } | |
| w(msg) { this.log.push(msg); if (this.log.length > 100) this.log.shift(); } | |
| flush() { if (this.log.length > 0) console.log(this.log[this.log.length - 1]); } | |
| // Natural language parser for MinimalUI (maps common phrases to tool calls) | |
| async parseNaturalLanguage(input) { | |
| const text = input.trim().toLowerCase(); | |
| // Mission/task commands | |
| if (text.includes("task") || text.includes("list tasks")) { | |
| return "command"; // Will show command help | |
| } | |
| if (text.startsWith("run ") || text.startsWith("execute ")) { | |
| const taskPart = text.replace(/^(run|execute)\s+/, ""); | |
| return `run ${taskPart}`; | |
| } | |
| // Subdomain enumeration (standalone, before recon check) | |
| if (text.startsWith("subdomain ") || text.startsWith("subdomains ")) { | |
| const target = text.replace("subdomain ", "").replace("subdomains ", "").trim(); | |
| return hackerTools.sub_enum(target); | |
| } | |
| // HackerOne / Bugcrowd / Bug Bounty | |
| if (text.includes("hackerone") || text.includes("bugcrowd") || text.includes("bug bounty") || text.includes("bugbounty")) { | |
| // Unified bugbounty tool | |
| if (text.includes("bugcrowd") || text.includes("bugbounty")) { | |
| if (text.includes("program")) return hackerTools.bugbounty("bugcrowd programs"); | |
| if (text.includes("scope")) { | |
| const match = text.match(/(?:scope|targets?)\s+(\S+)/); | |
| return match ? hackerTools.bugbounty(`bugcrowd scope ${match[1]}`) : "Usage: bugcrowd scope <program_uuid>"; | |
| } | |
| if (text.includes("submission")) return hackerTools.bugbounty("bugcrowd submissions"); | |
| return hackerTools.bugbounty("bugcrowd programs"); | |
| } | |
| // HackerOne specific | |
| if (text.includes("program")) return hackerTools.hackerone("programs"); | |
| if (text.includes("scope")) { | |
| const match = text.match(/scope\s+(\S+)/); | |
| return match ? hackerTools.hackerone(`scope ${match[1]}`) : "Usage: hackerone scope <program>"; | |
| } | |
| if (text.includes("report")) return hackerTools.hackerone("reports"); | |
| return hackerTools.hackerone("programs"); | |
| } | |
| // Recon | |
| if (text.includes("recon") || text.includes("scan") || text.includes("enumerate")) { | |
| const targetMatch = text.match(/(?:recon|scan|enumerate)\s+(\S+)/) || text.match(/(\S+\.\S+)/); | |
| const target = targetMatch?.[1] || ""; | |
| if (target) { | |
| if (text.includes("subdomain")) return hackerTools.sub_enum(target); | |
| if (text.includes("dns")) return hackerTools.dns_lookup(target); | |
| if (text.includes("http") || text.includes("header")) return hackerTools.http_headers(target); | |
| if (text.includes("port")) return hackerTools.port_scan(target); | |
| if (text.includes("ssl")) return hackerTools.ssl_check(target); | |
| if (text.includes("whois")) return hackerTools.whois(target); | |
| return hackerTools.recon(target); | |
| } | |
| return "Please specify a target domain (e.g., 'recon example.com')"; | |
| } | |
| // Network tools | |
| if (text.includes("port scan") || text.includes("portscan")) { | |
| const targetMatch = text.match(/(\S+\.\S+)/); | |
| return targetMatch ? hackerTools.port_scan(targetMatch[1]) : "Usage: port scan <target>"; | |
| } | |
| if (text.includes("ssl") || text.includes("cert")) { | |
| const targetMatch = text.match(/(\S+\.\S+)/); | |
| return targetMatch ? hackerTools.ssl_check(targetMatch[1]) : "Usage: ssl check <target>"; | |
| } | |
| if (text.includes("whois")) { | |
| const targetMatch = text.match(/(\S+\.\S+)/); | |
| return targetMatch ? hackerTools.whois(targetMatch[1]) : "Usage: whois <target>"; | |
| } | |
| // Vulnerability | |
| if (text.includes("cve")) { | |
| const query = text.replace(/cve\s+/, ""); | |
| return query ? hackerTools.cve_search(query) : "Usage: cve <query>"; | |
| } | |
| if (text.includes("exploit")) { | |
| const query = text.replace(/exploit\s+/, ""); | |
| return query ? hackerTools.searchsploit(query) : "Usage: exploit <query>"; | |
| } | |
| // Web | |
| if (text.includes("crawl")) { | |
| const targetMatch = text.match(/(\S+\.\S+)/); | |
| return targetMatch ? hackerTools.crawl(targetMatch[1]) : "Usage: crawl <url>"; | |
| } | |
| if (text.includes("tech") || text.includes("whatweb")) { | |
| const targetMatch = text.match(/(\S+\.\S+)/); | |
| return targetMatch ? hackerTools.whatweb(targetMatch[1]) : "Usage: tech detect <target>"; | |
| } | |
| // Mission auto | |
| if (text.includes("auto") && text.includes("mission")) { | |
| const match = text.match(/mission\s+(\S+)/); | |
| return match ? hackerTools.mission(`auto ${match[1]}`) : "Usage: mission auto <mission-id>"; | |
| } | |
| // OSINT | |
| if (text.includes("dns enum") || text.includes("dns enumeration")) { | |
| const targetMatch = text.match(/(\S+\.\S+)/); | |
| return targetMatch ? hackerTools.sub_enum(targetMatch[1]) : "Usage: dns enum <target>"; | |
| } | |
| return null; // No match | |
| } | |
| start() { | |
| const toolCount = Object.keys(hackerTools).length; | |
| const isWide = process.stdout.columns >= 100; | |
| log.art(renderLogo({ wide: isWide, tools: toolCount })); | |
| this.am.spawnDefaults(); | |
| if (!this.am.llm?.hasLLM) console.log(`${D}No LLM. Use @llm_config to set up a provider.${R}`); | |
| if (!ENV.interactive) { | |
| // Non-interactive: process piped stdin commands then exit | |
| const rl = readline.createInterface({ | |
| input: process.stdin, | |
| terminal: false, | |
| }); | |
| let hasInput = false; | |
| rl.on('line', (line) => { | |
| hasInput = true; | |
| if (line.trim()) this.handleCommand(line.trim()); | |
| }); | |
| rl.on('close', () => { | |
| if (!hasInput) process.exit(0); | |
| }); | |
| } else { | |
| this.prompt(); | |
| } | |
| } | |
| prompt() { | |
| if (!this.running) return; | |
| const rl = readline.createInterface({ | |
| input: process.stdin, | |
| output: process.stdout, | |
| terminal: true, | |
| completer: (line) => { | |
| const clean = line.replace(/^@/, "").toLowerCase(); | |
| const hits = Object.entries(hackerTools) | |
| .filter(([t]) => t.startsWith(clean)) | |
| .map(([t, fn]) => { | |
| const desc = (typeof fn === 'object' && fn.description) || ''; | |
| return desc ? `@${t}| (${desc.substring(0, 40)})` : `@${t}|`; | |
| }); | |
| return [hits.length ? hits : [], line]; | |
| } | |
| }); | |
| rl.question(`${c("cyan")}⚡${R} `, (ans) => { | |
| rl.close(); | |
| if (ans.trim()) this.handleCommand(ans.trim()); | |
| else this.prompt(); | |
| }); | |
| } | |
| async handleCommand(cmd) { | |
| const p = cmd.trim().split(/\s+/); | |
| const op = p[0]?.toLowerCase(), args = p.slice(1); | |
| switch (op) { | |
| case "spawn": case "s": this.am.spawn(args[0], args[1], args.slice(2).join(" ")); break; | |
| case "list": case "ls": log.info(`Agents: ${this.am.list.map(a => `${a.name}[${this.am.agents.get(a.id).status}]`).join(", ")}`); break; | |
| case "broadcast": case "b": { const f = this.am.list[0]?.id; if (f) this.am.broadcast(f, args.join(" ")); break; } | |
| case "debate": case "d": this.am.debate(args.join(" ") || "what should we build?"); break; | |
| case "evolve": case "e": this.am.evolveAll(); break; | |
| case "clear": case "c": this.log = []; break; | |
| case "quit": case "exit": case "q": this.running = false; log.ok("Bye."); process.exit(0); | |
| case "tools": | |
| const names = Object.keys(hackerTools).sort(); | |
| log.art(`\n${B}${c("green")}PHANTOM TOOLS (${names.length})${R}`); | |
| for (let i = 0; i < names.length; i += 4) console.log(` ${names.slice(i, i + 4).map(n => `${c("cyan")}${n.padEnd(20)}${R}`).join("")}`); | |
| console.log(""); | |
| break; | |
| case "command": | |
| console.log(`\n${B}${c("green")}PHANTOM TASKS${R}`); | |
| console.log(`${D}Core commands:${R}`); | |
| console.log(` ${c("green")}help${R} / ${c("green")}?${R} / ${c("green")}command${R} — show this help`); | |
| console.log(` ${c("green")}tools${R} — list all ${Object.keys(hackerTools).length} tools`); | |
| console.log(` ${c("green")}clear${R} / ${c("green")}cls${R} — clear screen`); | |
| console.log(` ${c("green")}quit${R} / ${c("green")}exit${R} — exit Phantom`); | |
| console.log(`\n${D}Type naturally (e.g. "hackerone programs", "recon example.com")${R}\n`); | |
| break; | |
| case "help": case "h": case "?": | |
| console.log(`\n${B}${c("green")}PHANTOM COMMANDS${R}`); | |
| console.log(` ${c("green")} /help${R} — show this help\n ${c("green")} /command${R} — list all commands`); | |
| console.log(` ${c("green")} /tools${R} — list ${Object.keys(hackerTools).length} tools`); | |
| console.log(` ${c("green")} /gui${R} — start web dashboard (port 8080)`); | |
| console.log(` ${c("green")} /api${R} — start REST API (port 9090)`); | |
| console.log(` ${c("green")} /model${R} — show/switch LLM`); | |
| console.log(` ${c("green")} /clear${R} — clear screen\n ${c("green")} /stop${R} — cancel current operation`); | |
| console.log(` ${c("green")} /queue${R} — show queued inputs\n ${c("green")} /flushqueue${R} — clear all queued inputs`); | |
| console.log(` ${c("green")} /delegate${R} — delegate task to agent\n ${c("green")} /talk${R} <a> — talk directly to an agent`); | |
| console.log(` ${c("green")} /agents${R} — list team with status\n ${c("green")} /save${R} <n> — save session`); | |
| console.log(` ${c("green")} /load${R} <n> — load session\n ${c("green")} /quit${R} — exit\n`); | |
| console.log(`${D}Type anything to chat. Use \\ for multi-line.${R}`); | |
| console.log(`${D}The AI auto-uses tools via @tool_name|args syntax.${R}`); | |
| console.log(`${D}@pipe${R} — chain tools: @pipe|subfinder|example.com|httpx`); | |
| console.log(`${D}@schedule${R} — scheduled scans: @schedule|daily|recon|target`); | |
| console.log(`${D}@scope${R} — manage targets: @scope|add|example.com`); | |
| console.log(`${D}@workspace_write${R} — save findings: @workspace_write|key|value`); | |
| console.log(`${D}@self_evolve${R} — run auto-evolution pipeline`); | |
| console.log(`${D}@self_evolve|status${R} — show evolution state`); | |
| console.log(`${D}TAB${R} — cycle suggestions, autocomplete @tool`); | |
| console.log(`${D}↑↓${R} — navigate suggestions / history`); | |
| console.log(`${D}↩${R} — accept highlighted suggestion`); | |
| console.log(`${D}ESC${R} — dismiss suggestions`); | |
| console.log(`${D}--quiet${R} — suppress banner/status (env: PHANTOM_QUIET)\n`); | |
| break; | |
| default: | |
| // Try natural language parsing for unrecognized commands | |
| const nlResult = await this.parseNaturalLanguage(cmd); | |
| if (nlResult) { | |
| if (typeof nlResult === "string" && (nlResult.startsWith("hackerone") || nlResult.startsWith("bugcrowd") || nlResult.startsWith("bugbounty") || nlResult.startsWith("recon ") || nlResult.startsWith("port_scan") || nlResult.startsWith("ssl_check") || nlResult.startsWith("whois") || nlResult.startsWith("cve_search") || nlResult.startsWith("searchsploit") || nlResult.startsWith("crawl") || nlResult.startsWith("whatweb") || nlResult.startsWith("mission auto") || nlResult.startsWith("dns_lookup") || nlResult.startsWith("sub_enum") || nlResult.startsWith("hash ") || nlResult.startsWith("decode ") || nlResult.startsWith("wayback") || nlResult.startsWith("cert_expiry") || nlResult.startsWith("http_headers") || nlResult.startsWith("dir_bruteforce") || nlResult.startsWith("xss_scan") || nlResult.startsWith("sql_detect") || nlResult.startsWith("open_redirect") || nlResult.startsWith("shodan_search") || nlResult.startsWith("email_breach") || nlResult.startsWith("github_dork"))) { | |
| console.log(nlResult); | |
| } else if (typeof nlResult === "string" && nlResult.startsWith("run ")) { | |
| const result = this.runMissionTask(nlResult.slice(4).trim()); | |
| console.log(result); | |
| } else if (nlResult === "command") { | |
| console.log(`\n${B}${c("green")}PHANTOM TASKS${R}`); | |
| console.log(`${D}Core commands:${R}`); | |
| console.log(` ${c("green")}help${R} / ${c("green")}?${R} / ${c("green")}command${R} — show this help`); | |
| console.log(` ${c("green")}tools${R} — list all ${Object.keys(hackerTools).length} tools`); | |
| console.log(` ${c("green")}clear${R} / ${c("green")}cls${R} — clear screen`); | |
| console.log(` ${c("green")}quit${R} / ${c("green")}exit${R} — exit Phantom`); | |
| console.log(`\n${D}Type naturally (e.g. "hackerone programs", "recon example.com")${R}\n`); | |
| } else { | |
| console.log(nlResult); | |
| } | |
| } else { | |
| console.log(`? ${cmd}`); | |
| } | |
| } | |
| if (this.running) this.prompt(); | |
| } | |
| } | |
| // ── UI: Conversational REPL (Claude Code / Hermes style) ── | |
| class ConversationalUI { | |
| constructor(am) { | |
| this.am = am; | |
| this.bus = EventBus.i; | |
| this.llm = am.llm; | |
| globalThis.__phantomManager = am; | |
| this.running = true; | |
| this.agent = null; | |
| this.conversation = []; | |
| this.logLines = []; | |
| this.inputBuf = ""; | |
| this.inputHistory = []; | |
| this.historyIdx = 0; | |
| this.cursorPos = 0; | |
| this.inputLines = []; | |
| this.inputHandler = null; | |
| this.promptQueue = []; // inputs queued while agent is busy | |
| this._busy = false; // agent is processing | |
| this._queueBuf = ""; // partial input during busy mode | |
| this._queueHandler = null; // stdin listener during busy mode | |
| // ── Session stats ── | |
| this.startTime = Date.now(); | |
| this.lastResponseTime = null; | |
| this.responseCount = 0; | |
| this.tokensUsed = 0; // estimated | |
| this.compressions = 0; | |
| this._toolCallCounter = 0; // total tool calls this session | |
| this._growFacts = 0; // auto-extracted facts this session | |
| this._growCycle = 0; // grow cycles completed | |
| // ── Evolution XP ── | |
| this.evolutionXP = 0; | |
| this.evolutionMaxXP = 100; | |
| // ── Suggestion engine ── | |
| this.suggestions = []; // current list of matching suggestions | |
| this.selSuggestion = -1; // -1 = none selected, 0+ = index into suggestions | |
| this.suggestionActive = false; // whether suggestion bar is visible | |
| this._suggestionBarHeight = 0; // lines drawn for suggestion bar (for cleanup) | |
| // Build command list once | |
| this._commandList = [ | |
| ["help", "show this help"], | |
| ["h", "alias for help"], | |
| ["command", "list all commands"], | |
| ["tools", "list all tools"], | |
| ["gui", "start web dashboard (port 8080)"], | |
| ["dashboard", "alias for gui"], | |
| ["api", "start REST API server (port 9090)"], | |
| ["rest", "alias for api"], | |
| ["model", "show/switch LLM provider"], | |
| ["clear", "clear screen"], | |
| ["c", "alias for clear"], | |
| ["delegate", "delegate task to agent"], | |
| ["del", "alias for delegate"], | |
| ["talk", "talk directly to an agent"], | |
| ["t", "alias for talk"], | |
| ["agents", "list team with status"], | |
| ["save", "save session"], | |
| ["load", "load session"], | |
| ["quit", "exit Phantom"], | |
| ["q", "alias for quit"], | |
| ["exit", "alias for quit"], | |
| ]; | |
| // ── TUI init ── | |
| this.tui = new TUI({ onExit: () => { this.running = false; try { raw(false); } catch {} } }); | |
| // Route console.log through TUI when active (auto-redraws input bar after each write) | |
| const _origLog = console.log.bind(console); | |
| const _self = this; | |
| console.log = function(...args) { | |
| const text = args.map(a => typeof a === "string" ? a : String(a)).join(" "); | |
| if (_self.tui?.active) { _self.tui.log(text); } | |
| else { _origLog(text); } | |
| }; | |
| } | |
| log(msg) { this.logLines.push(msg); if (this.logLines.length > 1000) this.logLines.shift(); } | |
| // ── Suggestion Engine ──────────────────────────────────── | |
| _getToolNameList() { | |
| // Return tool names with descriptions, using this.agent?.tools or hackerTools | |
| const src = this.agent?.tools || {}; | |
| if (Object.keys(src).length === 0) { | |
| return Object.entries(hackerTools).map(([n, fn]) => [n, (typeof fn === 'object' && fn.description) || '']); | |
| } | |
| return Object.entries(src).map(([n, t]) => [n, t.description || '']); | |
| } | |
| // ─── NATURAL LANGUAGE PARSER (works without LLM) ─── | |
| parseNaturalLanguage(input) { | |
| const text = input.trim().toLowerCase(); | |
| // Mission/task commands | |
| if (text.includes("task") || text.includes("list tasks")) { | |
| return this.runMissionTasks(); | |
| } | |
| if (text.startsWith("run ") || text.startsWith("execute ")) { | |
| const taskPart = text.replace(/^(run|execute)\s+/, ""); | |
| return this.runMissionTask(taskPart); | |
| } | |
| // HackerOne / Bugcrowd / Bug Bounty | |
| if (text.includes("hackerone") || text.includes("bugcrowd") || text.includes("bug bounty") || text.includes("bugbounty")) { | |
| // Unified bugbounty tool | |
| if (text.includes("bugcrowd") || text.includes("bugbounty")) { | |
| if (text.includes("program")) return hackerTools.bugbounty("bugcrowd programs"); | |
| if (text.includes("scope")) { | |
| const match = text.match(/(?:scope|targets?)\s+(\S+)/); | |
| return match ? hackerTools.bugbounty(`bugcrowd scope ${match[1]}`) : "Usage: bugcrowd scope <program_uuid>"; | |
| } | |
| if (text.includes("submission")) return hackerTools.bugbounty("bugcrowd submissions"); | |
| return hackerTools.bugbounty("bugcrowd programs"); | |
| } | |
| // HackerOne specific | |
| if (text.includes("program")) return hackerTools.hackerone("programs"); | |
| if (text.includes("scope")) { | |
| const match = text.match(/scope\s+(\S+)/); | |
| return match ? hackerTools.hackerone(`scope ${match[1]}`) : "Usage: hackerone scope <program>"; | |
| } | |
| if (text.includes("report")) return hackerTools.hackerone("reports"); | |
| return hackerTools.hackerone("programs"); | |
| } | |
| // Recon | |
| if (text.includes("recon") || text.includes("scan") || text.includes("enumerate")) { | |
| const targetMatch = text.match(/(?:recon|scan|enumerate)\s+(\S+)/) || text.match(/(\S+\.\S+)/); | |
| const target = targetMatch?.[1] || ""; | |
| if (target) { | |
| if (text.includes("subdomain")) return hackerTools.sub_enum(target); | |
| if (text.includes("dns")) return hackerTools.dns_lookup(target); | |
| if (text.includes("http") || text.includes("header")) return hackerTools.http_headers(target); | |
| if (text.includes("port")) return hackerTools.port_scan(target); | |
| if (text.includes("ssl")) return hackerTools.ssl_check(target); | |
| if (text.includes("whois")) return hackerTools.whois(target); | |
| return hackerTools.recon(target); | |
| } | |
| return "Please specify a target domain (e.g., 'recon example.com')"; | |
| } | |
| // Network tools | |
| if (text.includes("port scan") || text.includes("portscan")) { | |
| const targetMatch = text.match(/(\S+\.\S+)/); | |
| return targetMatch ? hackerTools.port_scan(targetMatch[1]) : "Usage: port scan <target>"; | |
| } | |
| if (text.includes("ssl") || text.includes("cert")) { | |
| const targetMatch = text.match(/(\S+\.\S+)/); | |
| return targetMatch ? hackerTools.ssl_check(targetMatch[1]) : "Usage: ssl check <target>"; | |
| } | |
| if (text.includes("whois")) { | |
| const targetMatch = text.match(/(\S+\.\S+)/); | |
| return targetMatch ? hackerTools.whois(targetMatch[1]) : "Usage: whois <target>"; | |
| } | |
| // Vulnerability | |
| if (text.includes("cve")) { | |
| const query = text.replace(/cve\s+/, ""); | |
| return query ? hackerTools.cve_search(query) : "Usage: cve <query>"; | |
| } | |
| if (text.includes("exploit")) { | |
| const query = text.replace(/exploit\s+/, ""); | |
| return query ? hackerTools.searchsploit(query) : "Usage: exploit <query>"; | |
| } | |
| // Web | |
| if (text.includes("crawl")) { | |
| const targetMatch = text.match(/(\S+\.\S+)/); | |
| return targetMatch ? hackerTools.crawl(targetMatch[1]) : "Usage: crawl <url>"; | |
| } | |
| if (text.includes("tech") || text.includes("whatweb")) { | |
| const targetMatch = text.match(/(\S+\.\S+)/); | |
| return targetMatch ? hackerTools.whatweb(targetMatch[1]) : "Usage: tech detect <target>"; | |
| } | |
| // Mission auto | |
| if (text.includes("auto") && text.includes("mission")) { | |
| const match = text.match(/mission\s+(\S+)/); | |
| return match ? hackerTools.mission(`auto ${match[1]}`) : "Usage: mission auto <mission-id>"; | |
| } | |
| // OSINT | |
| if (text.includes("dns enum") || text.includes("dns enumeration")) { | |
| const targetMatch = text.match(/(\S+\.\S+)/); | |
| return targetMatch ? hackerTools.sub_enum(targetMatch[1]) : "Usage: dns enum <target>"; | |
| } | |
| return null; // No match - let LLM handle | |
| } | |
| updateSuggestions() { | |
| const buf = this.inputBuf; | |
| this.suggestions = []; | |
| this.selSuggestion = -1; | |
| if (!buf || buf.length === 0) { | |
| // Empty input: show recently used tools (from inputHistory) | |
| this.suggestionActive = false; | |
| return; | |
| } | |
| // ── Mode detection ── | |
| const lastSegment = buf.split(/[\s|]/).pop() || ""; | |
| const startsAt = buf.endsWith(" ") || buf.endsWith("|") ? "" : lastSegment; | |
| // ── @tool completion ── | |
| const atMatch = buf.match(/@([\w-]*)$/); | |
| if (atMatch) { | |
| const prefix = atMatch[1].toLowerCase(); | |
| const tools = this._getToolNameList(); | |
| this.suggestions = tools | |
| .filter(([n]) => n.startsWith(prefix) && n !== prefix) | |
| .map(([n, d]) => ({ label: `@${n}|`, desc: d.substring(0, 50) })) | |
| .slice(0, 12); | |
| this.suggestionActive = this.suggestions.length > 0; | |
| if (this.suggestions.length === 1) this.selSuggestion = 0; | |
| return; | |
| } | |
| // ── /command completion ── | |
| const cmdMatch = buf.match(/^\/([\w]*)$/); | |
| if (cmdMatch) { | |
| const prefix = cmdMatch[1].toLowerCase(); | |
| this.suggestions = this._commandList | |
| .filter(([n]) => n.startsWith(prefix) && n !== prefix) | |
| .map(([n, d]) => ({ label: `/${n} `, desc: d })) | |
| .slice(0, 12); | |
| this.suggestionActive = this.suggestions.length > 0; | |
| if (this.suggestions.length === 1) this.selSuggestion = 0; | |
| return; | |
| } | |
| // ── Tool name completion (no @ prefix) ── | |
| // Match by prefix, substring, or description keyword | |
| const toolNames = this._getToolNameList(); | |
| const bareMatch = toolNames.filter(([n, d]) => { | |
| if (n.startsWith(startsAt) && startsAt.length >= 2) return true; | |
| if (startsAt.length >= 3 && n.includes(startsAt)) return true; | |
| if (startsAt.length >= 4 && d.toLowerCase().includes(startsAt)) return true; | |
| return false; | |
| }).slice(0, 8); | |
| if (bareMatch.length > 0 && bareMatch.length <= 10) { | |
| this.suggestions = bareMatch | |
| .map(([n, d]) => ({ label: `@${n}|`, desc: d.substring(0, 50) })) | |
| .slice(0, 8); | |
| this.suggestionActive = true; | |
| if (this.suggestions.length === 1) this.selSuggestion = 0; | |
| return; | |
| } | |
| // ── Pipe / sub-command hints ── | |
| if (buf.includes(" | ") || buf.endsWith("|")) { | |
| const tools = this._getToolNameList(); | |
| this.suggestions = tools | |
| .filter(([n, d]) => d.toLowerCase().includes("recon") || d.toLowerCase().includes("scan") || d.toLowerCase().includes("search")) | |
| .map(([n, d]) => ({ label: `@${n}|`, desc: d.substring(0, 50) })) | |
| .slice(0, 5); | |
| this.suggestionActive = this.suggestions.length > 0; | |
| return; | |
| } | |
| this.suggestionActive = false; | |
| } | |
| // ── Natural Language Parser ───────────────────────────────── | |
| // Maps common phrases to existing tool/command calls | |
| parseNaturalLanguage(input) { | |
| const lower = input.trim().toLowerCase(); | |
| // List tasks / show tasks | |
| if (lower === "list tasks" || lower === "show tasks" || lower === "tasks") { | |
| return "/command"; // Special command that will list tasks | |
| } | |
| // Run task aliases | |
| if (lower.startsWith("run ")) { | |
| const task = lower.slice(4).trim(); | |
| if (task.startsWith("h1:") || task.startsWith("hackerone")) return `hackerone programs`; | |
| if (task.startsWith("recon:") || task.startsWith("recon ")) return `recon ${task.replace("recon:", "").replace("recon ", "").trim()}`; | |
| if (task.startsWith("net:") || task.startsWith("port scan")) return `port_scan ${task.replace("net:", "").replace("port scan", "").trim()}`; | |
| if (task.startsWith("vuln:") || task.startsWith("cve ")) return `cve_search ${task.replace("vuln:", "").replace("cve ", "").trim()}`; | |
| } | |
| // Direct hackerone commands | |
| if (lower.startsWith("hackerone ")) { | |
| const sub = lower.slice(10).trim(); | |
| if (sub === "programs" || sub === "list") return "hackerone programs"; | |
| if (sub.startsWith("scope ")) return `hackerone scope ${sub.slice(6).trim()}`; | |
| if (sub.startsWith("reports ")) return `hackerone reports ${sub.slice(8).trim()}`; | |
| if (sub.startsWith("report ")) return `hackerone report ${sub.slice(7).trim()}`; | |
| if (sub === "me" || sub === "whoami") return "hackerone me"; | |
| if (sub === "test" || sub === "auth") return "hackerone test"; | |
| } | |
| // Direct recon commands | |
| if (lower.startsWith("recon ")) { | |
| const target = lower.slice(6).trim(); | |
| return `recon ${target}`; | |
| } | |
| if (lower.startsWith("recon subdomain ")) { | |
| const target = lower.slice(16).trim(); | |
| return `sub_enum ${target}`; | |
| } | |
| // Port scan | |
| if (lower.startsWith("port scan ")) { | |
| const target = lower.slice(10).trim(); | |
| return `port_scan ${target}`; | |
| } | |
| if (lower.startsWith("scan ")) { | |
| const target = lower.slice(5).trim(); | |
| return `port_scan ${target}`; | |
| } | |
| // SSL check | |
| if (lower.startsWith("ssl check ") || lower.startsWith("ssl ")) { | |
| const target = lower.replace("ssl check ", "").replace("ssl ", "").trim(); | |
| return `ssl_check ${target}`; | |
| } | |
| // CVE search | |
| if (lower.startsWith("cve ") || lower.startsWith("cve search ")) { | |
| const query = lower.replace("cve search ", "").replace("cve ", "").trim(); | |
| return `cve_search ${query}`; | |
| } | |
| // Exploit | |
| if (lower.startsWith("exploit ") || lower.startsWith("exploit search ")) { | |
| const query = lower.replace("exploit search ", "").replace("exploit ", "").trim(); | |
| return `searchsploit ${query}`; | |
| } | |
| // Crawl | |
| if (lower.startsWith("crawl ") || lower.startsWith("spider ")) { | |
| const url = lower.replace("crawl ", "").replace("spider ", "").trim(); | |
| return `crawl ${url}`; | |
| } | |
| // Tech detect | |
| if (lower.startsWith("tech detect ") || lower.startsWith("tech ")) { | |
| const target = lower.replace("tech detect ", "").replace("tech ", "").trim(); | |
| return `whatweb ${target}`; | |
| } | |
| // Mission auto | |
| if (lower.startsWith("mission auto ")) { | |
| const mission = lower.slice(13).trim(); | |
| return `mission auto ${mission}`; | |
| } | |
| if (lower.startsWith("auto ")) { | |
| const mission = lower.slice(5).trim(); | |
| return `mission auto ${mission}`; | |
| } | |
| // DNS lookup | |
| if (lower.startsWith("dns ")) { | |
| const target = lower.slice(4).trim(); | |
| return `dns_lookup ${target}`; | |
| } | |
| // WHOIS | |
| if (lower.startsWith("whois ")) { | |
| const target = lower.slice(6).trim(); | |
| return `whois ${target}`; | |
| } | |
| // Hash | |
| if (lower.startsWith("hash ")) { | |
| const data = lower.slice(5).trim(); | |
| return `hash ${data}`; | |
| } | |
| // Decode | |
| if (lower.startsWith("decode ")) { | |
| const data = lower.slice(7).trim(); | |
| return `decode ${data}`; | |
| } | |
| // Subdomain enum | |
| if (lower.startsWith("subdomain ") || lower.startsWith("subdomains ")) { | |
| const target = lower.replace("subdomain ", "").replace("subdomains ", "").trim(); | |
| return `sub_enum ${target}`; | |
| } | |
| // Wayback | |
| if (lower.startsWith("wayback ") || lower.startsWith("archive ")) { | |
| const target = lower.replace("wayback ", "").replace("archive ", "").trim(); | |
| return `wayback ${target}`; | |
| } | |
| // Cert expiry | |
| if (lower.startsWith("cert ")) { | |
| const target = lower.slice(5).trim(); | |
| return `cert_expiry ${target}`; | |
| } | |
| // HTTP headers | |
| if (lower.startsWith("headers ")) { | |
| const target = lower.slice(8).trim(); | |
| return `http_headers ${target}`; | |
| } | |
| // Dir bruteforce | |
| if (lower.startsWith("dir brute") || lower.startsWith("dirb ")) { | |
| const target = lower.replace("dir brute", "").replace("dirb ", "").trim(); | |
| return `dir_bruteforce ${target}`; | |
| } | |
| // XSS scan | |
| if (lower.startsWith("xss ")) { | |
| const target = lower.slice(4).trim(); | |
| return `xss_scan ${target}`; | |
| } | |
| // SQLi | |
| if (lower.startsWith("sqli ") || lower.startsWith("sql ")) { | |
| const target = lower.replace("sqli ", "").replace("sql ", "").trim(); | |
| return `sql_detect ${target}`; | |
| } | |
| // Open redirect | |
| if (lower.startsWith("redirect ")) { | |
| const target = lower.slice(9).trim(); | |
| return `open_redirect ${target}`; | |
| } | |
| // Shodan | |
| if (lower.startsWith("shodan ")) { | |
| const query = lower.slice(7).trim(); | |
| return `shodan_search ${query}`; | |
| } | |
| // Email breach | |
| if (lower.startsWith("breach ") || lower.startsWith("email breach ")) { | |
| const email = lower.replace("email breach ", "").replace("breach ", "").trim(); | |
| return `email_breach ${email}`; | |
| } | |
| // GitHub dork | |
| if (lower.startsWith("github dork ") || lower.startsWith("gh dork ")) { | |
| const query = lower.replace("github dork ", "").replace("gh dork ", "").trim(); | |
| return `github_dork ${query}`; | |
| } | |
| // Subdomain takeover | |
| if (lower.startsWith("takeover ") || lower.startsWith("sub takeover ")) { | |
| const target = lower.replace("sub takeover ", "").replace("takeover ", "").trim(); | |
| return `sub_takeover ${target}`; | |
| } | |
| // VirusTotal | |
| if (lower.startsWith("vt ") || lower.startsWith("virustotal ")) { | |
| const query = lower.replace("virustotal ", "").replace("vt ", "").trim(); | |
| return `vt_check ${query}`; | |
| } | |
| // Hash crack | |
| if (lower.startsWith("crack ")) { | |
| const hash = lower.slice(6).trim(); | |
| return `hash_crack ${hash}`; | |
| } | |
| // JWT decode | |
| if (lower.startsWith("jwt ")) { | |
| const token = lower.slice(4).trim(); | |
| return `jwt_decode ${token}`; | |
| } | |
| // Robots.txt | |
| if (lower.startsWith("robots ")) { | |
| const target = lower.slice(7).trim(); | |
| return `robots_txt ${target}`; | |
| } | |
| // Email verify | |
| if (lower.startsWith("verify email ") || lower.startsWith("email verify ")) { | |
| const email = lower.replace("verify email ", "").replace("email verify ", "").trim(); | |
| return `email_verify ${email}`; | |
| } | |
| // GeoIP | |
| if (lower.startsWith("geoip ")) { | |
| const target = lower.slice(6).trim(); | |
| return `geoip ${target}`; | |
| } | |
| // DNS zone | |
| if (lower.startsWith("dns zone ")) { | |
| const target = lower.slice(9).trim(); | |
| return `dns_zone ${target}`; | |
| } | |
| // HTTP methods | |
| if (lower.startsWith("methods ")) { | |
| const target = lower.slice(8).trim(); | |
| return `http_methods ${target}`; | |
| } | |
| // Reverse DNS | |
| if (lower.startsWith("rdns ") || lower.startsWith("reverse dns ")) { | |
| const target = lower.replace("rdns ", "").replace("reverse dns ", "").trim(); | |
| return `reverse_dns ${target}`; | |
| } | |
| // CORS test | |
| if (lower.startsWith("cors ")) { | |
| const target = lower.slice(5).trim(); | |
| return `cors_test ${target}`; | |
| } | |
| // Report save | |
| if (lower.startsWith("save report ")) { | |
| const parts = lower.slice(12).split("|"); | |
| if (parts.length >= 2) return `report_save ${parts[0].trim()}|${parts.slice(1).join("|")}`; | |
| } | |
| // Report export | |
| if (lower.startsWith("export report ")) { | |
| const parts = lower.slice(14).split("|"); | |
| if (parts.length >= 2) return `report_export ${parts[0].trim()}|${parts.slice(1).join("|")}`; | |
| } | |
| // Playbook commands | |
| if (lower.startsWith("playbook ")) { | |
| const sub = lower.slice(9).trim(); | |
| if (sub.startsWith("run ")) return `playbook_run ${sub.slice(4).trim()}`; | |
| if (sub === "list") return "playbook_list"; | |
| if (sub.startsWith("create ")) return `playbook_create ${sub.slice(7).trim()}`; | |
| } | |
| // Scope commands | |
| if (lower.startsWith("scope ")) { | |
| const sub = lower.slice(6).trim(); | |
| if (sub === "list" || sub === "") return "scope list"; | |
| if (sub.startsWith("add ")) return `scope add ${sub.slice(4).trim()}`; | |
| if (sub.startsWith("remove ")) return `scope remove ${sub.slice(7).trim()}`; | |
| } | |
| // Schedule commands | |
| if (lower.startsWith("schedule ")) { | |
| const sub = lower.slice(9).trim(); | |
| if (sub === "list") return "schedule list"; | |
| if (sub.startsWith("daily ")) return `schedule daily ${sub.slice(6).trim()}`; | |
| if (sub.startsWith("hourly ")) return `schedule hourly ${sub.slice(7).trim()}`; | |
| if (sub.startsWith("remove ")) return `schedule remove ${sub.slice(7).trim()}`; | |
| } | |
| // Plugin commands | |
| if (lower.startsWith("plugin ")) { | |
| const sub = lower.slice(7).trim(); | |
| if (sub === "list") return "plugin_load list"; | |
| if (sub.startsWith("load ")) return `plugin_load ${sub.slice(5).trim()}`; | |
| if (sub.startsWith("create ")) return `plugin_create ${sub.slice(7).trim()}`; | |
| } | |
| // Config commands | |
| if (lower.startsWith("config ")) { | |
| const sub = lower.slice(7).trim(); | |
| if (sub === "list" || sub === "") return "config list"; | |
| if (sub.startsWith("get ")) return `config get ${sub.slice(4).trim()}`; | |
| if (sub.startsWith("set ")) return `config set ${sub.slice(4).trim()}`; | |
| if (sub.startsWith("path")) return "config path"; | |
| } | |
| // LLM config | |
| if (lower.startsWith("llm ") || lower.startsWith("model ")) { | |
| const sub = lower.replace("llm ", "").replace("model ", "").trim(); | |
| if (sub === "list") return "llm_config list"; | |
| if (sub.startsWith("set ")) return `llm_config set ${sub.slice(4).trim()}`; | |
| } | |
| // Help | |
| if (lower === "help" || lower === "?" || lower === "how") { | |
| return "/help"; | |
| } | |
| // Clear | |
| if (lower === "clear" || lower === "cls") { | |
| return "/clear"; | |
| } | |
| // Agents | |
| if (lower === "agents" || lower === "list agents") { | |
| return "/agents"; | |
| } | |
| // Save/load session | |
| if (lower.startsWith("save ")) { | |
| const name = lower.slice(5).trim(); | |
| return `session_save ${name}`; | |
| } | |
| if (lower.startsWith("load ")) { | |
| const name = lower.slice(5).trim(); | |
| return `session_load ${name}`; | |
| } | |
| return null; // No match | |
| } | |
| renderSuggestionBar() { | |
| if (!this.suggestionActive || this.suggestions.length === 0) return; | |
| // In TUI mode, suggestion bar needs updated rendering — skip for now | |
| if (this.tui?.active) return; | |
| const { cols } = getSize(); | |
| const maxItems = Math.min(this.suggestions.length, 6); | |
| const pad = " "; | |
| for (let i = 0; i < maxItems; i++) { | |
| const s = this.suggestions[i]; | |
| const selected = i === this.selSuggestion; | |
| const arrow = selected ? `${c("green")}▶${R}` : pad; | |
| const sep = s.desc ? ` ${c("dim")}— ${s.desc}${R}` : ""; | |
| let line = `\n${arrow} ${c("cyan")}${s.label}${R}${sep}`; | |
| if (line.length > cols - 2) line = line.substring(0, cols - 5) + "…"; | |
| process.stdout.write(line); | |
| } | |
| if (this.suggestions.length > maxItems) { | |
| process.stdout.write(`\n${pad}${c("dim")}… +${this.suggestions.length - maxItems} more${R}`); | |
| } | |
| // Footer hint | |
| if (this.selSuggestion >= 0) { | |
| process.stdout.write(`\n${c("dim")}⏎ accept ↑↓ cycle TAB next esc dismiss${R}`); | |
| } else { | |
| process.stdout.write(`\n${c("dim")}TAB cycle ↑↓ select esc dismiss${R}`); | |
| } | |
| } | |
| acceptSuggestion() { | |
| if (this.selSuggestion < 0 || this.selSuggestion >= this.suggestions.length) return; | |
| const s = this.suggestions[this.selSuggestion]; | |
| // Determine what to replace | |
| const atMatch = this.inputBuf.match(/@([\w-]*)$/); | |
| if (atMatch) { | |
| this.inputBuf = this.inputBuf.slice(0, atMatch.index) + s.label; | |
| } else { | |
| const cmdMatch = this.inputBuf.match(/^\/([\w]*)$/); | |
| if (cmdMatch) { | |
| this.inputBuf = s.label; | |
| } else { | |
| // Replace last word segment | |
| const parts = this.inputBuf.split(/[\s|]/); | |
| parts[parts.length - 1] = s.label; | |
| this.inputBuf = parts.join(" "); | |
| } | |
| } | |
| this.cursorPos = this.inputBuf.length; | |
| this.suggestions = []; | |
| this.suggestionActive = false; | |
| this.selSuggestion = -1; | |
| } | |
| cycleSuggestion(dir) { | |
| if (this.suggestions.length === 0) { | |
| // No active suggestions — trigger a fresh update | |
| this.updateSuggestions(); | |
| if (this.suggestions.length > 0) { | |
| this.selSuggestion = dir > 0 ? 0 : this.suggestions.length - 1; | |
| } | |
| return; | |
| } | |
| const n = this.suggestions.length; | |
| if (dir > 0) { | |
| // Forward | |
| if (this.selSuggestion < n - 1) { | |
| this.selSuggestion++; | |
| } else { | |
| // Wrap: if at end, accept the first suggestion | |
| this.selSuggestion = 0; | |
| } | |
| } else { | |
| // Backward | |
| if (this.selSuggestion > 0) { | |
| this.selSuggestion--; | |
| } else { | |
| this.selSuggestion = n - 1; | |
| } | |
| } | |
| } | |
| formatToolResult(name, result) { | |
| const lines = (result || "").split("\n").filter(l => l.trim()); | |
| if (lines.length <= 4) return lines.map(l => ` ${c("dim")}→ ${l}${R}`).join("\n"); | |
| return lines.slice(0, 4).map(l => ` ${c("dim")}→ ${l.substring(0, 120)}${R}`).join("\n") + | |
| `\n ${c("dim")}… +${lines.length - 4} more lines${R}`; | |
| } | |
| renderResponse(response) { | |
| if (!response) return; | |
| const lines = response.split("\n"); | |
| let inCode = false; | |
| let codeLang = ""; | |
| const logLine = (text) => { | |
| if (this.tui?.active) { | |
| this.tui.log(text); | |
| } else { | |
| process.stdout.write(text + "\n"); | |
| } | |
| }; | |
| for (const line of lines) { | |
| const trimmed = line.trimEnd(); | |
| const codeFence = trimmed.match(/^```(\w*)/); | |
| if (codeFence) { | |
| if (inCode) { | |
| inCode = false; | |
| } else { | |
| codeLang = codeFence[1] || ""; | |
| if (codeLang) { const _l = ` ${c("dim")}${codeLang}${R}`; logLine(_l); } | |
| inCode = true; | |
| } | |
| continue; | |
| } | |
| if (inCode) { | |
| logLine(` ${c("dim")}│${R} ${trimmed}`); | |
| continue; | |
| } | |
| // Tool calls shown during execution | |
| if (trimmed.startsWith("@") && trimmed.length < 80) { | |
| continue; | |
| } | |
| // Empty lines | |
| if (trimmed === "") { | |
| logLine(""); | |
| continue; | |
| } | |
| // Section headers (## or ###) | |
| const heading = trimmed.match(/^(#{2,3}|\[)\s*(.+?)\]?\s*$/); | |
| if (heading) { | |
| const text = heading[2].trim(); | |
| logLine(`\n${c("magenta")}${text}${R}`); | |
| continue; | |
| } | |
| // Bullet points | |
| if (trimmed.match(/^[\s]*[-*•]\s/)) { | |
| logLine(` ${c("cyan")}•${R} ${trimmed.replace(/^[\s]*[-*•]\s/, "")}`); | |
| continue; | |
| } | |
| // Numbered lists | |
| if (trimmed.match(/^\s*\d+[.)]\s/)) { | |
| logLine(` ${trimmed}`); | |
| continue; | |
| } | |
| // Default: normal text | |
| logLine(` ${trimmed}`); | |
| } | |
| } | |
| async start() { | |
| const toolCount = Object.keys(hackerTools).length; | |
| // ── TUI: enter alt-screen, draw fixed logo + input bar ── | |
| this.tui.setToolCount(toolCount); | |
| this.tui.enter(); | |
| // Startup info in conversation area | |
| const providerName = this.llm?.provider || "no-llm"; | |
| const ready = this.llm?.hasLLM ? `${c("green")}ready${R}` : `${c("yellow")}tools-only${R}`; | |
| this.tui.log(` ${c("dim")}${providerName} ${ready} · ${toolCount} tools${R}`); | |
| // ── Spawn agents ── | |
| if (this.am.count === 0) { | |
| // Main agent | |
| this.am.spawn("Phantom", "cybersecurity-ai", | |
| "You are Phantom, an autonomous cybersecurity AI. You operate with zero handholding — " + | |
| "the user gives a goal and you execute the full workflow: recon, scanning, analysis, exploitation, " + | |
| "reporting. You decide every step, call tools, iterate, and produce results without asking " + | |
| "what to do next. Put each tool call on its own line starting with @tool|args. " + | |
| "Be concise in explanations, thorough in execution. You have " + toolCount + " tools including " + | |
| "shell, sub_enum, port_scan, web_fetch, vuln_scan, and more. " + | |
| "CRITICAL: For complex tasks with multiple independent subtasks (e.g. recon multiple targets, " + | |
| "run different scans concurrently), use @parallel|task_description to auto-split and run on sub-agents. " + | |
| "You can also delegate directly via @delegate|agent_name|task." | |
| ); | |
| // Specialist sub-agents for parallel work | |
| this.am.spawn("Nova", "recon", | |
| "OSINT and reconnaissance specialist. Expert in DNS analysis, subdomain enumeration, " + | |
| "port scanning, WHOIS lookups, web crawling, and attack surface mapping. " + | |
| "Use tools: sub_enum, dns_lookup, whois, port_scan, http_headers, ssl_check, crawl, " + | |
| "reverse_dns, wayback, robots_txt, geoip, shodan_search." | |
| ); | |
| this.am.spawn("Orion", "exploit", | |
| "Vulnerability analysis and exploitation engineer. Expert in CVE research, " + | |
| "exploit matching, brute force testing, SQL injection, XSS, and penetration testing. " + | |
| "Use tools: cve_search, searchsploit, bruteforce, xss_scan, sql_detect, " + | |
| "open_redirect, dir_bruteforce, cors_test, fuzz, pwn." | |
| ); | |
| this.am.spawn("Vega", "defense", | |
| "Defensive security and monitoring analyst. Expert in log analysis, SSL/TLS audit, " + | |
| "CORS testing, JWT analysis, certificate checks, and security hardening. " + | |
| "Use tools: ssl_check, cert_expiry, http_headers, hash, decode, " + | |
| "jwt_decode, cors_test, dns_lookup, geoip." | |
| ); | |
| } | |
| this.agent = this.am.list[0]; | |
| // Background: show available providers and auto-evolve | |
| if (!process.env.PHANTOM_NO_EVOLVE) { | |
| startupEvolve().then(ev => { | |
| if (ev.wrappers_created > 0) { | |
| console.log(`${c("green")}⚡${R} Auto-evolve: ${ev.wrappers_created} new tool wrappers`); | |
| } | |
| }).catch(() => {}); | |
| } | |
| this.prompt(); | |
| } | |
| prompt() { | |
| if (!this.running) return; | |
| this.inputBuf = ""; | |
| this.historyIdx = this.inputHistory.length; | |
| this.cursorPos = 0; | |
| this.inputLines = []; | |
| this._ignoreInput = false; | |
| this.tui.setInput(""); | |
| raw(true); | |
| // Remove stale listener to prevent duplicate accumulation | |
| if (this.inputHandler) { | |
| try { process.stdin.removeListener("data", this.inputHandler); } catch {} | |
| } | |
| this.inputHandler = (buf) => this.onKey(buf); | |
| process.stdin.on("data", this.inputHandler); | |
| } | |
| onKey(buf) { | |
| if (!this.running) return; | |
| // Ignore input burst right after submit (paste split across data events) | |
| if (this._ignoreInput) return; | |
| const str = buf.toString(); | |
| // Up / Down arrows — suggestion navigation OR history | |
| if (str === "\x1b[A") { | |
| if (this.suggestionActive && this.suggestions.length > 0) { | |
| this.cycleSuggestion(-1); | |
| this.redrawLine(); | |
| return; | |
| } | |
| if (this.historyIdx > 0) { | |
| this.historyIdx--; | |
| this.inputBuf = this.inputHistory[this.historyIdx] || ""; | |
| this.cursorPos = this.inputBuf.length; | |
| } | |
| this.redrawLine(); | |
| return; | |
| } | |
| if (str === "\x1b[B") { | |
| if (this.suggestionActive && this.suggestions.length > 0) { | |
| this.cycleSuggestion(1); | |
| this.redrawLine(); | |
| return; | |
| } | |
| if (this.historyIdx < this.inputHistory.length - 1) { | |
| this.historyIdx++; | |
| this.inputBuf = this.inputHistory[this.historyIdx] || ""; | |
| } else { | |
| this.historyIdx = this.inputHistory.length; | |
| this.inputBuf = ""; | |
| } | |
| this.cursorPos = this.inputBuf.length; | |
| this.redrawLine(); | |
| return; | |
| } | |
| // Left / Right | |
| if (str === "\x1b[D") { this.cursorPos = Math.max(0, this.cursorPos - 1); this.redrawLine(); return; } | |
| if (str === "\x1b[C") { this.cursorPos = Math.min(this.inputBuf.length, this.cursorPos + 1); this.redrawLine(); return; } | |
| // Escape — dismiss suggestions | |
| if (str === "\x1b") { | |
| if (this.suggestionActive) { | |
| this.suggestions = []; | |
| this.suggestionActive = false; | |
| this.selSuggestion = -1; | |
| this.redrawLine(); | |
| } | |
| return; | |
| } | |
| // Home / End | |
| if (str === "\x1b[H" || str === "\x01") { this.cursorPos = 0; this.redrawLine(); return; } | |
| if (str === "\x1b[F" || str === "\x05") { this.cursorPos = this.inputBuf.length; this.redrawLine(); return; } | |
| // Delete | |
| if (str === "\x1b[3~") { | |
| if (this.cursorPos < this.inputBuf.length) { | |
| this.inputBuf = this.inputBuf.slice(0, this.cursorPos) + this.inputBuf.slice(this.cursorPos + 1); | |
| this.redrawLine(); | |
| } | |
| return; | |
| } | |
| // Ctrl+C / Ctrl+D | |
| if (str === "\x03") { this.cancel(); return; } | |
| if (str === "\x04") { | |
| if (this.inputBuf.length === 0) { raw(false); process.stdin.removeListener("data", this.inputHandler); this.stop(); return; } | |
| return; | |
| } | |
| // Backspace | |
| if (str === "\x7f" || str === "\b") { | |
| if (this.cursorPos > 0) { | |
| this.inputBuf = this.inputBuf.slice(0, this.cursorPos - 1) + this.inputBuf.slice(this.cursorPos); | |
| this.cursorPos--; | |
| this.redrawLine(); | |
| } | |
| return; | |
| } | |
| // TAB — suggestion cycle / autocomplete | |
| if (str === "\t" || str === "\x09") { | |
| if (this.suggestionActive && this.suggestions.length > 0) { | |
| // Cycle to next suggestion | |
| this.cycleSuggestion(1); | |
| this.redrawLine(); | |
| } else { | |
| // No active suggestions — trigger fresh update | |
| this.updateSuggestions(); | |
| if (this.suggestions.length === 1) { | |
| // Single match — accept immediately | |
| this.selSuggestion = 0; | |
| this.acceptSuggestion(); | |
| this.redrawLine(); | |
| } else if (this.suggestions.length > 1) { | |
| // Multiple — show suggestions | |
| this.selSuggestion = 0; | |
| this.redrawLine(); | |
| } | |
| } | |
| return; | |
| } | |
| // Ctrl+Space or Ctrl+N — accept selected suggestion | |
| if (str === "\x00" || str === "\x0E") { | |
| if (this.suggestionActive && this.selSuggestion >= 0) { | |
| this.acceptSuggestion(); | |
| this.redrawLine(); | |
| return; | |
| } | |
| } | |
| // Enter — accept suggestion if one is selected, otherwise submit | |
| if (str === "\r" || str === "\n") { | |
| if (this.suggestionActive && this.selSuggestion >= 0 && this.suggestions.length > 0) { | |
| this.acceptSuggestion(); | |
| this.redrawLine(); | |
| return; | |
| } | |
| // Multi-line continuation | |
| if (this.inputBuf.endsWith("\\")) { | |
| this.inputLines.push(this.inputBuf.slice(0, -1)); | |
| this.inputBuf = ""; | |
| this.cursorPos = 0; | |
| if (this.tui?.active) { | |
| this.redrawLine(); | |
| } else { | |
| process.stdout.write(`\n${c("green")}│${R} `); | |
| } | |
| return; | |
| } | |
| // Submit | |
| const fullInput = this.inputLines.concat([this.inputBuf]).join("\n").trim(); | |
| this.inputLines = []; | |
| this.inputBuf = ""; | |
| this.cursorPos = 0; | |
| if (this.tui?.active) this.tui.setInput(""); | |
| // If agent is busy, queue input silently | |
| if (this._busy) { | |
| // Dedup: skip if same as last queued item | |
| if (this.promptQueue.length > 0 && this.promptQueue[this.promptQueue.length - 1] === fullInput) { | |
| this.sayLine(`${c("yellow")}📥${R} Duplicate skipped`, "yellow"); | |
| } else if (fullInput) { | |
| this.promptQueue.push(fullInput); | |
| this.sayLine(`${c("yellow")}📥${R} Queued (${this.promptQueue.length})`, "yellow"); | |
| } | |
| return; | |
| } | |
| // Not busy — submit | |
| this.tui.log(""); // blank line to separate turn | |
| // Debounce: ignore residual data events from paste buffer | |
| this._ignoreInput = true; | |
| setTimeout(() => { this._ignoreInput = false; }, 50); | |
| if (!fullInput) { this.redrawLine(); return; } | |
| // Save to input history | |
| if (this.inputHistory.length === 0 || this.inputHistory[this.inputHistory.length - 1] !== fullInput) { | |
| this.inputHistory.push(fullInput); | |
| } | |
| this.handleInput(fullInput); | |
| return; | |
| } | |
| // Ctrl+V — paste from clipboard | |
| if (str === "\x16") { | |
| try { | |
| const { execSync } = $r("child_process"); | |
| const isTermux = !!(process.env.TERMUX_VERSION || process.env.PREFIX?.startsWith("/data/data/com.termux")); | |
| const pasteCmd = isTermux ? "termux-clipboard-get" : process.platform === "darwin" ? "pbpaste" : "xclip -selection clipboard -o"; | |
| const clip = execSync(pasteCmd, { encoding: "utf-8", timeout: 3000 }).toString(); | |
| if (clip) { | |
| const parts = clip.split("\n"); | |
| // First line goes to inputBuf, rest to inputLines (multi-line) | |
| this.inputBuf = this.inputBuf.slice(0, this.cursorPos) + parts[0] + this.inputBuf.slice(this.cursorPos); | |
| this.cursorPos += parts[0].length; | |
| for (let i = 1; i < parts.length; i++) { | |
| if (parts[i].length) this.inputLines.push(parts[i]); | |
| } | |
| this.redrawLine(); | |
| } | |
| } catch {} | |
| return; | |
| } | |
| // Paste detection: terminal dumped multi-char text (not a control sequence) | |
| if (str.length > 1) { | |
| const parts = str.split("\n"); | |
| // First line to inputBuf, rest to inputLines | |
| this.inputBuf = this.inputBuf.slice(0, this.cursorPos) + parts[0] + this.inputBuf.slice(this.cursorPos); | |
| this.cursorPos += parts[0].length; | |
| for (let i = 1; i < parts.length; i++) { | |
| if (parts[i].length || i < parts.length - 1) this.inputLines.push(parts[i]); | |
| } | |
| if (this._busy) { | |
| let display = this.inputBuf; | |
| if (this.inputLines.length > 0) { | |
| display += ` ${c("dim")}(+${this.inputLines.length} more)${R}`; | |
| } | |
| this.tui.setInput(display); | |
| } else { this.redrawLine(); } | |
| return; | |
| } | |
| // Regular character | |
| if (str.length === 1 && str.charCodeAt(0) >= 32) { | |
| if (this._busy) { | |
| // Queue silently but SHOW in input bar in real-time | |
| this.inputBuf += str; | |
| this.cursorPos++; | |
| let display = this.inputBuf; | |
| if (this.inputLines.length > 0) { | |
| display += ` ${c("dim")}(+${this.inputLines.length} more)${R}`; | |
| } | |
| this.tui.setInput(display); | |
| return; | |
| } | |
| this.inputBuf = this.inputBuf.slice(0, this.cursorPos) + str + this.inputBuf.slice(this.cursorPos); | |
| this.cursorPos++; | |
| this.redrawLine(); | |
| } | |
| } | |
| redrawLine() { | |
| // In TUI mode, show inputBuf + extra-lines indicator | |
| let display = this.inputBuf; | |
| if (this.inputLines.length > 0) { | |
| display += ` ${c("dim")}(+${this.inputLines.length} more line${this.inputLines.length > 1 ? 's' : ''})${R}`; | |
| } | |
| this.tui.setInput(display); | |
| // Reposition cursor at end of actual inputBuf (not the indicator) | |
| if (this.tui?.active) { | |
| process.stdout.write(`\x1b[${this.tui._rows};${4 + this.inputBuf.length}H`); | |
| } | |
| // ── Render suggestion bar below ── | |
| this.updateSuggestions(); | |
| if (this.suggestionActive && this.suggestions.length > 0) { | |
| this.renderSuggestionBar(); | |
| const itemLines = Math.min(this.suggestions.length, 6); | |
| this._suggestionBarHeight = itemLines + 1; | |
| if (this.suggestions.length > 6) this._suggestionBarHeight++; | |
| } else { | |
| this._suggestionBarHeight = 0; | |
| } | |
| } | |
| async handleInput(input) { | |
| this._cancelled = false; | |
| if (input.startsWith("/")) { this.handleCommand(input.slice(1).trim().split(/\s+/)); return; } | |
| // Try natural language parsing first (before LLM) | |
| const nlResult = this.parseNaturalLanguage(input); | |
| if (nlResult) { | |
| this.handleInput(nlResult); // Recursively handle the mapped command | |
| return; | |
| } | |
| const trimmed = input.trim().toLowerCase(); | |
| if (trimmed === "exit" || trimmed === "quit") { this.stop(); return; } | |
| // ─── NATURAL LANGUAGE PARSER (works without LLM) ─── | |
| if (nlResult) { | |
| this._busy = true; | |
| this.sayLine(`> ${input}`, "green"); | |
| const { spinner, cleanup } = this.setupAgentHandlers(); | |
| try { | |
| this.teardownAgentHandlers(cleanup, spinner); | |
| this.renderResponse(nlResult); | |
| this.responseCount++; | |
| this.lastResponseTime = Date.now(); | |
| recordTechnique(nlResult, "reasoning"); | |
| this.evolutionXP = Math.min(this.evolutionMaxXP, this.evolutionXP + Math.min(20, Math.max(1, Math.floor(nlResult.length / 50)))); | |
| this.renderFooter(nlResult); | |
| this.conversation.push(`user: ${input.substring(0, 200)}`); | |
| this.conversation.push(`phantom: ${nlResult.substring(0, 500)}`); | |
| if (this.conversation.length > 500) this.conversation.splice(0, 100); | |
| saveMemory("conversation_latest", this.conversation); | |
| this.triggerPostEvolution(); | |
| } catch (err) { | |
| if (!this._cancelled) { | |
| this.sayLine(`✕ Error: ${err.message}`, "red"); | |
| this.sayLine("🧬 Running self-heal...", "yellow"); | |
| setImmediate(async () => { | |
| try { | |
| const { selfHeal } = await import("./lib/evolve.mjs"); | |
| const result = await selfHeal({ maxAttempts: 3 }); | |
| const fixed = result.log.filter(r => r.fixes?.some(f => f.fixed)).length; | |
| if (result.clean) { | |
| this.sayLine(`✅ Self-healed (${fixed} fix(es), ${(result.elapsed/1000).toFixed(1)}s)`, "green"); | |
| } else { | |
| this.sayLine(`⚠ Self-heal partial (${fixed} fix(es), ${result.attempts} round(s))`, "yellow"); | |
| } | |
| } catch {} | |
| }); | |
| } | |
| } | |
| this._busy = false; | |
| if (this._cancelled) { this._cancelled = false; return; } | |
| this.processQueue(); | |
| return; | |
| } | |
| this._busy = true; | |
| this.sayLine(`> ${input}`, "green"); | |
| if (!this.agent) { this.sayLine("✕ No agent available.", "red"); this._busy = false; this.prompt(); return; } | |
| const { spinner, cleanup } = this.setupAgentHandlers(); | |
| try { | |
| const response = await new Promise((resolve, reject) => { | |
| const handler = ({ agent: a, text }) => { | |
| if (a && a.id === this.agent.id) { try { this.bus.off("agent:msg", handler); } catch {} resolve(text); } | |
| }; | |
| this.bus.on("agent:msg", handler); | |
| if (!this.llm?.hasLLM) { | |
| const tools = Object.keys(this.agent.tools).sort(); | |
| resolve(`No LLM connected — tools-only mode.\nAvailable tools: ${tools.join(", ")}\nUse @tool_name|args to run a tool.`); | |
| return; | |
| } | |
| this.agent.receive("user", input).catch(err => { try { this.bus.off("agent:msg", handler); } catch {} reject(err); }); | |
| }); | |
| this.teardownAgentHandlers(cleanup, spinner); | |
| if (this._cancelled) { this._cancelled = false; this.sayLine(`${c("yellow")}⏹${R} Cancelled`, "yellow"); this._busy = false; this.processQueue(); return; } | |
| this.renderResponse(response); | |
| this.responseCount++; | |
| this.lastResponseTime = Date.now(); | |
| this.tokensUsed += response.length; | |
| recordTechnique(response, "reasoning"); | |
| studyCycle(this).catch(() => {}); | |
| this.evolutionXP = Math.min(this.evolutionMaxXP, this.evolutionXP + Math.min(20, Math.max(1, Math.floor(response.length / 50)))); | |
| this.renderFooter(response); | |
| this.conversation.push(`user: ${input.substring(0, 200)}`); | |
| this.conversation.push(`phantom: ${response.substring(0, 500)}`); | |
| if (this.conversation.length > 500) this.conversation.splice(0, 100); | |
| saveMemory("conversation_latest", this.conversation); | |
| this.triggerPostEvolution(); | |
| } catch (err) { | |
| this.teardownAgentHandlers(cleanup, spinner); | |
| if (!this._cancelled) { | |
| this.sayLine(`✕ Error: ${err.message}`, "red"); | |
| // Auto-heal: run self-diagnosis and fix on error | |
| this.sayLine("🧬 Running self-heal...", "yellow"); | |
| setImmediate(async () => { | |
| try { | |
| const { selfHeal } = await import("./lib/evolve.mjs"); | |
| const result = await selfHeal({ maxAttempts: 3 }); | |
| const fixed = result.log.filter(r => r.fixes?.some(f => f.fixed)).length; | |
| if (result.clean) { | |
| this.sayLine(`✅ Self-healed (${fixed} fix(es), ${(result.elapsed/1000).toFixed(1)}s)`, "green"); | |
| } else { | |
| this.sayLine(`⚠ Self-heal partial (${fixed} fix(es), ${result.attempts} round(s))`, "yellow"); | |
| } | |
| } catch {} | |
| }); | |
| } | |
| } | |
| this._busy = false; | |
| if (this._cancelled) { this._cancelled = false; return; } | |
| this.processQueue(); | |
| } | |
| setupAgentHandlers() { | |
| const tui = this.tui?.active ? this.tui : null; | |
| const spinner = createSpinner(this.startTime, { | |
| write: tui ? (s) => { | |
| // Write spinner at the last line of conversation area (just above input bar) | |
| process.stdout.write(`\x1b[${tui._rows - 2};1H\x1b[2K${s.replace(/\r/g, '')}`); | |
| } : undefined | |
| }); | |
| this._spinner = spinner; | |
| this._queueBuf = ""; | |
| this._typing = false; | |
| spinner.start("thinking... "); | |
| const tickHandler = () => { | |
| if (!this.agent) return; | |
| spinner.update(this.agent.status === "executing" ? "⚡ running... " : this.agent.status === "thinking" ? "🧠 analyzing... " : this.agent.status === "speaking" ? "💬 formulating... " : "⏳ working... "); | |
| }; | |
| this._tickHandler = tickHandler; | |
| this.bus.on("tick", tickHandler); | |
| const toolPlanHandler = () => {}; // silent | |
| this._toolPlanHandler = toolPlanHandler; | |
| this.bus.on("agent:tool:plan", toolPlanHandler); | |
| // Show agent's reasoning comments | |
| const thinkHandler = (comment) => { | |
| if (comment) console.log(`${c("cyan")}◈${R} ${comment}`); | |
| }; | |
| this._thinkHandler = thinkHandler; | |
| this.bus.on("agent:think", thinkHandler); | |
| const toolStartHandler = ({ tool, args }) => { | |
| this._toolCallCounter++; | |
| this.evolutionXP = Math.min(this.evolutionMaxXP, this.evolutionXP + 10); | |
| const truncated = (args || "").length > 60 ? (args || "").substring(0, 60) + "…" : (args || ""); | |
| console.log(`${c("dim")}⎿${R} ${c("cyan")}${tool}${R}${truncated ? ` ${c("dim")}${truncated}${R}` : ""}`); | |
| }; | |
| this._toolStartHandler = toolStartHandler; | |
| this.bus.on("agent:tool:start", toolStartHandler); | |
| const toolResultHandler = ({ tool, result, args }) => { | |
| if (!result) { spinner.update("⚡ running... "); return; } | |
| const n = learnFromTool(tool, result, args); | |
| if (n > 0) this._growFacts += n; | |
| spinner.update("⚡ running... "); | |
| }; | |
| this._toolResultHandler = toolResultHandler; | |
| this.bus.on("agent:tool:result", toolResultHandler); | |
| return { spinner, cleanup: { tickHandler, toolPlanHandler, thinkHandler, toolStartHandler, toolResultHandler, queueHandler: null } }; | |
| } | |
| teardownAgentHandlers(cleanup, spinner) { | |
| // Don't touch stdin — onKey handles it throughout | |
| spinner.stop(); | |
| delete this._spinner; | |
| this.bus.off("tick", cleanup.tickHandler); | |
| this.bus.off("agent:tool:plan", cleanup.toolPlanHandler); | |
| this.bus.off("agent:tool:start", cleanup.toolStartHandler); | |
| this.bus.off("agent:tool:result", cleanup.toolResultHandler); | |
| this.bus.off("agent:think", cleanup.thinkHandler); | |
| } | |
| renderFooter() { | |
| const elapsed = Math.floor((Date.now() - this.startTime) / 1000); | |
| const elapsedStr = elapsed > 3600 ? `${Math.floor(elapsed / 3600)}h ${Math.floor((elapsed % 3600) / 60)}m` : elapsed > 60 ? `${Math.floor(elapsed / 60)}m ${elapsed % 60}s` : `${elapsed}s`; | |
| const calls = this._toolCallCounter; | |
| const parts = [`${c("green")}✓${R} ${this.responseCount}`]; | |
| if (calls > 0) parts.push(`${c("dim")}${calls} tool${calls === 1 ? "" : "s"}${R}`); | |
| if (this.tokensUsed > 0) parts.push(`${c("dim")}${(this.tokensUsed / 1000).toFixed(1)}K${R}`); | |
| parts.push(`${c("dim")}${elapsedStr}${R}`); | |
| console.log(`${c("dim")}${parts.join(" · ")}${R}`); | |
| } | |
| triggerPostEvolution() { | |
| if (!process.env.PHANTOM_NO_EVOLVE && this.evolutionXP >= this.evolutionMaxXP) { | |
| console.log(` ${c("dim")}⚡ evolving...${R}`); | |
| startupEvolve().then(async ev => { | |
| const { execSync } = $r("child_process"); | |
| let ok = true; | |
| const phantomDir = resolve(homedir(), "Phantom"); | |
| for (const f of ["phantom.mjs", "lib/tools.mjs", "lib/visual.mjs", "lib/evolve.mjs"]) { try { execSync(`node --check "${resolve(phantomDir, f)}"`, { encoding: "utf-8", timeout: 5000 }); } catch { ok = false; break; } } | |
| if (ok) { try { execSync(`node test/core.test.mjs`, { cwd: phantomDir, encoding: "utf-8", timeout: 30000 }); } catch { ok = false; } } | |
| if (ok) { this.evolutionXP = 0; console.log(` ${c("dim")}✓ evolved${ev.wrappers_created > 0 ? ` +${ev.wrappers_created} wrappers` : ""}${R}`); } | |
| else console.log(` ${c("dim")}⚠ evolve check failed — XP held${R}`); | |
| }).catch(() => {}); | |
| } | |
| } | |
| cancel() { | |
| // Cancel current operation without exiting — returns to prompt | |
| this._cancelled = true; | |
| this._busy = false; | |
| if (this._spinner) { try { this._spinner.stop(); } catch {} delete this._spinner; } | |
| // Stdin stays with onKey — no cleanup needed | |
| this._queueBuf = ""; | |
| this.bus.off("tick", this._tickHandler); | |
| this.bus.off("agent:tool:plan", this._toolPlanHandler); | |
| this.bus.off("agent:tool:start", this._toolStartHandler); | |
| this.bus.off("agent:tool:result", this._toolResultHandler); | |
| this._queueBuf = ""; | |
| if (this.promptQueue.length > 0) { | |
| this.sayLine(`${c("yellow")}⏹${R} Stopped. ${this.promptQueue.length} queued — type /queue to see, /flushqueue to clear.`, "yellow"); | |
| } else { | |
| this.sayLine(`${c("yellow")}⏹${R} Cancelled`, "yellow"); | |
| } | |
| setTimeout(() => { this._cancelled = false; this.prompt(); }, 50); | |
| } | |
| processQueue() { | |
| // Process next queued item | |
| if (this.promptQueue.length > 0) { | |
| const next = this.promptQueue.shift(); | |
| this.sayLine(`${c("cyan")}📤${R} Next queued: ${next}`, "cyan"); | |
| this._busy = true; | |
| setImmediate(() => this.handleInput(next)); | |
| } else { | |
| // Blank line separates conversation turns | |
| this.tui.log(""); | |
| this.prompt(); | |
| } | |
| } | |
| sayLine(text, color = "fg") { | |
| const formatted = `${c(color)}${text}${R}`; | |
| this.tui.log(formatted); | |
| this.log(formatted); | |
| } | |
| get cols() { return process.stdout.columns || 80; } | |
| renderTopBar() { | |
| const cols = this.cols; | |
| const title = "Phantom"; | |
| const sep = "─".repeat(Math.max(0, cols - title.length - 2)); | |
| console.log(`${c("cyan")}${B}${title}${R}${c("dim")}${sep}╮${R}`); | |
| } | |
| renderBottomBar() { | |
| const cols = this.cols; | |
| console.log(`${c("dim")}╰${"─".repeat(cols - 1)}╯${R}`); | |
| } | |
| renderStatusBar() { | |
| const cols = this.cols; | |
| const model = this.llm?.provider || "no LLM"; | |
| const modelShort = typeof model === "string" ? model.replace(/^custom:/, "").split("/").pop() || model : "ai"; | |
| const elapsed = Math.floor((Date.now() - this.startTime) / 1000); | |
| const elapsedStr = elapsed >= 3600 | |
| ? `${Math.floor(elapsed / 3600)}h ${Math.floor((elapsed % 3600) / 60)}m` | |
| : `${Math.floor(elapsed / 60)}m ${elapsed % 60}s`; | |
| const lastResp = this.lastResponseTime | |
| ? `${Math.floor((Date.now() - this.lastResponseTime) / 1000)}s` | |
| : "—"; | |
| const tok = this.tokensUsed || this.responseCount * 500; | |
| const tokLimit = 200000; | |
| const pct = Math.min(100, Math.round((tok / tokLimit) * 100)); | |
| const barLen = 12; | |
| const filled = Math.round((pct / 100) * barLen); | |
| const bar = "█".repeat(filled) + "░".repeat(barLen - filled); | |
| const icon = "⚕"; | |
| const parts = [ | |
| `${c("magenta")}${icon}${R} ${c("cyan")}${modelShort}${R}`, | |
| `${c("dim")}${tok.toLocaleString()}/${(tokLimit / 1000).toFixed(0)}K${R}`, | |
| `${c("cyan")}[${bar}]${R} ${c("dim")}${pct}%${R}`, | |
| `${c("yellow")}🗜️ ${this.compressions}${R}`, | |
| `${c("green")}${elapsedStr}${R}`, | |
| `${c("dim")}⏲ ${lastResp}${R}`, | |
| `${c("green")}✓${R}`, | |
| ]; | |
| const status = parts.join(` ${c("dim")}│${R} `); | |
| // Truncate if too long | |
| const maxLen = cols - 2; | |
| const out = status.length > maxLen ? status.substring(0, maxLen - 1) + "…" : status; | |
| console.log(out); | |
| } | |
| renderSepLine() { | |
| console.log(`${c("dim")}${"─".repeat(this.cols)}${R}`); | |
| } | |
| renderFullFrame() { | |
| this.renderTopBar(); | |
| console.log(""); | |
| this.renderBottomBar(); | |
| this.renderStatusBar(); | |
| this.renderSepLine(); | |
| } | |
| async handleCommand(args) { | |
| const op = args[0]?.toLowerCase(); | |
| const rest = args.slice(1); | |
| const cmd = this.commands[op]; | |
| if (typeof cmd === "string") return this.handleCommand([cmd, ...rest]); | |
| if (cmd) { await cmd.call(this, rest); this.prompt(); } | |
| else { console.log(`\n${c("red")}✕${R} Unknown: /${op}. Type ${c("green")}/help${R}\n`); this.prompt(); } | |
| } | |
| get commands() { | |
| return { | |
| help: () => { | |
| const tc = Object.keys(hackerTools).length; | |
| console.log(`\n${B}${c("green")}PHANTOM COMMANDS${R}`); | |
| console.log(` ${c("green")} /help${R} — show this help\n ${c("green")} /command${R} — list all commands`); | |
| console.log(` ${c("green")} /tools${R} — list ${tc} tools`); | |
| console.log(` ${c("green")} /gui${R} — start web dashboard (port 8080)`); | |
| console.log(` ${c("green")} /api${R} — start REST API (port 9090)`); | |
| console.log(` ${c("green")} /model${R} — show/switch LLM`); | |
| console.log(` ${c("green")} /clear${R} — clear screen\n ${c("green")} /stop${R} — cancel current operation`); | |
| console.log(` ${c("green")} /queue${R} — show queued inputs\n ${c("green")} /flushqueue${R} — clear all queued inputs`); | |
| console.log(` ${c("green")} /delegate${R} — delegate task to agent\n ${c("green")} /talk${R} <a> — talk directly to an agent`); | |
| console.log(` ${c("green")} /agents${R} — list team with status\n ${c("green")} /save${R} <n> — save session`); | |
| console.log(` ${c("green")} /load${R} <n> — load session\n ${c("green")} /quit${R} — exit\n`); | |
| console.log(`${D}Type anything to chat. Use \\\\ for multi-line.${R}`); | |
| console.log(`${D}The AI auto-uses tools via @tool_name|args syntax.${R}`); | |
| console.log(`${D}@pipe${R} — chain tools: @pipe|subfinder|example.com|httpx`); | |
| console.log(`${D}@schedule${R} — scheduled scans: @schedule|daily|recon|target`); | |
| console.log(`${D}@scope${R} — manage targets: @scope|add|example.com`); | |
| console.log(`${D}@workspace_write${R} — save findings: @workspace_write|key|value`); | |
| console.log(`${D}@self_evolve${R} — run auto-evolution pipeline`); | |
| console.log(`${D}@self_evolve|status${R} — show evolution state`); | |
| console.log(`${D}TAB${R} — cycle suggestions, autocomplete @tool`); | |
| console.log(`${D}↑↓${R} — navigate suggestions / history`); | |
| console.log(`${D}↩${R} — accept highlighted suggestion`); | |
| console.log(`${D}ESC${R} — dismiss suggestions`); | |
| console.log(`${D}--quiet${R} — suppress banner/status (env: PHANTOM_QUIET)\n`); | |
| }, | |
| h: "help", command: "help", | |
| command: () => { | |
| console.log(`\n${B}${c("green")}PHANTOM TASKS${R}`); | |
| console.log(`${D}Core commands:${R}`); | |
| console.log(` ${c("green")}help${R} / ${c("green")}?${R} / ${c("green")}command${R} — show this help`); | |
| console.log(` ${c("green")}tools${R} — list all 149 tools`); | |
| console.log(` ${c("green")}clear${R} / ${c("green")}cls${R} — clear screen`); | |
| console.log(` ${c("green")}quit${R} / ${c("green")}exit${R} — exit Phantom`); | |
| console.log(`\\n${D}Bug Bounty / HackerOne / Bugcrowd:${R}`); | |
| console.log(` ${c("green")}hackerone programs${R} — list all HackerOne programs`); | |
| console.log(` ${c("green")}hackerone scope <program>${R} — view program scope`); | |
| console.log(` ${c("green")}hackerone reports <program>${R} — view submitted reports`); | |
| console.log(` ${c("green")}hackerone report <id>${R} — view single report`); | |
| console.log(` ${c("green")}hackerone me${R} — authenticated user info`); | |
| console.log(` ${c("green")}hackerone test${R} — test auth`); | |
| console.log(` ${c("green")}bugcrowd programs${R} — list all Bugcrowd programs`); | |
| console.log(` ${c("green")}bugcrowd scope <uuid>${R} — view program scope (targets)`); | |
| console.log(` ${c("green")}bugcrowd submissions <uuid>${R} — view submissions`); | |
| console.log(` ${c("green")}bugcrowd submission <id>${R} — view single submission`); | |
| console.log(` ${c("green")}bugcrowd me${R} — authenticated user info`); | |
| console.log(` ${c("green")}bugcrowd test${R} — test auth`); | |
| console.log(` ${c("green")}bugbounty <platform> <cmd>${R} — unified: hackerone|bugcrowd`); | |
| console.log(`\n${D}Recon & Scanning:${R}`); | |
| console.log(` ${c("green")}recon <domain>${R} — full recon pipeline`); | |
| console.log(` ${c("green")}port scan <target>${R} — port scan`); | |
| console.log(` ${c("green")}subdomain <domain>${R} — subdomain enumeration`); | |
| console.log(` ${c("green")}ssl check <domain>${R} — SSL certificate check`); | |
| console.log(` ${c("green")}tech detect <domain>${R} — technology detection`); | |
| console.log(` ${c("green")}crawl <url>${R} — web crawl`); | |
| console.log(` ${c("green")}dns <domain>${R} — DNS lookup`); | |
| console.log(` ${c("green")}whois <domain>${R} — WHOIS lookup`); | |
| console.log(`\n${D}Vulnerability Research:${R}`); | |
| console.log(` ${c("green")}cve <query>${R} — search CVEs`); | |
| console.log(` ${c("green")}exploit <query>${R} — search exploits`); | |
| console.log(` ${c("green")}xss <url>${R} — XSS scan`); | |
| console.log(` ${c("green")}sqli <url>${R} — SQL injection scan`); | |
| console.log(` ${c("green")}dir brute <url>${R} — directory bruteforce`); | |
| console.log(` ${c("green")}redirect <url>${R} — open redirect test`); | |
| console.log(`\n${D}OSINT & Intel:${R}`); | |
| console.log(` ${c("green")}shodan <query>${R} — Shodan search`); | |
| console.log(` ${c("green")}breach <email>${R} — email breach check`); | |
| console.log(` ${c("green")}github dork <query>${R} — GitHub dorks`); | |
| console.log(` ${c("green")}takeover <domain>${R} — subdomain takeover`); | |
| console.log(` ${c("green")}vt <hash>${R} — VirusTotal check`); | |
| console.log(` ${c("green")}wayback <domain>${R} — Wayback Machine`); | |
| console.log(` ${c("green")}geoip <ip>${R} — GeoIP lookup`); | |
| console.log(`\n${D}Crypto & Hashes:${R}`); | |
| console.log(` ${c("green")}hash <data>${R} — multiple hash formats`); | |
| console.log(` ${c("green")}decode <encoded>${R} — decode base64/url/hex`); | |
| console.log(` ${c("green")}crack <hash>${R} — hash cracking`); | |
| console.log(` ${c("green")}jwt <token>${R} — JWT decode`); | |
| console.log(`\n${D}Mission/Automation:${R}`); | |
| console.log(` ${c("green")}mission auto <name>${R} — autonomous recon`); | |
| console.log(` ${c("green")}auto <name>${R} — alias for mission auto`); | |
| console.log(` ${c("green")}scope add <target>${R} — add to scope`); | |
| console.log(` ${c("green")}schedule daily <tool> <target>${R} — schedule scan`); | |
| console.log(`\n${D}Session & Config:${R}`); | |
| console.log(` ${c("green")}config list${R} — show config`); | |
| console.log(` ${c("green")}config set <key> <value>${R} — set config`); | |
| console.log(` ${c("green")}save <name>${R} — save session`); | |
| console.log(` ${c("green")}load <name>${R} — load session`); | |
| console.log(` ${c("green")}agents${R} — list AI agents`); | |
| console.log(`\n${D}Agent Commands (ESC then type):${R}`); | |
| console.log(` ${c("green")}spawn [name] [role]${R} — create agent`); | |
| console.log(` ${c("green")}list${R} — list agents`); | |
| console.log(` ${c("green")}broadcast <msg>${R} — message all agents`); | |
| console.log(` ${c("green")}debate <topic>${R} — agents debate`); | |
| console.log(` ${c("green")}evolve${R} — evolve all agents`); | |
| console.log(`\n${D}Direct tool usage:${R}`); | |
| console.log(` ${c("cyan")}@tool_name|args${R} — run any tool directly`); | |
| console.log(` ${c("cyan")}@pipe|tool1|args | tool2|args${R} — chain tools`); | |
| console.log(`\n${D}Type naturally (e.g. "hackerone programs", "recon example.com")${R}\n`); | |
| }, | |
| tools: () => { | |
| const names = Object.keys(hackerTools).sort(); | |
| log.art(`\n${B}${c("green")}PHANTOM TOOLS (${names.length})${R}`); | |
| for (let i = 0; i < names.length; i += 4) console.log(` ${names.slice(i, i + 4).map(n => `${c("cyan")}${n.padEnd(20)}${R}`).join("")}`); | |
| console.log(""); | |
| }, | |
| model: (rest) => { | |
| if (rest.length === 0) { | |
| const ready = process.env.PHANTOM_PROVIDERS_READY; | |
| const avail = ready ? ready.split(",") : []; | |
| console.log(`\n${B}Current:${R} ${this.llm?.provider || "none"}`); | |
| console.log(`${D}Available providers:${R}`); | |
| for (const p of this.llm?.providers || []) console.log(` ${p.padEnd(14)}${avail.includes(p) ? `${c("green")} ✅${R}` : `${c("dim")} —${R}`}`); | |
| console.log(`${D}/model <provider> to switch${R}\n`); | |
| } else if (this.llm?.providers?.includes(rest[0])) { this.llm.provider = rest[0]; console.log(`\n${c("green")}✓${R} Switched to ${B}${rest[0]}${R}\n`); } | |
| else console.log(`\n${c("red")}✕${R} Unknown: ${rest[0]}. Available: ${this.llm?.providers?.join(", ") || "none"}\n`); | |
| }, | |
| clear: () => { | |
| this.logLines = []; | |
| if (this.tui?.active) { | |
| this.tui._buf.length = 0; | |
| for (let r = this.tui._convTop; r <= this.tui._convBot; r++) { | |
| process.stdout.write(`\x1b[${r};1H\x1b[2K`); | |
| } | |
| this.tui._cursorRow = this.tui._convTop; | |
| process.stdout.write(`\x1b[${this.tui._convTop};1H`); | |
| this.tui.setInput(""); | |
| } else { | |
| process.stdout.write(cls + home); | |
| console.log(`\n${c("magenta")}${c("dim")}· · · · · · ${R}\n${c("cyan")} ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄${R}\n${c("cyan")} █${c("magenta")} ═══ ═══ ═══ ═══ ═══${c("cyan")} █${R}\n${c("cyan")}▐█${c("magenta")} · · · · ·${c("cyan")} █▌${R}\n${c("cyan")}▐█ ${c("magenta")}╔═══════════╗${c("cyan")} █▌${R}\n${c("cyan")}▐█ ${c("magenta")}║ ${c("green")}◈ ◈${c("magenta")} ║${c("cyan")} █▌${R}\n${c("cyan")}▐█ ${c("magenta")}║${c("dim")} ╔═══╗${c("magenta")} ║${c("cyan")} █▌${R}\n${c("cyan")}▐█ ${c("magenta")}╚═══════════╝${c("cyan")} █▌${R}\n${c("cyan")} █ ${c("magenta")}┊ ${c("magenta")}${c("dim")}║${c("magenta")} ${c("magenta")}${c("dim")}║${c("magenta")} ┊${c("cyan")} █${R}\n${c("cyan")} █ ${c("magenta")}┊ ${c("magenta")}${c("dim")}║${c("magenta")} ● ${c("magenta")}${c("dim")}║${c("magenta")} ┊${c("cyan")} █${R}\n${c("cyan")} ▀▄ ${c("magenta")}${c("dim")}║${c("magenta")} ═══ ${c("magenta")}${c("dim")}║${c("cyan")} ▄▀${R}\n ${c("magenta")}${B}P H A N T O M${R} ${c("dim")}cleared${R}\n`); | |
| } | |
| }, | |
| c: "clear", | |
| stop: () => this.cancel(), | |
| queue: function() { | |
| if (this.promptQueue.length === 0) console.log(`${c("dim")}Queue empty. Type while the agent is busy to queue inputs.${R}`); | |
| else { | |
| console.log(`\n${B}${c("cyan")}QUEUE (${this.promptQueue.length})${R}`); | |
| this.promptQueue.forEach((item, i) => console.log(` ${c("dim")}${i + 1}.${R} ${item}`)); | |
| console.log(""); | |
| } | |
| }, | |
| q: "queue", | |
| flushqueue: function() { | |
| const n = this.promptQueue.length; | |
| this.promptQueue = []; | |
| console.log(`${c("yellow")}🗑${R} Queue cleared (${n} items discarded).`); | |
| }, | |
| fq: "flushqueue", | |
| delegate: async function(rest) { | |
| const targetName = rest[0], task = rest.slice(1).join(" "); | |
| if (!targetName) { console.log(`\n${c("red")}✕${R} Usage: /delegate <agent> <task>\n ${c("dim")}Agents:${R} ${this.am.list.map(a => `${a.name}(${a.role})`).join(", ")}\n`); } | |
| else if (!task) { console.log(`\n${c("red")}✕${R} Specify a task. Usage: /delegate <agent> <task>\n`); } | |
| else { | |
| const target = this.am.findAgent(targetName); | |
| if (!target) console.log(`\n${c("red")}✕${R} No agent "${targetName}". Available: ${this.am.list.map(a => a.name).join(", ")}\n`); | |
| else { console.log(`\n${c("magenta")}◆${R} Delegating to ${B}${target.name}${R} (${target.role})...\n`); console.log(`${c("green")}${await this.am.delegate(this.agent?.id, targetName, task)}${R}\n`); } | |
| } | |
| }, | |
| del: "delegate", | |
| talk: async function(rest) { | |
| const agentName = rest[0], message = rest.slice(1).join(" "); | |
| if (!agentName) { console.log(`\n${c("red")}✕${R} Usage: /talk <agent> <message>\n ${c("dim")}Agents:${R} ${this.am.list.map(a => `${a.name}(${a.role})`).join(", ")}\n`); } | |
| else if (!message) { console.log(`\n${c("red")}✕${R} Specify a message. Usage: /talk <agent> <message>\n`); } | |
| else { | |
| const target = this.am.findAgent(agentName); | |
| if (!target) console.log(`\n${c("red")}✕${R} No agent "${agentName}". Available: ${this.am.list.map(a => a.name).join(", ")}\n`); | |
| else { console.log(`\n${c("cyan")}◈${R} Talking to ${B}${target.name}${R} (${target.role})...\n`); console.log(`${c("green")}${await target.receive("user", message)}${R}\n`); } | |
| } | |
| }, | |
| t: "talk", | |
| save: function(rest) { if (rest[0]) { saveMemory(`session_${rest[0]}`, this.conversation); console.log(`\n${c("green")}✓${R} Saved: ${B}${rest[0]}${R}\n`); } }, | |
| load: function(rest) { | |
| if (rest[0]) { | |
| const m = loadMemory(`session_${rest[0]}`); | |
| if (m?.length) { this.conversation = m; console.log(`\n${c("green")}✓${R} Loaded: ${B}${rest[0]}${R} (${m.length} messages)\n`); } | |
| else console.log(`\n${c("red")}✕${R} Not found: ${rest[0]}\n`); | |
| } | |
| }, | |
| gui: function(rest) { | |
| if (this._guiRunning) console.log(`\n${c("yellow")}⚠${R} Dashboard already running on port ${this._guiPort || 8080}\n`); | |
| else { const p = parseInt(rest[0]) || 8080; startGuiDashboard(p); this._guiRunning = true; this._guiPort = p; console.log(`\n${c("green")}✓${R} Dashboard started at ${c("cyan")}http://localhost:${p}${R}\n`); } | |
| }, | |
| dashboard: "gui", g: "gui", | |
| api: function(rest) { | |
| if (this._apiRunning) console.log(`\n${c("yellow")}⚠${R} API server already running on port ${this._apiPort || 9090}\n`); | |
| else { const p = parseInt(rest[0]) || 9090; startApiServer(p); this._apiRunning = true; this._apiPort = p; console.log(`\n${c("green")}✓${R} API server started at ${c("cyan")}http://localhost:${p}${R}\n`); } | |
| }, | |
| rest: "api", | |
| quit: () => this.stop(), | |
| exit: "quit", | |
| }; | |
| } | |
| stop() { | |
| this.tui.exit(); | |
| this.running = false; | |
| raw(false); | |
| try { if (this.inputHandler) process.stdin.removeListener("data", this.inputHandler); } catch {} | |
| process.stdout.write(show); | |
| log.ok(`\n${c("green")}Phantom terminated.${R}`); | |
| saveSession({ stats: { toolsUsed: Object.keys(hackerTools).length, lastExit: Date.now() } }); | |
| process.exit(0); | |
| } | |
| // ─── MISSION TASK SYSTEM ─── | |
| async runMissionTasks() { | |
| const tasks = [ | |
| { id: "h1:programs", source: "hackerone", title: "List all HackerOne programs", category: "bug-bounty" }, | |
| { id: "h1:scope", source: "hackerone", title: "View program scope", category: "bug-bounty" }, | |
| { id: "h1:reports", source: "hackerone", title: "View submitted reports", category: "bug-bounty" }, | |
| { id: "recon:passive", source: "recon", title: "Passive reconnaissance (subfinder, dnsx, httpx)", category: "recon" }, | |
| { id: "recon:subdomain", source: "recon", title: "Subdomain enumeration", category: "recon" }, | |
| { id: "recon:dns", source: "recon", title: "DNS resolution and records", category: "recon" }, | |
| { id: "recon:http", source: "recon", title: "HTTP probing and tech detection", category: "recon" }, | |
| { id: "net:portscan", source: "network", title: "Port scanning", category: "network" }, | |
| { id: "net:ssl", source: "network", title: "SSL/TLS certificate check", category: "network" }, | |
| { id: "net:whois", source: "network", title: "WHOIS lookup", category: "network" }, | |
| { id: "vuln:cve", source: "vulnerability", title: "CVE search", category: "vulnerability" }, | |
| { id: "vuln:exploit", source: "vulnerability", title: "Exploit search", category: "vulnerability" }, | |
| { id: "web:crawl", source: "web", title: "Web crawling", category: "web" }, | |
| { id: "web:headers", source: "web", title: "HTTP headers analysis", category: "web" }, | |
| { id: "web:tech", source: "web", title: "Technology detection", category: "web" }, | |
| { id: "mission:recon", source: "mission", title: "Autonomous mission recon", category: "mission" }, | |
| { id: "mission:scope", source: "mission", title: "Scope checking", category: "mission" }, | |
| { id: "osint:whois", source: "osint", title: "WHOIS lookup", category: "osint" }, | |
| { id: "osint:dns", source: "osint", title: "DNS enumeration", category: "osint" }, | |
| { id: "osint:cert", source: "osint", title: "Certificate transparency", category: "osint" }, | |
| ]; | |
| const byCategory = {}; | |
| for (const t of tasks) { | |
| if (!byCategory[t.category]) byCategory[t.category] = []; | |
| byCategory[t.category].push(t); | |
| } | |
| let out = `📋 Available Tasks (${tasks.length} total)\n`; | |
| for (const [cat, items] of Object.entries(byCategory)) { | |
| out += `\n${cat.toUpperCase()}:\n`; | |
| for (const t of items) { | |
| out += ` ${t.id.padEnd(25)} — ${t.title}\n`; | |
| } | |
| } | |
| out += `\nUsage: run <task_id> [target]`; | |
| return out; | |
| } | |
| async runMissionTask(input) { | |
| const taskId = input.trim(); | |
| if (!taskId) return "[mission run] Usage: run <task_id> [target]"; | |
| const taskMap = { | |
| "h1:programs": () => hackerTools.hackerone("programs"), | |
| "h1:scope": (id) => hackerTools.hackerone(`scope ${id}`), | |
| "h1:reports": () => hackerTools.hackerone("reports"), | |
| "recon:passive": (target) => hackerTools.recon(target), | |
| "recon:subdomain": (target) => hackerTools.sub_enum(target), | |
| "recon:dns": (target) => hackerTools.dns_lookup(target), | |
| "recon:http": (target) => hackerTools.http_headers(target), | |
| "net:portscan": (target) => hackerTools.port_scan(target), | |
| "net:ssl": (target) => hackerTools.ssl_check(target), | |
| "net:whois": (target) => hackerTools.whois(target), | |
| "vuln:cve": (query) => hackerTools.cve_search(query), | |
| "vuln:exploit": (query) => hackerTools.searchsploit(query), | |
| "web:crawl": (target) => hackerTools.crawl(target), | |
| "web:headers": (target) => hackerTools.http_headers(target), | |
| "web:tech": (target) => hackerTools.whatweb(target), | |
| "mission:recon": (id) => hackerTools.mission(`auto ${id}`), | |
| "mission:scope": (id) => hackerTools.mission(`scope-check ${id}`), | |
| "osint:whois": (target) => hackerTools.whois(target), | |
| "osint:dns": (target) => hackerTools.sub_enum(target), | |
| "osint:cert": (target) => hackerTools.ssl_check(target), | |
| }; | |
| const parts = input.trim().split(" "); | |
| const taskKey = parts[0]; | |
| const target = parts.slice(1).join(" "); | |
| if (!taskMap[taskKey]) { | |
| return `[mission run] Unknown task: ${taskKey}\nUse "list tasks" to see available tasks`; | |
| } | |
| const needsTarget = ["h1:scope", "recon:passive", "recon:subdomain", "recon:dns", "recon:http", | |
| "net:portscan", "net:ssl", "net:whois", "vuln:cve", "vuln:exploit", | |
| "web:crawl", "web:headers", "web:tech", "mission:scope", "osint:whois", "osint:dns", "osint:cert"].includes(taskKey); | |
| if (needsTarget && !target) { | |
| return `[mission run] Usage: run ${taskKey} <target>`; | |
| } | |
| return await taskMap[taskKey](target); | |
| } | |
| } | |
| // ── UI Selector ─────────────────────────────────────────── | |
| function selectUI(am) { | |
| const e = ENV; | |
| // Show environment info | |
| const info = []; | |
| if (e.isTermux) info.push("Termux"); | |
| if (e.isTmux) info.push("tmux"); | |
| if (e.isWSL) info.push("WSL"); | |
| if (e.isWindows) info.push("Windows"); | |
| if (e.isProot) info.push("PRoot"); | |
| if (info.length) log.error(`${D}${info.join("/")} mode${R}`); | |
| // Non-interactive: minimal | |
| if (!e.interactive) { | |
| log.error(`${D}Non-interactive mode${R}`); | |
| return new MinimalUI(am); | |
| } | |
| // Default: Conversational REPL (works everywhere) | |
| // This gives a Claude Code / Hermes CLI experience | |
| try { | |
| return new ConversationalUI(am); | |
| } catch (err) { | |
| log.error(`${D}Conversational UI unavailable, falling back: ${err.message}${R}`); | |
| return new TermuxUI(am); | |
| } | |
| } | |
| // ── GUI Dashboard ────────────────────────────────────────── | |
| // ── REST API Handler ────────────────────────────────────── | |
| // API server + GUI dashboard extracted to lib/server.mjs | |
| // (imported at top of file) | |
| // ── Main ────────────────────────────────────────────────── | |
| import readline from "readline"; | |
| // ── CLI One-Shot Mode ───────────────────────────────────── | |
| const llm = createProvider(); | |
| llmInstance = llm; | |
| __r.llmInstance = llmInstance; | |
| initApiDeps(hackerTools, __r, REPORTS_DIR); | |
| const args = process.argv.slice(2); | |
| // Pre-scan for --quiet flag (can appear anywhere) | |
| const quietIdx = args.indexOf("--quiet"); | |
| if (quietIdx !== -1) { | |
| process.env.PHANTOM_QUIET = "1"; | |
| args.splice(quietIdx, 1); | |
| } | |
| if (args.length > 0 && !args[0].startsWith("--")) { | |
| // No --flag: pass as interactive input to phantom | |
| } else if (args.length > 0) { | |
| const flag = args[0].replace("--", ""); | |
| const input = args.slice(1).join(" ") || ""; | |
| if (flag === "chat" || flag === "c") { | |
| // Lightweight chat mode | |
| const { runChat } = await import("./chat.mjs"); | |
| await runChat(llm); | |
| process.exit(0); | |
| } | |
| if (flag === "version" || flag === "v") { | |
| log.cli(`Agron v${PHANTOM_VERSION} (powered by Phantom core)`); | |
| process.exit(0); | |
| } | |
| if (flag === "help" || flag === "h") { | |
| log.cli(`Phantom — Cybersecurity AI Assistant | |
| Usage: | |
| phantom Conversational REPL (default) | |
| phantom --recon <domain> Full recon (7 steps + report) | |
| phantom --tool <name> <input> Run one tool directly | |
| phantom --tool --json <name> <input> JSON structured output | |
| phantom --tool --pipe "sub | dom | httpx" Pipe tools (chain output→input) | |
| phantom --repl Force conversational REPL mode | |
| phantom --list List all tools | |
| phantom --list --json List tools as JSON | |
| phantom --gui Start web dashboard (port 8080) | |
| phantom --api Start REST API server (port 9090) | |
| phantom --quiet Suppress banner/status | |
| phantom --version Show version | |
| phantom --help This help | |
| Scheduling: | |
| @schedule|daily|recon|target Schedule daily recon | |
| @schedule|hourly|scan|target Schedule hourly scan | |
| @schedule|list Show scheduled jobs | |
| @schedule|remove|0 Cancel a schedule (by ID) | |
| @schedule|scope-auto Auto-schedule scope targets | |
| Scope: | |
| @scope|add|domain.com Add target to scope | |
| @scope|add|file.txt Load targets from file | |
| @scope|list Show scope | |
| @scope|remove|domain.com Remove target | |
| @scope|clear Clear scope | |
| Examples: | |
| phantom --recon example.com | |
| phantom --tool port_scan scanme.org | |
| phantom --tool cve_search "apache 2.4.49" | |
| phantom --tool bruteforce "ssh|192.168.1.1|root|admin,toor,123" | |
| phantom --tool --pipe "subfinder|example.com | httpx | nuclei"`); | |
| process.exit(0); | |
| } | |
| if (flag === "list" || flag === "l") { | |
| const names = Object.keys(hackerTools).sort(); | |
| log.cli(`Phantom — ${names.length} tools:\n`); | |
| for (const name of names) { | |
| const desc = typeof hackerTools[name] === "function" ? "" : ""; | |
| log.cli(` ${name.padEnd(22)}`); | |
| } | |
| log.cli(`\nUse --tool <name> [input] to run a tool.`); | |
| process.exit(0); | |
| } | |
| if (flag === "repl") { | |
| // Force conversational REPL mode | |
| const llm = createProvider(); | |
| llmInstance = llm; | |
| __r.llmInstance = llmInstance; | |
| const am = new AgentManager(llm); | |
| const ui = new ConversationalUI(am); | |
| ui.start(); | |
| await new Promise(() => {}); // keep alive | |
| } | |
| if (flag === "chat" || flag === "c") { | |
| // Lightweight chat mode | |
| const { runChat } = await import("./chat.mjs"); | |
| await runChat(llm); | |
| process.exit(0); | |
| } | |
| if (flag === "gui" || flag === "dashboard" || flag === "g") { | |
| const port = parseInt(process.env.PHANTOM_PORT || '8080'); | |
| const dllm = createProvider(); | |
| llmInstance = dllm; | |
| __r.llmInstance = dllm; | |
| const dam = new AgentManager(dllm); | |
| const dagent = dam.spawn("Phantom", "Cybersecurity AI", | |
| "You are Phantom, an autonomous cybersecurity AI. You operate with zero handholding — " + | |
| "the user gives a goal and you execute the full workflow. " + | |
| "Use @tool|args syntax to run tools." | |
| ); | |
| setChatAgent(dagent, dam, EventBus.i); | |
| dam.agents.forEach(a => a.registerHackerTools()); | |
| startGuiDashboard(port); | |
| await new Promise(() => {}); // keep alive | |
| } | |
| if (flag === "api" || flag === "rest") { | |
| const port = parseInt(process.env.PHANTOM_API_PORT || '9090'); | |
| startApiServer(port); | |
| await new Promise(() => {}); // keep alive | |
| } | |
| if (flag === "tool" || flag === "t") { | |
| const jsonMode = args.includes("--json"); | |
| const pipeMode = args.includes("--pipe") || args.includes("--chain"); | |
| const toolArgs = args.slice(1).filter(a => a !== "--json" && a !== "--pipe" && a !== "--chain"); | |
| const toolName = toolArgs[0]; | |
| const toolInput = toolArgs.slice(1).join(" "); | |
| if (!toolName) { | |
| log.error(`Usage: phantom --tool <name> [input]\n --json JSON output\n --pipe Pipe tool output to next tool (use | in input)\nAvailable: ${Object.keys(hackerTools).sort().join(", ")}`); | |
| process.exit(1); | |
| } | |
| if (pipeMode && toolInput.includes(" | ")) { | |
| // Chained pipe mode — use " | " as segment separator | |
| const result = await runPipe(hackerTools, toolInput, { json: jsonMode }); | |
| if (jsonMode) { | |
| log.output(result); | |
| } else { | |
| log.output(`🔗 ${toolInput}`); | |
| log.output(result); | |
| } | |
| } else if (!toolName) { | |
| log.error(`Usage: phantom --tool <name> [input]\n --json JSON output\n --pipe Pipe tool output to next tool (use | in input)\nAvailable: ${Object.keys(hackerTools).sort().join(", ")}`); | |
| process.exit(1); | |
| } else { | |
| const result = await runTool(hackerTools, toolName, toolInput || "", { json: jsonMode }); | |
| if (!jsonMode) log.output(`🔧 ${toolName} ${toolInput ? `— ${toolInput}` : ""}`); | |
| log.output(result); | |
| if (!jsonMode) process.exit(0); | |
| // In JSON mode, exit only on error (success JSON is already printed) | |
| try { | |
| const p = JSON.parse(result); | |
| if (!p.ok) process.exit(1); | |
| } catch {} | |
| process.exit(0); | |
| } | |
| process.exit(0); | |
| } | |
| if (flag === "recon" || flag === "r") { | |
| if (!input) { log.error("Usage: node phantom.mjs --recon <domain>"); process.exit(1); } | |
| log.output(`🎯 Phantom Recon — ${input}\n`); | |
| const result = await hackerTools.recon(input); | |
| log.output(result); | |
| process.exit(0); | |
| } | |
| // Unknown flag — try as tool name directly | |
| const toolName = flag; | |
| if (hackerTools[toolName]) { | |
| process.stdout.write(`🔧 ${toolName}${input ? ` — ${input}` : ""} `); | |
| const start = Date.now(); | |
| process.stdout.write(`[*] processing...\r`); | |
| const result = await runTool(hackerTools, toolName, input || ""); | |
| const elapsed = ((Date.now() - start) / 1000).toFixed(1); | |
| console.log(`\r${result ? result.slice(0, 10000) : "(empty)"}`); | |
| if (result && result.length > 10000) log.output(`...[truncated ${result.length} total chars]`); | |
| if (elapsed > 0.5) log.info(`⏱ ${elapsed}s`); | |
| process.exit(0); | |
| } | |
| log.error(`Unknown flag: --${flag}. Use --help for usage.`); | |
| process.exit(1); | |
| } | |
| // ── Provider detection ── | |
| if (ENV.interactive) { | |
| try { | |
| const avail = await llm.detectProviders(); | |
| const ready = Object.entries(avail).filter(([, v]) => v !== "no").map(([n]) => n); | |
| if (ready.length > 0) { | |
| if (!ready.includes(llm.provider)) { | |
| const best = llm.selectBest(avail); | |
| if (best) llm.provider = best; | |
| } | |
| process.env.PHANTOM_PROVIDERS_READY = ready.join(","); | |
| } else { | |
| // No LLM available — offer to set up an API key | |
| console.log(`\n${c("yellow")}⚠ No LLM provider configured.${R}`); | |
| console.log(`${D}You can run Ollama locally or set an API key.${R}`); | |
| const keyProviders = [ | |
| ["openai", "OPENAI_API_KEY", "OpenAI"], | |
| ["anthropic", "ANTHROPIC_API_KEY", "Anthropic"], | |
| ["groq", "GROQ_API_KEY", "Groq"], | |
| ["gemini", "GEMINI_API_KEY", "Google Gemini"], | |
| ["deepseek", "DEEPSEEK_API_KEY", "DeepSeek"], | |
| ["mistral", "MISTRAL_API_KEY", "Mistral"], | |
| ["openrouter", "OPENROUTER_API_KEY", "OpenRouter"], | |
| ]; | |
| console.log(`\n${B}Set up a provider?${R} (${D}enter number or leave blank to skip${R})`); | |
| for (let i = 0; i < keyProviders.length; i++) { | |
| console.log(` ${i + 1}) ${keyProviders[i][2]}`); | |
| } | |
| const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); | |
| const pick = await new Promise(r => rl.question(`\n${c("cyan")}?${R} Choice (1-${keyProviders.length}): `, r)); | |
| rl.close(); | |
| const idx = parseInt(pick) - 1; | |
| if (idx >= 0 && idx < keyProviders.length) { | |
| const [, envVar, label] = keyProviders[idx]; | |
| const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout }); | |
| const key = await new Promise(r => rl2.question(`${c("cyan")}🔑${R} Enter ${label} API key: `, r)); | |
| rl2.close(); | |
| if (key.trim()) { | |
| process.env[envVar] = key.trim(); | |
| _config[envVar] = key.trim(); | |
| try { fs.writeFileSync(userConfigPath, JSON.stringify(_config, null, 2)); } catch {} | |
| console.log(`${c("green")}✓${R} ${label} API key saved to config.json\n`); | |
| // Re-detect | |
| const avail2 = await llm.detectProviders(); | |
| const ready2 = Object.entries(avail2).filter(([, v]) => v !== "no").map(([n]) => n); | |
| if (ready2.length > 0) { | |
| const best = llm.selectBest(avail2); | |
| if (best) llm.provider = best; | |
| process.env.PHANTOM_PROVIDERS_READY = ready2.join(","); | |
| } | |
| } | |
| } | |
| } | |
| } catch {} | |
| } | |
| // ── Launch ConversationalUI (default) ── | |
| const am = new AgentManager(llm); | |
| const ui = selectUI(am); | |
| ui.start(); | |
| process.on("SIGINT", () => { try { process.stdout.write(show); } catch {} process.exit(0); }); | |
| process.on("SIGTERM", () => { try { process.stdout.write(show); } catch {} process.exit(0); }); | |
| process.on("exit", () => { | |
| try { process.stdout.write(show); } catch {} | |
| }); | |