Asnip / phantom.mjs
teddyoas's picture
Rebrand CLI version output as Agron
7d07c11
Raw History Blame Contribute Delete
184 kB
#!/usr/bin/env node
// 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 {}
});