Download lib/evolve.mjs from teddyoas/Asnip: direct link, hf CLI and curl.
- Browser
- Download file 35.3 kB
-
https://huggingface.co/teddyoas/Asnip/resolve/main/lib/evolve.mjs
- Command line
-
hf download hf://teddyoas/Asnip/lib/evolve.mjs
-
curl -L -o evolve.mjs https://huggingface.co/teddyoas/Asnip/resolve/main/lib/evolve.mjs
35.3 kB
| // Phantom — Auto-Evolution Engine | |
| // Self-healing, self-optimizing, self-growing. | |
| // Runs automatically on startup and after tasks. | |
| import fs from "fs"; | |
| import { execFileSync, execSync } from "child_process"; | |
| import { resolve, join, dirname } from "path"; | |
| import { fileURLToPath } from "url"; | |
| import { homedir } from "os"; | |
| import { BASE_DIR, TOOLS_DIR, PHANTOM_VERSION } from "./config.mjs"; | |
| import { __r } from "./runtime.mjs"; | |
| // ── Constants ────────────────────────────────────────────── | |
| const PHANTOM_DIR = resolve(dirname(fileURLToPath(import.meta.url)), ".."); // project root | |
| const EVOLVE_LOG = resolve(BASE_DIR, "evolve.json"); | |
| const AUTO_TOOLS_DIR = resolve(PHANTOM_DIR, "lib", "auto_tools"); // in-repo so git-tracked | |
| // ── Git helpers ──────────────────────────────────────────── | |
| function gitRoot() { | |
| try { | |
| const r = execSync("git rev-parse --show-toplevel 2>/dev/null", { encoding: "utf-8", timeout: 5000 }).trim(); | |
| return r || null; | |
| } catch { return null; } | |
| } | |
| export function gitCommit(files, msg) { | |
| const root = gitRoot(); | |
| if (!root) return false; | |
| try { | |
| // Stage specific files or all changes | |
| if (files && files.length > 0) { | |
| for (const f of files) { | |
| const abs = f.startsWith("/") ? f : resolve(PHANTOM_DIR, f); | |
| if (fs.existsSync(abs)) execSync(`git add "${abs}"`, { cwd: root, encoding: "utf-8", timeout: 10000 }); | |
| } | |
| } else { | |
| execSync("git add -A", { cwd: root, encoding: "utf-8", timeout: 10000 }); | |
| } | |
| // Check if anything staged | |
| const staged = execSync("git diff --cached --stat", { cwd: root, encoding: "utf-8", timeout: 5000 }).trim(); | |
| if (!staged) return false; | |
| execSync(`git commit -m "${msg}"`, { cwd: root, encoding: "utf-8", timeout: 15000 }); | |
| return true; | |
| } catch { return false; } | |
| } | |
| export function gitPush() { | |
| const root = gitRoot(); | |
| if (!root) return { pushed: false, reason: "no git root" }; | |
| // Check if remote exists | |
| try { | |
| const remotes = execSync("git remote", { cwd: root, encoding: "utf-8", timeout: 5000 }).trim(); | |
| if (!remotes) return { pushed: false, reason: "no remote configured" }; | |
| } catch { return { pushed: false, reason: "no remote" }; } | |
| // Fetch and check for upstream divergence (misalignment) | |
| try { | |
| const upstream = execSync("git rev-parse --symbolic-full-name @{upstream} 2>/dev/null", { cwd: root, encoding: "utf-8", timeout: 5000 }).trim(); | |
| if (upstream) { | |
| execSync("git fetch origin 2>/dev/null", { cwd: root, encoding: "utf-8", timeout: 30000 }); | |
| const behind = execSync("git rev-list --count HEAD..@{upstream} 2>/dev/null", { cwd: root, encoding: "utf-8", timeout: 5000 }).trim(); | |
| if (parseInt(behind) > 0) return { pushed: false, reason: `${behind} commit(s) behind upstream — pull first` }; | |
| } | |
| } catch { /* no upstream tracking, push anyway */ } | |
| // Alignment check on the diff | |
| const alignment = verifyDiffAlignment(root); | |
| if (!alignment.ok) return { pushed: false, reason: `alignment: ${alignment.errors[0]}`, alignment }; | |
| // Push | |
| try { | |
| execSync("git push 2>&1", { cwd: root, encoding: "utf-8", timeout: 60000 }); | |
| return { pushed: true }; | |
| } catch (e) { | |
| return { pushed: false, reason: e.stderr?.slice(0, 200) || e.message }; | |
| } | |
| } | |
| // ── Diff misalignment check ── | |
| // Scans staged changes for regressions before push. | |
| export function verifyDiffAlignment(root) { | |
| const result = { ok: true, errors: [], warnings: [] }; | |
| let diffStat; | |
| try { diffStat = execSync("git diff --cached --stat", { cwd: root, encoding: "utf-8", timeout: 5000 }).trim(); } | |
| catch { return result; } | |
| if (!diffStat) { result.warnings.push("no staged changes"); return result; } | |
| // Extract changed .mjs files | |
| const changedMjs = diffStat.split("\n") | |
| .map(l => l.split("|")[0]?.trim()) | |
| .filter(f => f && f.endsWith(".mjs")); | |
| // 1. Syntax check every changed .mjs file | |
| for (const f of changedMjs) { | |
| const abs = f.startsWith("/") ? f : resolve(root, f); | |
| if (fs.existsSync(abs)) { | |
| try { execSync(`node --check "${abs}"`, { encoding: "utf-8", timeout: 10000 }); } | |
| catch (e) { | |
| const err = (e.stderr || e.message || "").slice(0, 200); | |
| result.errors.push(`${f}: syntax error — ${err}`); | |
| result.ok = false; | |
| } | |
| } | |
| } | |
| // 2. Forbid debug artifacts in changed files | |
| const diffContent = execSync("git diff --cached -U0", { cwd: root, encoding: "utf-8", timeout: 5000 }); | |
| if (diffContent) { | |
| // Added lines that are debug artifacts | |
| const additions = diffContent.split("\n").filter(l => l.startsWith("+") && !l.startsWith("+++") && !l.startsWith("+ ")); | |
| const debugPatterns = [/^\+.*console\.(log|debug)\s*\(/m, /^\+.*debugger\s*;?$/m, /^\+.*process\.env\.PHANTOM_GIT_PUSH/m]; | |
| for (const pat of debugPatterns) { | |
| const matches = additions.filter(l => pat.test(l)); | |
| if (matches.length > 0) { | |
| result.warnings.push(`${matches.length} line(s) match ${pat.source} — verify intentional`); | |
| // Don't block for debug warnings, just flag | |
| } | |
| } | |
| // 3. Check for hardcoded secrets (API keys, tokens, passwords) | |
| const secretPatterns = [/^\+.*(?:api.?key|secret|password|token|passwd)\s*[:=]\s*['\"][^'\"]{8,}['\"]/im]; | |
| for (const pat of secretPatterns) { | |
| if (additions.some(l => pat.test(l))) { | |
| result.errors.push("hardcoded secret detected in staged diff — blocking push"); | |
| result.ok = false; | |
| } | |
| } | |
| // 4. Check file sizes (no binaries >100KB staged) | |
| try { | |
| const binaryCheck = execSync("git diff --cached --numstat | awk '{sum+=$1} END {print sum+0}'", { cwd: root, encoding: "utf-8", timeout: 5000 }).trim(); | |
| const totalAdditions = parseInt(binaryCheck); | |
| if (totalAdditions > 5000) result.warnings.push(`large diff: ${totalAdditions} additions — verify`); | |
| } catch { /* numstat unavailable */ } | |
| } | |
| // 5. Run core tests to verify nothing regressed | |
| try { | |
| const testOut = execSync("node test/core.test.mjs 2>&1", { cwd: root, encoding: "utf-8", timeout: 45000 }); | |
| const failMatch = testOut.match(/fail\s+(\d+)/); | |
| if (failMatch && parseInt(failMatch[1]) > 0) { | |
| result.errors.push(`${failMatch[1]} test(s) failing — blocking push`); | |
| result.ok = false; | |
| } | |
| } catch (e) { | |
| result.warnings.push("test runner unavailable — skipping test gate"); | |
| } | |
| return result; | |
| } | |
| // ── Known external binaries & their wrapper boilerplate ── | |
| const WRAP_BLUEPRINTS = { | |
| naabu: { | |
| install: "go install github.com/projectdiscovery/naabu/v2/cmd/naabu@latest", | |
| generate: (bin) => `naabu: async (input) => { | |
| try { | |
| const { execSync } = await import("child_process"); | |
| const { resolve } = await import("path"); | |
| const target = input.trim() || ""; | |
| if (!target) return "[naabu] Usage: @naabu|<target> [options]\\nFast port scanner by ProjectDiscovery.\\nExamples:\\n naabu|scanme.org\\n naabu|scanme.org -p 80,443,8443\\n naabu|scanme.org -top-ports 1000"; | |
| const r = execSync(\`naabu \${target}\`, { encoding: "utf-8", timeout: 120000, maxBuffer: 1024 * 1024 }); | |
| return r.trim() || "(no results)"; | |
| } catch (e) { return \`[naabu Error] \${e.stderr?.slice(0, 500) || e.message}\`; } | |
| },` | |
| }, | |
| notify: { | |
| install: "go install github.com/projectdiscovery/notify/cmd/notify@latest", | |
| generate: (bin) => `notify: async (input) => { | |
| try { | |
| const { execSync } = await import("child_process"); | |
| const msg = input.trim(); | |
| if (!msg) return "[notify] Usage: @notify|<message>\\nSend notifications to Slack/Telegram/Discord/etc.\\nRequires ~/.config/notify/provider-config.yaml"; | |
| execSync(\`notify -data <(echo "\${msg}")\`, { encoding: "utf-8", timeout: 15000 }); | |
| return "[notify] Sent"; | |
| } catch (e) { return \`[notify Error] \${e.message}\`; } | |
| },` | |
| }, | |
| puredns: { | |
| install: "go install github.com/d3mondev/puredns/v2@latest", | |
| generate: (bin) => `puredns: async (input) => { | |
| try { | |
| const { execSync } = await import("child_process"); | |
| const args = input.trim(); | |
| if (!args) return "[puredns] Usage: @puredns|<args>\\nFast DNS resolver by d3mondev.\\nExamples:\\n puredns|resolve domains.txt\\n puredns|bruteforce wordlist.txt example.com"; | |
| const r = execSync(\`puredns \${args}\`, { encoding: "utf-8", timeout: 120000, maxBuffer: 1024 * 1024 }); | |
| return r.trim() || "(no results)"; | |
| } catch (e) { return \`[puredns Error] \${e.stderr?.slice(0, 500) || e.message}\`; } | |
| },` | |
| }, | |
| httprobe: { | |
| install: "go install github.com/tomnomnom/httprobe@latest", | |
| generate: (bin) => `httprobe: async (input) => { | |
| try { | |
| const { execSync } = await import("child_process"); | |
| const targets = input.trim(); | |
| if (!targets) return "[httprobe] Usage: @httprobe|<targets>\\nProbe for alive HTTP/HTTPS servers. Takes list of hosts.\\nExamples:\\n httprobe|example.com:443\\n httprobe|subs.txt"; | |
| const r = execSync(\`echo "\${targets}" | httprobe\`, { encoding: "utf-8", timeout: 60000, maxBuffer: 1024 * 1024 }); | |
| return r.trim() || "(no alive hosts)"; | |
| } catch (e) { return \`[httprobe Error] \${e.message}\`; } | |
| },` | |
| }, | |
| chaos: { | |
| install: "go install github.com/projectdiscovery/chaos-client/cmd/chaos@latest", | |
| generate: (bin) => `chaos: async (input) => { | |
| try { | |
| const { execSync } = await import("child_process"); | |
| const domain = input.trim(); | |
| if (!domain) return "[chaos] Usage: @chaos|<domain>\\nProjectDiscovery Chaos - subdomain enumeration from passive sources.\\nRequires CHAOS_API_KEY env var."; | |
| const r = execSync(\`chaos -d \${domain} -silent\`, { encoding: "utf-8", timeout: 30000, maxBuffer: 1024 * 1024 }); | |
| return r.trim() || "(no subs from Chaos)"; | |
| } catch (e) { return \`[chaos Error] \${e.message}\`; } | |
| },` | |
| }, | |
| uncover: { | |
| install: "go install github.com/projectdiscovery/uncover/cmd/uncover@latest", | |
| generate: (bin) => `uncover: async (input) => { | |
| try { | |
| const { execSync } = await import("child_process"); | |
| const query = input.trim(); | |
| if (!query) return "[uncover] Usage: @uncover|<query>\\nSearch Shodan/Censys/Fofa/Publicwww for hosts.\\nRequires API keys configured in ~/.config/uncover/"; | |
| const r = execSync(\`uncover -q "\${query}" -silent\`, { encoding: "utf-8", timeout: 30000, maxBuffer: 1024 * 1024 }); | |
| return r.trim() || "(no results)"; | |
| } catch (e) { return \`[uncover Error] \${e.message}\`; } | |
| },` | |
| }, | |
| mapcidr: { | |
| install: "go install github.com/projectdiscovery/mapcidr/cmd/mapcidr@latest", | |
| generate: (bin) => `mapcidr: async (input) => { | |
| try { | |
| const { execSync } = await import("child_process"); | |
| const cidr = input.trim(); | |
| if (!cidr) return "[mapcidr] Usage: @mapcidr|<cidr>\\nCIDR expansion utility by ProjectDiscovery.\\nExamples:\\n mapcidr|192.168.1.0/24\\n mapcidr|-a 2a00:1450:4000::/48"; | |
| const r = execSync(\`mapcidr -cidr \${cidr} -silent\`, { encoding: "utf-8", timeout: 30000, maxBuffer: 1024 * 1024 }); | |
| return r.trim() || "(no results)"; | |
| } catch (e) { return \`[mapcidr Error] \${e.message}\`; } | |
| },` | |
| }, | |
| "subfinder": null, // already wrapped | |
| "httpx": null, // already wrapped | |
| "nuclei": null, // already wrapped | |
| "nmap": null, // already wrapped | |
| "ffuf": null, // already wrapped | |
| "gobuster": null, // already wrapped | |
| "hydra": null, // already wrapped | |
| "masscan": null, // already wrapped | |
| "nikto": null, // already wrapped | |
| "whatweb": null, // already wrapped | |
| "sqlmap": null, // already wrapped | |
| "amass": null, // already wrapped | |
| "dnsx": null, // already wrapped | |
| "gau": null, // already wrapped | |
| "katana": null, // already wrapped | |
| "gitleaks": null, // already wrapped | |
| "s3scanner": null, // already wrapped | |
| "trufflehog": null, // already wrapped | |
| "wafw00f": null, // already wrapped | |
| "arjun": null, // already wrapped | |
| "gospider": null, // already wrapped | |
| "interactsh": null, // already wrapped | |
| }; | |
| // ── Tools Phantom already wraps (skip) ── | |
| const EXISTING_TOOLS = new Set(Object.keys(WRAP_BLUEPRINTS).filter(k => WRAP_BLUEPRINTS[k] === null)); | |
| // ── Load evolve state ── | |
| function loadState() { | |
| try { | |
| if (fs.existsSync(EVOLVE_LOG)) return JSON.parse(fs.readFileSync(EVOLVE_LOG, "utf-8")); | |
| } catch {} | |
| return { generation: 1, patches: [], wrappers: [], errors_fixed: 0, last_evolve: null, auto_fixes: [] }; | |
| } | |
| function saveState(st) { | |
| try { | |
| if (!fs.existsSync(BASE_DIR)) fs.mkdirSync(BASE_DIR, { recursive: true }); | |
| fs.writeFileSync(EVOLVE_LOG, JSON.stringify(st, null, 2), "utf-8"); | |
| } catch {} | |
| } | |
| // ── 1. Auto-detect missing wrappers ── | |
| // Scan PATH for installed security tools Phantom doesn't wrap yet. | |
| export function detectMissingWrappers() { | |
| const existing = new Set([ | |
| "nmap","sqlmap","searchsploit","ffuf","gobuster","hydra","john","hashcat", | |
| "aircrack-ng","tshark","masscan","nikto","wpscan","dirb","enum4linux", | |
| "smbclient","nc","ncat","socat","curl","wget","git","python3","node", | |
| "docker","kubectl","terraform","yara","clamscan","sslyze","testssl", | |
| "whois","dig","nslookup","host","tcpdump","sqlite3","jq","yt-dlp","ffmpeg", | |
| "subfinder","httpx","nuclei","amass","gau","dnsx","katana","gitleaks", | |
| "s3scanner","gobuster","nmap","sqlmap","whatweb","wafw00f","trufflehog", | |
| "hydra","masscan","nikto","arjun","gospider","cloud_enum","notify", | |
| "interactsh","burpsuite","metasploit","naabu","puredns","httprobe", | |
| "chaos","uncover","mapcidr","crtsh", | |
| ]); | |
| const found = []; | |
| const candidates = Object.entries(WRAP_BLUEPRINTS).filter(([, v]) => v !== null); | |
| for (const [bin, blueprint] of candidates) { | |
| if (existing.has(bin)) continue; | |
| try { | |
| execFileSync("which", [bin], { encoding: "utf-8", timeout: 3000 }); | |
| found.push({ bin, blueprint }); | |
| } catch {} | |
| } | |
| return found; | |
| } | |
| // Generate wrapper code and save it as a proper ESM module | |
| export function generateWrapper(bin, blueprint) { | |
| let code = blueprint.generate(bin); | |
| const dir = AUTO_TOOLS_DIR; | |
| if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); | |
| // Strip the "name: async (input) => {" prefix and trailing "},\n" suffix | |
| // The blueprints include object-literal formatting we don't need in standalone modules | |
| code = code.replace(/^\w+:\s*async\s*\(\s*input\s*\)\s*=>\s*{/, "").replace(/},\s*$/, "").trim(); | |
| // Wrap in a proper ESM default export | |
| const moduleCode = `// Auto-generated wrapper for ${bin} | |
| // Created by Phantom auto-evolution | |
| export default async function(input) { | |
| ${code} | |
| }\n`; | |
| const filePath = resolve(dir, `${bin}.mjs`); | |
| fs.writeFileSync(filePath, moduleCode, "utf-8"); | |
| return filePath; | |
| } | |
| // Load all auto-generated tools and return as {name: executeFn} | |
| export async function loadAutoTools() { | |
| const tools = {}; | |
| if (!fs.existsSync(AUTO_TOOLS_DIR)) return tools; | |
| const files = fs.readdirSync(AUTO_TOOLS_DIR).filter(f => f.endsWith(".mjs")); | |
| for (const file of files) { | |
| try { | |
| const name = file.replace(/\.mjs$/, ""); | |
| const mod = await import(resolve(AUTO_TOOLS_DIR, file) + `?t=${Date.now()}`); | |
| const fn = mod.default || mod.execute; | |
| if (typeof fn === "function") tools[name] = fn; | |
| } catch {} | |
| } | |
| return tools; | |
| } | |
| // ── 2. Auto-fix common tool errors ── | |
| // Patterns that can be auto-patched | |
| function autoInstall(bin) { | |
| // Try to auto-install a missing binary | |
| try { | |
| // Check if npm package exists | |
| execSync(`which npm 2>/dev/null`, { timeout: 3000 }); | |
| execSync(`npm list -g ${bin} 2>/dev/null || npm install -g ${bin} 2>/dev/null`, { timeout: 60000 }); | |
| return { fixed: true, message: `Auto-installed ${bin} via npm` }; | |
| } catch {} | |
| try { | |
| execSync(`which apt 2>/dev/null && apt install -y ${bin} 2>/dev/null`, { timeout: 60000 }); | |
| return { fixed: true, message: `Auto-installed ${bin} via apt` }; | |
| } catch {} | |
| try { | |
| execSync(`which pkg 2>/dev/null && pkg install -y ${bin} 2>/dev/null`, { timeout: 60000 }); | |
| return { fixed: true, message: `Auto-installed ${bin} via pkg` }; | |
| } catch {} | |
| return { fixed: false, message: `${bin} not installed and auto-install failed` }; | |
| } | |
| const FIX_PATTERNS = [ | |
| { | |
| match: /command not found/i, | |
| fix: async (tool, err) => { | |
| const bin = tool || err.match(/'([^']+)'/)?.[1]; | |
| if (bin) return autoInstall(bin); | |
| return { fixed: false, message: `Tool not installed. Install it and try again.` }; | |
| } | |
| }, | |
| { | |
| match: /Cannot find module/i, | |
| fix: async (tool, err) => { | |
| const mod = err.match(/'([^']+)'/)?.[1]; | |
| if (mod) { | |
| try { | |
| execSync(`npm install ${mod} 2>/dev/null`, { timeout: 60000, cwd: PHANTOM_DIR }); | |
| return { fixed: true, message: `Auto-installed module ${mod}` }; | |
| } catch { | |
| try { | |
| execSync(`npm install ${mod} 2>/dev/null`, { timeout: 60000 }); | |
| return { fixed: true, message: `Auto-installed module ${mod} globally` }; | |
| } catch {} | |
| } | |
| } | |
| return { fixed: false, message: `Missing module ${mod || tool} — could not auto-install` }; | |
| } | |
| }, | |
| { | |
| match: /EACCES|EPERM/i, | |
| fix: async (tool, err) => { | |
| const file = err.match(/'([^']+)'/)?.[1]; | |
| if (file) { | |
| try { | |
| execSync(`chmod +x "${file}" 2>/dev/null`, { timeout: 5000 }); | |
| return { fixed: true, message: `Made ${file} executable` }; | |
| } catch {} | |
| } | |
| return { fixed: false, message: `Permission denied — run with elevated privileges` }; | |
| } | |
| }, | |
| { | |
| match: /ETIMEOUT|ENOTFOUND|fetch.*failed/i, | |
| fix: async (tool, err) => { | |
| return { fixed: false, message: `Network issue — check connectivity and try again` }; | |
| } | |
| }, | |
| { | |
| match: /ENOENT/i, | |
| fix: async (tool, err) => { | |
| return { fixed: false, message: `File/directory not found — verify path exists` }; | |
| } | |
| }, | |
| { | |
| match: /Syntax error.*backquote|EOF in backquote|unclosed.*backtick/i, | |
| fix: async (tool, err) => { | |
| return { fixed: false, message: `Check for unescaped backticks or $() in command` }; | |
| } | |
| }, | |
| { | |
| match: /Shell Error/i, | |
| fix: async (tool, err) => { | |
| return { fixed: false, message: `Shell command failed — check syntax and retry` }; | |
| } | |
| }, | |
| ]; | |
| // Analyze a tool error and attempt auto-fix | |
| export async function analyzeError(tool, errorMessage) { | |
| for (const pattern of FIX_PATTERNS) { | |
| if (pattern.match.test(errorMessage)) { | |
| const result = await pattern.fix(tool, errorMessage); | |
| if (result) { | |
| const st = loadState(); | |
| st.errors_fixed++; | |
| st.auto_fixes.push({ ts: new Date().toISOString(), tool, error: errorMessage.slice(0, 100), fix: result.message }); | |
| saveState(st); | |
| return result; | |
| } | |
| } | |
| } | |
| return null; | |
| } | |
| // ── 3. Auto-syntax heal — actually fix common issues ── | |
| // Auto-fix common syntax/errors in Phantom's own source | |
| export async function autoHealSyntax() { | |
| const results = []; | |
| const files = [ | |
| "phantom.mjs", | |
| ...fs.readdirSync(resolve(PHANTOM_DIR, "lib")).filter(f => f.endsWith(".mjs")).map(f => `lib/${f}`), | |
| ]; | |
| for (const file of files) { | |
| const fp = resolve(PHANTOM_DIR, file); | |
| if (!fs.existsSync(fp)) continue; | |
| try { | |
| execSync(`node --check "${fp}" 2>/dev/null`, { encoding: "utf-8", timeout: 10000 }); | |
| continue; // syntax OK | |
| } catch (e) { | |
| // Syntax error — try to fix common issues | |
| try { | |
| const content = fs.readFileSync(fp, "utf-8"); | |
| const stderr = e.stderr || e.message || ""; | |
| let fixed = false; | |
| // Fix 1: trailing commas in non-module context (common in object literals) | |
| if (stderr.includes("trailing comma") || stderr.includes("nexpected token")) { | |
| const newContent = content.replace(/,(\s*[\n\r]+\s*[}\]])/g, '$1'); | |
| if (newContent !== content) { | |
| fs.writeFileSync(fp, newContent, "utf-8"); | |
| try { | |
| execSync(`node --check "${fp}" 2>/dev/null`, { encoding: "utf-8", timeout: 10000 }); | |
| fixed = true; | |
| results.push({ file, fixed: true, error: "Fixed trailing commas" }); | |
| continue; | |
| } catch {} | |
| } | |
| } | |
| // Fix 2: unclosed string — look for unterminated string error | |
| if (stderr.includes("Unterminated string") || stderr.includes("unterminated string")) { | |
| const newContent = content.replace(/('''|"""|````)/g, (m) => m.slice(0, 3)); | |
| if (newContent !== content) { | |
| fs.writeFileSync(fp, newContent, "utf-8"); | |
| try { | |
| execSync(`node --check "${fp}" 2>/dev/null`, { encoding: "utf-8", timeout: 10000 }); | |
| fixed = true; | |
| results.push({ file, fixed: true, error: "Fixed unterminated string" }); | |
| continue; | |
| } catch {} | |
| } | |
| } | |
| // Fix 3: missing closing brace or paren | |
| if (stderr.includes("Unexpected end of input") || stderr.includes("missing )") || stderr.includes("missing }")) { | |
| // Try adding closing braces | |
| let newContent = content; | |
| const opens = (content.match(/{/g) || []).length; | |
| const closes = (content.match(/}/g) || []).length; | |
| const opensP = (content.match(/\(/g) || []).length; | |
| const closesP = (content.match(/\)/g) || []).length; | |
| if (opens > closes) { | |
| newContent += "\n" + "}".repeat(opens - closes); | |
| } | |
| if (opensP > closesP) { | |
| newContent += "\n" + ")".repeat(opensP - closesP); | |
| } | |
| if (newContent !== content) { | |
| fs.writeFileSync(fp, newContent, "utf-8"); | |
| try { | |
| execSync(`node --check "${fp}" 2>/dev/null`, { encoding: "utf-8", timeout: 10000 }); | |
| fixed = true; | |
| results.push({ file, fixed: true, error: `Fixed unbalanced braces/parens` }); | |
| continue; | |
| } catch {} | |
| } | |
| } | |
| if (!fixed) { | |
| results.push({ file, fixed: false, error: stderr.trim().slice(0, 100) || "Unknown syntax error" }); | |
| } | |
| } catch { | |
| results.push({ file, fixed: false, error: "Cannot read file" }); | |
| } | |
| } | |
| } | |
| return results; | |
| } | |
| // ── 4. Auto-optimization scanner ── | |
| // Scan Phantom source for optimization opportunities | |
| export function scanOptimizations() { | |
| const issues = []; | |
| const mainPath = resolve(PHANTOM_DIR, "phantom.mjs"); | |
| if (!fs.existsSync(mainPath)) return issues; | |
| const src = fs.readFileSync(mainPath, "utf-8"); | |
| const lines = src.split("\n"); | |
| // Check for oversized functions | |
| let fnStart = 0; | |
| let braceCount = 0; | |
| let inFn = false; | |
| for (let i = 0; i < lines.length; i++) { | |
| const l = lines[i]; | |
| if (!inFn && l.match(/async\s+\w+\s*\(/)) { | |
| fnStart = i; | |
| inFn = true; | |
| braceCount = (l.match(/{/g) || []).length - (l.match(/}/g) || []).length; | |
| } else if (inFn) { | |
| braceCount += (l.match(/{/g) || []).length - (l.match(/}/g) || []).length; | |
| if (braceCount <= 0) { | |
| const len = i - fnStart; | |
| if (len > 100) { | |
| issues.push({ line: fnStart + 1, type: "long_fn", detail: `Function at line ${fnStart + 1} is ${len} lines — consider splitting` }); | |
| } | |
| inFn = false; | |
| } | |
| } | |
| } | |
| // Check for repeated patterns | |
| const dynamicImportCount = (src.match(/import\("child_process"\)/g) || []).length; | |
| if (dynamicImportCount > 3) { | |
| issues.push({ line: 0, type: "repeated_import", detail: `Dynamic import("child_process") used ${dynamicImportCount} times — hoist to module scope` }); | |
| } | |
| // Check for console.log vs structured logging | |
| const consoleCount = (src.match(/console\.(log|error|warn)\(/g) || []).length; | |
| if (consoleCount > 60) { | |
| issues.push({ line: 0, type: "console_spam", detail: `${consoleCount} console.* calls — use structured logging` }); | |
| } | |
| return issues; | |
| } | |
| // ── 5. Run full evolution cycle ── | |
| export async function autoEvolve() { | |
| const st = loadState(); | |
| const results = []; | |
| // Phase 1: Auto-detect missing wrappers | |
| try { | |
| const missing = detectMissingWrappers(); | |
| if (missing.length > 0) { | |
| const created = []; | |
| for (const { bin, blueprint } of missing) { | |
| try { | |
| const path = generateWrapper(bin, blueprint); | |
| st.wrappers.push({ bin, path, ts: new Date().toISOString() }); | |
| created.push(bin); | |
| results.push({ phase: "wrapper", bin, status: "created", path }); | |
| } catch (e) { | |
| results.push({ phase: "wrapper", bin, status: "error", error: e.message }); | |
| } | |
| } | |
| // ── Git commit auto-generated wrappers ── | |
| if (created.length > 0) { | |
| const committed = gitCommit( | |
| created.map(b => `lib/auto_tools/${b}.mjs`), | |
| `🧬 auto-evolve: add ${created.length} tool wrapper(s): ${created.join(", ")}` | |
| ); | |
| if (committed) { | |
| results.push({ phase: "git", status: "committed", files: created.length }); | |
| } | |
| } | |
| } | |
| } catch (e) { | |
| results.push({ phase: "wrapper", status: "error", error: e.message }); | |
| } | |
| // Phase 2: Syntax check and auto-heal | |
| try { | |
| const healResults = await autoHealSyntax(); | |
| for (const r of healResults) { | |
| results.push({ phase: "heal", file: r.file, status: r.fixed ? "fixed" : "failed", error: r.error }); | |
| } | |
| } catch (e) { | |
| results.push({ phase: "heal", status: "error", error: e.message }); | |
| } | |
| // Phase 3: Scan for optimizations | |
| try { | |
| const opts = scanOptimizations(); | |
| for (const o of opts) { | |
| results.push({ phase: "optimize", type: o.type, detail: o.detail, line: o.line }); | |
| } | |
| } catch (e) { | |
| results.push({ phase: "optimize", status: "error", error: e.message }); | |
| } | |
| // Phase 4: Self-syntax validation | |
| try { | |
| const { execSync } = await import("child_process"); | |
| execSync(`node --check "${resolve(PHANTOM_DIR, "phantom.mjs")}"`, { encoding: "utf-8", timeout: 10000 }); | |
| results.push({ phase: "validate", file: "phantom.mjs", status: "ok" }); | |
| execSync(`node --check "${resolve(PHANTOM_DIR, "lib/tools.mjs")}"`, { encoding: "utf-8", timeout: 10000 }); | |
| results.push({ phase: "validate", file: "lib/tools.mjs", status: "ok" }); | |
| execSync(`node --check "${resolve(PHANTOM_DIR, "lib/runtime.mjs")}"`, { encoding: "utf-8", timeout: 10000 }); | |
| results.push({ phase: "validate", file: "lib/runtime.mjs", status: "ok" }); | |
| execSync(`node --check "${resolve(PHANTOM_DIR, "lib/evolve.mjs")}"`, { encoding: "utf-8", timeout: 10000 }); | |
| results.push({ phase: "validate", file: "lib/evolve.mjs", status: "ok" }); | |
| } catch (e) { | |
| results.push({ phase: "validate", status: "error", error: e.stderr?.slice(0, 200) || e.message }); | |
| } | |
| // Phase 5: Test gate — only suggest push if tests pass | |
| try { | |
| const { execSync } = await import("child_process"); | |
| const testOut = execSync("node test/core.test.mjs", { cwd: PHANTOM_DIR, encoding: "utf-8", timeout: 30000 }).trim(); | |
| const passMatch = testOut.match(/pass\s+(\d+)/); | |
| const failMatch = testOut.match(/fail\s+(\d+)/); | |
| const passed = passMatch ? parseInt(passMatch[1]) : 0; | |
| const failed = failMatch ? parseInt(failMatch[1]) : 0; | |
| results.push({ phase: "test", status: failed > 0 ? "failed" : "passed", passed, failed }); | |
| if (failed > 0) { | |
| // Attempt auto-heal on test failures | |
| try { | |
| const { autoHealSyntax } = await import("./evolve.mjs"); | |
| const healed = await autoHealSyntax(); | |
| for (const h of healed) { | |
| results.push({ phase: "heal_retry", file: h.file, status: h.fixed ? "fixed" : "failed", error: h.error }); | |
| } | |
| if (healed.some(h => h.fixed)) { | |
| // Re-run tests after heal | |
| const retryOut = execSync("node test/core.test.mjs", { cwd: PHANTOM_DIR, encoding: "utf-8", timeout: 30000 }).trim(); | |
| const retryPass = retryOut.match(/pass\s+(\d+)/); | |
| const retryFail = retryOut.match(/fail\s+(\d+)/); | |
| const rPassed = retryPass ? parseInt(retryPass[1]) : 0; | |
| const rFailed = retryFail ? parseInt(retryFail[1]) : 0; | |
| results.push({ phase: "test_retry", status: rFailed > 0 ? "failed" : "passed", passed: rPassed, failed: rFailed }); | |
| } | |
| } catch { /* heal attempt failed */ } | |
| } | |
| } catch (e) { | |
| results.push({ phase: "test", status: "error", error: e.stderr?.slice(0, 200) || e.message }); | |
| } | |
| // Check readiness: have committed wrappers AND all tests passed? | |
| const hasCommits = results.some(r => r.phase === "git" && r.status === "committed"); | |
| const testsPassed = results.some(r => r.phase === "test" && r.status === "passed"); | |
| const finalTestsOk = results.some(r => r.phase === "test_retry" && r.status === "passed") || testsPassed; | |
| if (hasCommits && finalTestsOk) { | |
| const pushResult = gitPush(); | |
| if (pushResult.pushed) { | |
| results.push({ phase: "git", status: "pushed", summary: "✅ Auto-pushed to remote" }); | |
| } else { | |
| results.push({ phase: "git", status: pushResult.reason?.startsWith("alignment") ? "blocked" : "push_failed", | |
| reason: pushResult.reason, summary: `⚠ Push skipped: ${pushResult.reason}` }); | |
| } | |
| } else if (hasCommits && !finalTestsOk) { | |
| results.push({ phase: "git", status: "tests_failed", summary: "⚠ Not pushing — tests failing after changes" }); | |
| } | |
| // Update state | |
| st.generation++; | |
| st.last_evolve = new Date().toISOString(); | |
| st.patches = [...(st.patches || []), ...results.filter(r => r.phase === "heal")]; | |
| saveState(st); | |
| return { generation: st.generation, results, wrappers_created: st.wrappers.length, errors_fixed: st.errors_fixed }; | |
| } | |
| // ── Quick startup evolution check ── | |
| export async function startupEvolve() { | |
| const st = loadState(); | |
| const results = { wrappers_checked: 0, wrappers_created: 0, issues: [] }; | |
| // Only run full evolution if more than 6 hours since last, or never run | |
| const last = st.last_evolve ? new Date(st.last_evolve).getTime() : 0; | |
| const sixHours = 6 * 60 * 60 * 1000; | |
| if (Date.now() - last < sixHours && st.generation > 1) { | |
| results.issues.push("skipped — recently evolved"); | |
| return results; | |
| } | |
| // Quick wrapper check | |
| try { | |
| const missing = detectMissingWrappers(); | |
| results.wrappers_checked = missing.length; | |
| const created = []; | |
| for (const { bin, blueprint } of missing) { | |
| try { | |
| generateWrapper(bin, blueprint); | |
| st.wrappers.push({ bin, path: resolve(AUTO_TOOLS_DIR, `${bin}.mjs`), ts: new Date().toISOString() }); | |
| created.push(bin); | |
| results.wrappers_created++; | |
| } catch {} | |
| } | |
| // Git commit wrappers created at startup | |
| if (created.length > 0) { | |
| gitCommit(created.map(b => `lib/auto_tools/${b}.mjs`), "🧬 startup: add new auto-generated wrappers"); | |
| const pushResult = gitPush(); | |
| if (pushResult.pushed) results.push("auto-pushed"); | |
| else if (pushResult.reason) results.push(`push: ${pushResult.reason}`); | |
| } | |
| } catch {} | |
| // Quick syntax check | |
| try { | |
| execSync(`node --check "${resolve(PHANTOM_DIR, "phantom.mjs")}"`, { encoding: "utf-8", timeout: 10000 }); | |
| } catch (e) { | |
| results.issues.push("syntax error in phantom.mjs"); | |
| } | |
| // Ensure generated modules directory exists | |
| const generatedDir = resolve(PHANTOM_DIR, "lib", "generated"); | |
| if (!fs.existsSync(generatedDir)) fs.mkdirSync(generatedDir, { recursive: true }); | |
| if (results.wrappers_created > 0 || results.issues.length > 0) { | |
| st.last_evolve = new Date().toISOString(); | |
| st.generation++; | |
| saveState(st); | |
| } | |
| return results; | |
| } | |
| export function getEvolveStatus() { | |
| const st = loadState(); | |
| return { | |
| generation: st.generation, | |
| wrappers_created: st.wrappers.length, | |
| errors_fixed: st.errors_fixed, | |
| patches_applied: st.patches.length, | |
| last_evolve: st.last_evolve, | |
| wrapper_list: st.wrappers, | |
| recent_fixes: st.auto_fixes?.slice(-5), | |
| }; | |
| } | |
| // ── 7. SELF-HEAL: full auto-diagnose → fix → test → iterate ── | |
| // Runs until clean or max attempts reached. | |
| export async function selfHeal(options = {}) { | |
| const maxAttempts = options.maxAttempts || 5; | |
| const log = []; | |
| const startTime = Date.now(); | |
| for (let attempt = 1; attempt <= maxAttempts; attempt++) { | |
| const round = { attempt, checks: [], fixes: [], tests: [] }; | |
| // Phase 1: Syntax check all .mjs files | |
| const mjsFiles = ["phantom.mjs", ...fs.readdirSync(resolve(PHANTOM_DIR, "lib")).filter(f => f.endsWith(".mjs")).map(f => `lib/${f}`)]; | |
| let allClean = true; | |
| for (const file of mjsFiles) { | |
| const fp = resolve(PHANTOM_DIR, file); | |
| if (!fs.existsSync(fp)) continue; | |
| try { | |
| execSync(`node --check "${fp}" 2>/dev/null`, { encoding: "utf-8", timeout: 10000 }); | |
| round.checks.push({ file, status: "ok" }); | |
| } catch { | |
| allClean = false; | |
| round.checks.push({ file, status: "error" }); | |
| } | |
| } | |
| // Phase 2: Auto-fix syntax issues | |
| if (!allClean) { | |
| const healResults = await autoHealSyntax(); | |
| for (const r of healResults) { | |
| round.fixes.push(r); | |
| if (r.fixed) { | |
| const st = loadState(); | |
| st.patches.push(r); | |
| st.errors_fixed = (st.errors_fixed || 0) + 1; | |
| saveState(st); | |
| } | |
| } | |
| // If nothing was fixed, break — can't heal further | |
| if (!healResults.some(r => r.fixed)) { | |
| log.push(round); | |
| break; | |
| } | |
| } | |
| // Phase 3: Run tests | |
| try { | |
| const testOut = execSync("node test/core.test.mjs 2>/dev/null && echo 'TESTS_PASSED'", { | |
| cwd: PHANTOM_DIR, encoding: "utf-8", timeout: 60000 | |
| }).trim(); | |
| const passed = testOut.includes("TESTS_PASSED"); | |
| round.tests.push({ status: passed ? "passed" : "failed" }); | |
| if (passed) { | |
| log.push(round); | |
| // If all clean and tests pass, commit and push | |
| try { | |
| const staged = execSync("git diff --cached --stat", { cwd: PHANTOM_DIR, encoding: "utf-8", timeout: 5000 }).trim(); | |
| if (staged) { | |
| const committed = gitCommit([], `🧬 self-heal: auto-fix ${round.fixes.filter(f => f.fixed).length} issue(s)`); | |
| if (committed) { | |
| const pushResult = gitPush(); | |
| round.push = pushResult.pushed ? "pushed" : pushResult.reason || "push_failed"; | |
| } | |
| } | |
| } catch {} | |
| break; // done | |
| } | |
| } catch (e) { | |
| round.tests.push({ status: "error", error: e.stderr?.slice(0, 200) || e.message }); | |
| } | |
| log.push(round); | |
| } | |
| // Update state | |
| const st = loadState(); | |
| st.last_evolve = new Date().toISOString(); | |
| st.generation++; | |
| saveState(st); | |
| return { | |
| elapsed: Date.now() - startTime, | |
| attempts: log.length, | |
| clean: log[log.length - 1]?.tests?.some(t => t.status === "passed") || false, | |
| log, | |
| }; | |
| } | |