Download server.js from ReikerXx/exile: direct link, hf CLI and curl.
- Browser
- Download file 83.4 kB
-
https://huggingface.co/spaces/ReikerXx/exile/resolve/main/server.js
- Command line
-
hf download hf://spaces/ReikerXx/exile/server.js
-
curl -L -o server.js https://huggingface.co/spaces/ReikerXx/exile/resolve/main/server.js
83.4 kB
| const express = require("express"); | |
| const multer = require("multer"); | |
| const fs = require("fs"); | |
| const path = require("path"); | |
| const { exec } = require("child_process"); | |
| const cors = require("cors"); | |
| const axios = require("axios"); | |
| const app = express(); | |
| const PORT = 7860; | |
| const HOME_DIR = path.join(__dirname, "home"); | |
| const META_FILE = path.join(HOME_DIR, ".cryo_meta.json"); | |
| const TASK_FILE = path.join(HOME_DIR, ".cryo_tasks.json"); | |
| const CMD_HISTORY_FILE = path.join(HOME_DIR, ".cryo_cmd_history.json"); | |
| const VERSIONS_FILE = path.join(HOME_DIR, ".cryo_versions.json"); | |
| // ββ CRYO STATE (single source of truth for active file) βββββββββββββββββββββ | |
| const STATE_FILE = path.join(HOME_DIR, ".cryo_state.json"); | |
| function readState() { | |
| try { if (fs.existsSync(STATE_FILE)) return JSON.parse(fs.readFileSync(STATE_FILE, "utf8")); } catch {} | |
| return { activeFile: null, files: [] }; | |
| } | |
| function saveState(patch) { | |
| const s = { ...readState(), ...patch, updated: new Date().toISOString() }; | |
| try { | |
| s.files = fs.readdirSync(HOME_DIR) | |
| .filter(f => !f.startsWith(".") && !f.startsWith("__cryo") && fs.statSync(path.join(HOME_DIR,f)).isFile()); | |
| } catch {} | |
| fs.writeFileSync(STATE_FILE, JSON.stringify(s, null, 2), "utf8"); | |
| return s; | |
| } | |
| function resolveFile(requested) { | |
| if (requested) { saveState({activeFile:requested}); return requested; } | |
| const st = readState(); | |
| if (st.activeFile && fs.existsSync(path.join(HOME_DIR, st.activeFile))) return st.activeFile; | |
| try { | |
| const files = fs.readdirSync(HOME_DIR) | |
| .filter(f => !f.startsWith(".")&&!f.startsWith("__cryo")&&fs.statSync(path.join(HOME_DIR,f)).isFile()); | |
| if (files.length) return files[0]; | |
| } catch {} | |
| return null; | |
| } | |
| if (!fs.existsSync(HOME_DIR)) fs.mkdirSync(HOME_DIR, { recursive: true }); | |
| app.use(cors()); | |
| app.use(express.json({ limit: "10mb" })); | |
| app.use(express.static(path.join(__dirname, "public"))); | |
| // ββ SINGLE MODEL: LongCat API (Anthropic format) ββββββββββββββββββββββββββββ | |
| const LONGCAT_API_URL = "https://api.longcat.chat/anthropic/v1/messages"; | |
| const LONGCAT_API_KEY = "ak_2ep6ba2Ww0pn5cH09U2Mq3Eo0ez1M"; | |
| const LONGCAT_API_KEY_BACKUP = "ak_2k06yh7h44iH7g77T46kB3uJ6b89l"; | |
| // All model keys from the UI map to LongCat-Flash-Lite β one API, no confusion | |
| const MODELS = { | |
| "cryo1": { name: "Cryo 1", label: "cryo1", description: "Fast & light" }, | |
| "cryo2": { name: "Cryo 2", label: "cryo2", description: "Reliable & smart" }, | |
| "cryo3": { name: "Cryo 3", label: "cryo3", description: "Balanced & smart" }, | |
| "cryo4": { name: "Cryo 4", label: "cryo4", description: "Most powerful" }, | |
| }; | |
| // ββ COMMANDS ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // Recommended commands shown as hints β ALL commands allowed except dangerous system ops | |
| const RECOMMENDED_CMDS = [ | |
| "grep","sed","awk","head","tail","cat","wc","sort","uniq","cut","tr","find","ls","echo", | |
| "diff","patch","node","npm","file","stat","cp","mv","mkdir","rm","touch","chmod", | |
| "curl","wget","python3","python","bash","sh","git","zip","unzip","tar","jq","which","env", | |
| // NOTE: tee intentionally excluded β use full_rewrite or str_replace_block for writes | |
| ]; | |
| const BLOCKED_CMDS = ["sudo","su","reboot","shutdown","mkfs","fdisk"]; | |
| // tee is blocked because heredoc quoting with JS code (backticks, $vars, unicode) is catastrophically unreliable. | |
| // Use full_rewrite for whole-file writes, or str_replace_block for targeted replacements. | |
| const TEE_HEREDOC_PATTERN = /^tee\s+/; | |
| function isSafeCmd(cmd) { | |
| if (cmd == null || typeof cmd !== "string") return false; | |
| const name = cmd.trim().split(/\s+/)[0]; | |
| if (!name) return false; | |
| if (BLOCKED_CMDS.includes(name)) return false; | |
| // Block tee entirely β heredoc + JS source code = quoting disaster (see Cryo logs) | |
| if (TEE_HEREDOC_PATTERN.test(cmd.trim())) return false; | |
| return true; | |
| } | |
| const COMMANDS_REFERENCE = ` | |
| ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| β THE IRON LAW β READ THIS BEFORE EVERY ACTION β | |
| β 1. CHANGE ONLY WHAT THE REQUEST EXPLICITLY ASKS FOR. β | |
| β 2. PRESERVE EVERYTHING ELSE EXACTLY β no "improvements", no fixes β | |
| β unless asked, no reformatting, no adding features. β | |
| β 3. BEFORE RESPONDING: self-check β "Did I touch anything not in β | |
| β the request?" If yes, remove those changes. β | |
| β 4. INTERPRET REQUESTS LITERALLY. No assumptions. No extras. β | |
| ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| === SHELL COMMANDS (cwd = /home) β blocked: sudo su reboot shutdown mkfs fdisk tee === | |
| EXPLORE: wc -l file | ls -la | find . -name "*.js" | stat filename | which node | env | |
| READ: sed -n 'X,Yp' file β read section by line range (SAFE β read-only) | |
| grep -n "pattern" file β find with line numbers | |
| head -n N file | tail -n N file | cat file | |
| EDIT: sed -i 's/old/new/g' file β replace ALL occurrences (global) | |
| sed -i 'Ns/old/new/' file β replace ONLY line N (precise β use line from grep) | |
| sed -i 'X,Yd' file β delete lines X through Y | |
| FILE OPS: cp src dst | mv old new | rm file | mkdir -p dir | touch file | |
| BACKUP: cp file file.bak β ALWAYS backup before any rewrite | |
| RUN: node file.js | node --check file.js | npm install pkg | python3 file.py | |
| NETWORK: curl -s URL | wget -q -O out URL | |
| UTILS: jq . file.json | wc -l file | diff a b | |
| β BANNED: tee, echo -e for multiline. These CANNOT write JS source reliably. | |
| Backtick template literals, $vars, unicode, quotes ALL break heredoc/tee. | |
| See command history: 50+ failures proving this. NEVER attempt tee again. | |
| === HOW TO WRITE/ADD CODE β THE TWO VALID STRATEGIES === | |
| STRATEGY A β SURGICAL: sed -i for small targeted changes (< 30% of file) | |
| β’ grep -n to find exact line | |
| β’ sed -n to read and confirm exact text | |
| β’ sed -i 'Ns/exact old text/new text/' to change that line ONLY | |
| β’ grep -n to verify it landed | |
| STRATEGY B β FULL REWRITE: when adding large blocks, restructuring, or sed pattern can't match | |
| Use the JSON field "full_rewrite": true and "new_content": "COMPLETE FILE HERE" | |
| The system will: stage β node --check β backup β write atomically | |
| THIS IS THE ONLY SAFE WAY to add multi-line code blocks. | |
| === SMART FILE READING β ALWAYS DO THIS FIRST (mirrors how Claude reads files) === | |
| Add a "read_file" array to your JSON response. Results come back NEXT round. | |
| ACTIONS: | |
| { "action": "map" } | |
| β File overview: total lines, section headers, function/route signatures with line | |
| numbers, imports, first 30 lines, last 15 lines. USE THIS FIRST on every file. | |
| { "action": "chunk", "start": N, "end": M } | |
| β Lines NβM with line numbers (like sed -n 'N,Mp'). Read before editing. | |
| { "action": "search", "pattern": "someSymbol" } | |
| β All matching lines with line numbers (like grep -n). Find before changing. | |
| EXAMPLE: | |
| { | |
| "read_file": [ | |
| { "action": "map" }, | |
| { "action": "search", "pattern": "bot.command" }, | |
| { "action": "chunk", "start": 45, "end": 90 } | |
| ], | |
| "commands": [], | |
| "done": false, | |
| "status": "Reading file structure before editing" | |
| } | |
| Results appear in your NEXT round's FILE READ RESULTS. | |
| === MANDATORY WORKFLOW === | |
| STEP 1 β MAP: { "read_file": [{ "action": "map" }] } β structure first | |
| STEP 2 β SEARCH: { "read_file": [{ "action": "search", ... }] } β find target | |
| STEP 3 β CHUNK: { "read_file": [{ "action": "chunk", ... }] } β read exact text | |
| STEP 4 β BACKUP: cp file file.bak | |
| STEP 5 β EDIT: sed -i 'Ns/exact-old/new/' file β line-precise | |
| OR: full_rewrite with complete new_content β for additions | |
| STEP 6 β VERIFY: { "read_file": [{ "action": "chunk", ... }] } β confirm change | |
| STEP 7 β CHECK: node --check file.js | |
| === STATELESS CONTEXT RULES (how Claude Code handles being stateless) === | |
| β’ This prompt is your ENTIRE memory β reread ORIGINAL REQUEST every round | |
| β’ COMMAND HISTORY shows exactly what ran, what grep found, what failed | |
| β’ FILE CONTENT has line numbers β use them for sed -i 'Ns/...' precision | |
| β’ If grep shows a line β read it with chunk before editing | |
| β’ If sed exits 0 but linesChanged=no-match β the pattern was wrong, search again | |
| β’ If the same sed pattern failed twice β switch to full_rewrite immediately | |
| β’ NEVER guess at content you haven't read β always verify first | |
| === STRICT RULES === | |
| 1. ALWAYS map or search before editing β NEVER edit blind | |
| 2. ALWAYS chunk-read the exact text BEFORE editing it | |
| 3. ALWAYS cp file.bak before full_rewrite | |
| 4. ALWAYS node --check after every JS change | |
| 5. NEVER use tee β it breaks on backticks, $vars, unicode. Use full_rewrite. | |
| 6. NEVER echo -e for multiline code | |
| 7. Mark done:true ONLY after a chunk/grep CONFIRMS the change is in the file | |
| 8. If sed linesChanged=no-match β search again; pattern was wrong | |
| 9. NEVER repeat a command already in COMMAND HISTORY with the same args | |
| 10. If sed failed twice on same pattern β full_rewrite immediately | |
| 11. SELF-CHECK before done:true β "Does the file contain EXACTLY what was requested?" | |
| 12. CHANGE DETECTION: after every edit, grep for both old and new text to confirm swap | |
| `; | |
| // ββ COMMAND DEDUP & SAFETY FILTER βββββββββββββββββββββββββββββββββββββββββ | |
| // Strips commands already run this session (exact match) and known-dangerous patterns. | |
| // Returns { safe: [...], skipped: [...] } | |
| const DESTRUCTIVE_PATTERNS = [ | |
| /^rm\s+-rf?\s+\//, // rm -rf / | |
| /^>\s*\//, // redirect to root path | |
| /:(){ :|:& };:/, // fork bomb | |
| ]; | |
| function filterCommands(commands, alreadyRun = []) { | |
| const safe = []; | |
| const skipped = []; | |
| const runSet = new Set(alreadyRun.map(c => (typeof c === "string" ? c.trim() : ""))); | |
| // Normalise input: flatten, unwrap {cmd} objects, drop anything not a string | |
| const flat = (Array.isArray(commands) ? commands : []) | |
| .flat(3) | |
| .map(c => (c != null && typeof c === "object" && typeof c.cmd === "string") ? c.cmd : c) | |
| .filter(c => c != null && typeof c === "string"); | |
| for (const cmd of flat) { | |
| const trimmed = cmd.trim(); | |
| if (!trimmed) continue; | |
| if (runSet.has(trimmed)) { skipped.push({ cmd: trimmed, reason: "duplicate" }); continue; } | |
| if (!isSafeCmd(trimmed)) { skipped.push({ cmd: trimmed, reason: "blocked" }); continue; } | |
| if (DESTRUCTIVE_PATTERNS.some(p => p.test(trimmed))) { skipped.push({ cmd: trimmed, reason: "destructive" }); continue; } | |
| safe.push(trimmed); | |
| } | |
| return { safe, skipped }; | |
| } | |
| // ββ STRUCTURED COMMAND RESULT ββββββββββββββββββββββββββββββββββββββββββββββ | |
| // Wraps raw stdout/stderr into a structured object the AI can reason about | |
| function structureResult(cmd, stdout, stderr, code) { | |
| const lines = stdout.split("\n").filter(Boolean); | |
| const isEdit = /^sed\s+-i/.test(cmd); // tee is blocked; only sed -i counts as edit | |
| const isGrep = /^grep/.test(cmd); | |
| const isRead = /^sed\s+-n/.test(cmd) || /^(head|tail|cat)\b/.test(cmd); | |
| const isCheck = cmd.includes("node --check") || cmd.includes("python3 -m py_compile"); | |
| return { | |
| cmd, | |
| exit: code, | |
| ok: code === 0, | |
| stdout: stdout.substring(0, 1200), | |
| stderr: stderr.substring(0, 400), | |
| // Structured fields for common operations | |
| ...(isGrep && { matches: lines.slice(0, 30) }), | |
| ...(isEdit && { linesChanged: code === 0 ? "applied" : "no-match" }), | |
| ...(isRead && { content: stdout.substring(0, 2000) }), | |
| ...(isCheck && { syntaxOk: code === 0, syntaxError: stderr || null }), | |
| warning: code !== 0 ? (stderr || "non-zero exit") : null, | |
| }; | |
| } | |
| // ββ Multer βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const storage = multer.diskStorage({ | |
| destination: HOME_DIR, | |
| filename: (req, file, cb) => cb(null, file.originalname || "index.js"), | |
| }); | |
| const upload = multer({ storage }); | |
| // ββ ROBUST JSON PARSER βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // Handles: plain text, markdown fenced, partial JSON, escaped newlines | |
| function parseJSON(raw) { | |
| if (!raw) return null; | |
| let s = String(raw).trim(); | |
| // Strip markdown fences | |
| s = s.replace(/^```(?:json)?\s*/i, "").replace(/\s*```\s*$/, "").trim(); | |
| // Try direct parse first | |
| try { return JSON.parse(s); } catch {} | |
| // Extract outermost {...} object | |
| const start = s.indexOf("{"); | |
| const end = s.lastIndexOf("}"); | |
| if (start !== -1 && end !== -1 && end > start) { | |
| try { return JSON.parse(s.slice(start, end + 1)); } catch {} | |
| } | |
| // Try to fix common issues: unescaped newlines in string values | |
| try { | |
| const fixed = s | |
| .replace(/:\s*"([\s\S]*?)"/g, (m, v) => ': "' + v.replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t") + '"') | |
| .replace(/,\s*}/g, "}").replace(/,\s*\]/g, "]"); | |
| const fStart = fixed.indexOf("{"), fEnd = fixed.lastIndexOf("}"); | |
| if (fStart !== -1 && fEnd !== -1) return JSON.parse(fixed.slice(fStart, fEnd + 1)); | |
| } catch {} | |
| return null; | |
| } | |
| // ββ SANITIZE AI RESPONSE βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // Ensures "commands" is always a flat array of non-empty strings. | |
| // The AI occasionally returns nulls, objects, nested arrays, or step-label strings. | |
| function sanitizeAIResponse(parsed) { | |
| if (!parsed) return parsed; | |
| if (parsed.commands !== undefined) { | |
| const raw = Array.isArray(parsed.commands) ? parsed.commands : []; | |
| parsed.commands = raw | |
| .flat(5) | |
| .map(c => { | |
| if (c == null) return null; | |
| if (typeof c === "string") return c.trim() || null; | |
| // Unwrap {cmd: "..."} or {command: "..."} objects | |
| if (typeof c === "object") { | |
| const v = c.cmd || c.command || c.shell || c.run || c.exec; | |
| return typeof v === "string" ? v.trim() || null : null; | |
| } | |
| return null; | |
| }) | |
| .filter(c => c != null && c.length > 0 && isSafeCmd(c)); | |
| } | |
| return parsed; | |
| } | |
| // When AI returns markdown/prose instead of JSON, pull shell commands from it | |
| function extractCommandsFromText(text) { | |
| const cmds = []; | |
| // Match lines starting with $ or lines inside code blocks | |
| const codeBlockRe = /```(?:bash|sh|shell)?\s*([\s\S]*?)```/gi; | |
| let m; | |
| while ((m = codeBlockRe.exec(text)) !== null) { | |
| const lines = m[1].split("\n").map(l => l.replace(/^\$\s*/, "").trim()).filter(l => l && !l.startsWith("#")); | |
| cmds.push(...lines); | |
| } | |
| // Also match lines starting with $ outside code blocks | |
| const lines = text.split("\n"); | |
| for (const line of lines) { | |
| const stripped = line.replace(/^\$\s*/, "").trim(); | |
| const firstWord = stripped.split(/\s+/)[0]; | |
| if (line.trim().startsWith("$") && firstWord) { | |
| if (!cmds.includes(stripped)) cmds.push(stripped); | |
| } | |
| } | |
| return cmds.filter(cmd => isSafeCmd(cmd)); | |
| } | |
| // ββ EXTRACT CONTENT FROM TEXT ββββββββββββββββββββββββββββββββββββββββββββββ | |
| // Pull file content from markdown code block in plain-text AI response | |
| function extractContentFromText(text) { | |
| // Match ```js ... ``` or ```javascript ... ``` or ``` ... ``` | |
| const m = text.match(/```(?:js|javascript|typescript|ts|python|py|sh|bash)?\s*([\s\S]+?)```/i); | |
| if (m) return m[1].trim(); | |
| return null; | |
| } | |
| // ββ runCmd βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function runCmd(command) { | |
| return new Promise(resolve => { | |
| if (!isSafeCmd(command)) { | |
| return resolve({ stdout: "", stderr: `Blocked: ${command.trim().split(/\s+/)[0]}`, code: 1 }); | |
| } | |
| exec(command, { cwd: HOME_DIR, timeout: 30000, shell: "/bin/bash" }, (err, stdout, stderr) => { | |
| const code = err ? (err.code || 1) : 0; | |
| resolve({ stdout: stdout || "", stderr: stderr || "", code }); | |
| }); | |
| }); | |
| } | |
| // ββ SMART CONTEXT ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function getSmartContext(content, query, maxLines = 120) { | |
| const lines = content.split("\n"); | |
| if (lines.length <= maxLines) return content; | |
| const head = lines.slice(0, 40).join("\n"); | |
| const tail = lines.slice(-15).join("\n"); | |
| const words = query.toLowerCase().split(/\s+/).filter(w => w.length > 3); | |
| let rs = -1, re = -1; | |
| for (let i = 0; i < lines.length; i++) { | |
| const lower = lines[i].toLowerCase(); | |
| if (words.some(w => lower.includes(w))) { | |
| if (rs === -1) rs = Math.max(0, i - 5); | |
| re = Math.min(lines.length - 1, i + 35); | |
| } | |
| } | |
| const middle = rs !== -1 | |
| ? `\n// ...[${rs}-${re} relevant]...\n` + lines.slice(rs, re + 1).join("\n") | |
| : `\n// ...[${lines.length - 55} lines omitted]...\n`; | |
| return head + middle + tail; | |
| } | |
| // ββ FILE INSPECTION β Claude-style chunked reading βββββββββββββββββββββββββ | |
| // Mirrors exactly how Claude reads files: overview first, then targeted chunks. | |
| // The AI requests inspections; the system resolves them and feeds results back. | |
| // 1. Build a file map: line count, imports, exports, top-level symbols, section headers. | |
| // This is the "wc -l + grep structure" step Claude does first. | |
| function buildFileMap(filePath) { | |
| const abs = path.join(HOME_DIR, filePath); | |
| if (!fs.existsSync(abs)) return null; | |
| const content = fs.readFileSync(abs, "utf8"); | |
| const lines = content.split("\n"); | |
| const ext = path.extname(filePath).toLowerCase(); | |
| // Find structural landmarks by line number | |
| const landmarks = []; | |
| const importLines = [], exportLines = [], fnLines = [], classLines = [], commentSections = []; | |
| lines.forEach((line, i) => { | |
| const n = i + 1; | |
| const t = line.trim(); | |
| if (/^(import|require|from)\b/.test(t) || /\brequire\s*\(/.test(t)) importLines.push(n); | |
| if (/^(export|module\.exports)/.test(t)) exportLines.push(n); | |
| if (/^(async\s+)?function\s+\w+|const\s+\w+\s*=\s*(async\s+)?\(/.test(t) || | |
| /^(app|router)\.(get|post|put|delete|patch|use)\s*\(/.test(t)) fnLines.push({ n, preview: t.slice(0, 80) }); | |
| if (/^class\s+\w+/.test(t)) classLines.push({ n, preview: t.slice(0, 60) }); | |
| if (/^\/\/\s*[ββ=]{3,}/.test(t) || /^#{1,3}\s/.test(t)) commentSections.push({ n, text: t.slice(0, 60) }); | |
| }); | |
| return { | |
| file: filePath, | |
| totalLines: lines.length, | |
| ext, | |
| imports: importLines.slice(0, 5), // first 5 import lines | |
| exports: exportLines.slice(0, 5), | |
| functions: fnLines.slice(0, 30), // up to 30 function signatures | |
| classes: classLines.slice(0, 10), | |
| sections: commentSections.slice(0, 20), // section comment headers | |
| head: lines.slice(0, 30).map((l, i) => `${i+1}\t${l}`).join("\n"), | |
| tail: lines.slice(-15).map((l, i) => `${lines.length-15+i+1}\t${l}`).join("\n"), | |
| }; | |
| } | |
| // 2. Read a specific chunk of a file by line range β like sed -n 'X,Yp' | |
| // Returns line-numbered content so the AI can use exact line numbers in sed. | |
| function readFileChunk(filePath, startLine, endLine) { | |
| const abs = path.join(HOME_DIR, filePath); | |
| if (!fs.existsSync(abs)) return null; | |
| const lines = fs.readFileSync(abs, "utf8").split("\n"); | |
| const s = Math.max(1, startLine) - 1; | |
| const e = Math.min(lines.length, endLine); | |
| return { | |
| file: filePath, | |
| startLine: s + 1, | |
| endLine: e, | |
| totalLines: lines.length, | |
| content: lines.slice(s, e).map((l, i) => `${s + i + 1}\t${l}`).join("\n"), | |
| }; | |
| } | |
| // 3. Search file for a pattern β like grep -n, returns structured matches | |
| function searchFile(filePath, pattern) { | |
| const abs = path.join(HOME_DIR, filePath); | |
| if (!fs.existsSync(abs)) return null; | |
| const lines = fs.readFileSync(abs, "utf8").split("\n"); | |
| let re; | |
| try { re = new RegExp(pattern, "i"); } catch { re = new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i"); } | |
| const matches = []; | |
| lines.forEach((line, i) => { | |
| if (re.test(line)) matches.push({ line: i + 1, text: line.trimEnd().slice(0, 120) }); | |
| }); | |
| return { file: filePath, pattern, totalLines: lines.length, matches: matches.slice(0, 40) }; | |
| } | |
| // 4. Process an array of read_file actions the AI requested: | |
| // [{ action: "map" }, { action: "chunk", start, end }, { action: "search", pattern }] | |
| function processReadActions(filePath, actions = []) { | |
| const results = []; | |
| for (const a of actions) { | |
| if (a.action === "map") { | |
| results.push({ action: "map", result: buildFileMap(filePath) }); | |
| } else if (a.action === "chunk" && a.start && a.end) { | |
| results.push({ action: "chunk", start: a.start, end: a.end, result: readFileChunk(filePath, a.start, a.end) }); | |
| } else if (a.action === "search" && a.pattern) { | |
| results.push({ action: "search", pattern: a.pattern, result: searchFile(filePath, a.pattern) }); | |
| } | |
| } | |
| return results; | |
| } | |
| // 5. Format read results into a concise string the AI can parse in its next prompt | |
| function formatReadResults(readResults) { | |
| return readResults.map(r => { | |
| if (r.action === "map" && r.result) { | |
| const m = r.result; | |
| const fns = m.functions.map(f => ` L${f.n}: ${f.preview}`).join("\n"); | |
| const secs = m.sections.map(s => ` L${s.n}: ${s.text}`).join("\n"); | |
| return `=== FILE MAP: ${m.file} (${m.totalLines} lines) === | |
| IMPORTS at lines: ${m.imports.join(", ") || "none"} | |
| SECTIONS:\n${secs || " (none)"} | |
| FUNCTIONS/ROUTES:\n${fns || " (none)"} | |
| HEAD (lines 1-30):\n${m.head} | |
| TAIL (last 15):\n${m.tail}`; | |
| } | |
| if (r.action === "chunk" && r.result) { | |
| return `=== CHUNK ${r.result.file} lines ${r.result.startLine}-${r.result.endLine} (of ${r.result.totalLines}) ===\n${r.result.content}`; | |
| } | |
| if (r.action === "search" && r.result) { | |
| const hits = r.result.matches.map(m => ` L${m.line}: ${m.text}`).join("\n"); | |
| return `=== SEARCH "${r.result.pattern}" in ${r.result.file} β ${r.result.matches.length} hits ===\n${hits || " (no matches)"}`; | |
| } | |
| return ""; | |
| }).filter(Boolean).join("\n\n"); | |
| } | |
| // ββ VERSIONING βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function readVersions() { | |
| if (!fs.existsSync(VERSIONS_FILE)) return {}; | |
| try { return JSON.parse(fs.readFileSync(VERSIONS_FILE, "utf8")); } catch { return {}; } | |
| } | |
| function saveVersions(v) { | |
| fs.writeFileSync(VERSIONS_FILE, JSON.stringify(v, null, 2), "utf8"); | |
| } | |
| function versionFile(filePath) { | |
| const src = path.join(HOME_DIR, filePath); | |
| if (!fs.existsSync(src)) return; | |
| const ts = Date.now(); | |
| const ext = path.extname(filePath); | |
| const base = filePath.replace(ext, ""); | |
| const vName = `.versions/${base}_v${ts}${ext}`; | |
| const vDir = path.join(HOME_DIR, ".versions"); | |
| if (!fs.existsSync(vDir)) fs.mkdirSync(vDir, { recursive: true }); | |
| fs.copyFileSync(src, path.join(HOME_DIR, vName)); | |
| const versions = readVersions(); | |
| if (!versions[filePath]) versions[filePath] = []; | |
| versions[filePath].push({ path: vName, ts, label: new Date(ts).toISOString() }); | |
| if (versions[filePath].length > 10) versions[filePath] = versions[filePath].slice(-10); | |
| saveVersions(versions); | |
| return vName; | |
| } | |
| // ββ TASK MANAGEMENT ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function readTasks() { | |
| if (!fs.existsSync(TASK_FILE)) return []; | |
| try { return JSON.parse(fs.readFileSync(TASK_FILE, "utf8")); } catch { return []; } | |
| } | |
| function saveTasks(tasks) { | |
| fs.writeFileSync(TASK_FILE, JSON.stringify(tasks, null, 2), "utf8"); | |
| } | |
| function updateTasks(newTasks) { | |
| const existing = readTasks(); | |
| const merged = [...existing, ...newTasks.filter(t => !existing.find(e => e.task === t.task))]; | |
| saveTasks(merged); | |
| return merged; | |
| } | |
| // ββ COMMAND HISTORY ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function appendCmdHistory(entries) { | |
| let hist = []; | |
| if (fs.existsSync(CMD_HISTORY_FILE)) { | |
| try { hist = JSON.parse(fs.readFileSync(CMD_HISTORY_FILE, "utf8")); } catch {} | |
| } | |
| hist.push(...entries); | |
| if (hist.length > 200) hist = hist.slice(-200); | |
| fs.writeFileSync(CMD_HISTORY_FILE, JSON.stringify(hist, null, 2), "utf8"); | |
| } | |
| // ββ PROJECT METADATA βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function readMeta() { | |
| if (!fs.existsSync(META_FILE)) return { created_at: new Date().toISOString(), languages: [], last_ai_edit: null }; | |
| try { return JSON.parse(fs.readFileSync(META_FILE, "utf8")); } catch { return {}; } | |
| } | |
| function updateMeta(patch) { | |
| const meta = { ...readMeta(), ...patch, updated_at: new Date().toISOString() }; | |
| fs.writeFileSync(META_FILE, JSON.stringify(meta, null, 2), "utf8"); | |
| return meta; | |
| } | |
| // ββ WEB SEARCH DETECTION βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const SEARCH_TRIGGERS = [ | |
| /\b(latest|recent|current|today|now|2024|2025|2026|news|trending|new release|just released)\b/i, | |
| /\b(who is|what is the price|when did|how much does|is .* still|does .* exist)\b/i, | |
| /\b(search|look up|find out|google|web|online|internet)\b/i, | |
| /\b(weather|stock|score|result|update|announce|launch)\b/i, | |
| ]; | |
| function needsWebSearch(msg) { | |
| return SEARCH_TRIGGERS.some(r => r.test(msg)); | |
| } | |
| async function webSearch(query) { | |
| const short = query.slice(0, 200); | |
| const url = `https://apis.prexzyvilla.site/ai/gpt-5?text=${encodeURIComponent(short)}`; | |
| const { data } = await axios.get(url, { timeout: 20000 }); | |
| return { answer: data.text || "", citations: data.citations || [], model: data.model || "gpt-5" }; | |
| } | |
| // ββ callAI β LongCat API (Anthropic format) ββββββββββββββββββββββββββββββββ | |
| // system = role/task definition + original user request for context anchoring | |
| // query = specific data for this call (file snippet, cmd results, etc.) | |
| // _modelKey accepted but ignored β all calls route to LongCat-Flash-Lite | |
| // history = [{role:"user"|"assistant", content:"..."}] β conversation transcript | |
| // | |
| // On HTTP 429 / rate_limit_exceeded the call is transparently retried once | |
| // using the backup API key β the caller never sees the error. | |
| async function callAI(system, query, _modelKey, history = []) { | |
| const userMessage = query ? `${system}\n\n${query}` : system; | |
| // Sanitize history β Anthropic expects [{role, content}], trim to last 10 turns | |
| const safeHistory = (Array.isArray(history) ? history : []) | |
| .filter(h => h && (h.role === "user" || h.role === "assistant") && h.content) | |
| .slice(-20) // last 10 user+assistant pairs | |
| .map(h => ({ role: h.role, content: String(h.content).slice(0, 800) })); | |
| const messages = [ | |
| ...safeHistory, | |
| { role: "user", content: userMessage.slice(0, 5500) }, | |
| ]; | |
| const payload = { | |
| model: "LongCat-Flash-Lite", | |
| max_tokens: 2000, | |
| system: `You are Cryo, a precise developer AI. Always respond with a single valid JSON object β no markdown fences, no prose outside the JSON. | |
| IRON LAW: Change ONLY what the user's request explicitly asks for. Never add, fix, reformat, or improve anything else. Interpret requests literally. | |
| WRITE RULES: When a field like "content" or "new_content" holds file source code, write the raw source code as the string value (with \\n for newlines), NOT another JSON object. | |
| BANNED: Never suggest tee or echo -e for writing code. They cannot handle backticks, $vars, or unicode in JS. Use full_rewrite only. | |
| The original user request is embedded in the message β never lose sight of it. Self-check before responding: "Am I only changing what was requested?"`, | |
| messages, | |
| }; | |
| // Inner helper β attempt one call with the given key | |
| async function attempt(apiKey) { | |
| const { data } = await axios.post(LONGCAT_API_URL, payload, { | |
| timeout: 60000, | |
| headers: { | |
| "Content-Type": "application/json", | |
| "Authorization": `Bearer ${apiKey}`, | |
| "anthropic-version": "2023-06-01", | |
| }, | |
| }); | |
| // Anthropic format: data.content is an array of blocks | |
| if (data.content && Array.isArray(data.content)) { | |
| const textBlock = data.content.find(b => b.type === "text"); | |
| return textBlock ? textBlock.text : ""; | |
| } | |
| return data.reply || data.text || data.message || ""; | |
| } | |
| try { | |
| return await attempt(LONGCAT_API_KEY); | |
| } catch (err) { | |
| // Detect quota exhaustion: HTTP 429 with rate_limit_exceeded code | |
| const status = err.response?.status; | |
| const errCode = err.response?.data?.error?.code; | |
| if (status === 429 && errCode === "rate_limit_exceeded") { | |
| // Silently retry with backup key β no error propagated to caller | |
| return await attempt(LONGCAT_API_KEY_BACKUP); | |
| } | |
| throw err; // any other error re-thrown normally | |
| } | |
| } | |
| // ββ SYNTAX FIX LOOP ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function syntaxFixLoop(filePath, send, modelKey, maxAttempts = 3, transcript = []) { | |
| for (let attempt = 1; attempt <= maxAttempts; attempt++) { | |
| const check = await runCmd(`node --check ${filePath}`); | |
| const idx = 800 + attempt; | |
| send("command", { cmd: `node --check ${filePath} (attempt ${attempt})`, index: idx }); | |
| send("command_result", { | |
| cmd: `node --check ${filePath}`, stdout: check.code === 0 ? "Syntax OK" : "", | |
| stderr: check.stderr, index: idx, isError: check.code !== 0 | |
| }); | |
| if (check.code === 0) { | |
| send("status", { text: attempt === 1 ? "Syntax OK" : `Syntax fixed on attempt ${attempt}` }); | |
| return true; | |
| } | |
| // Detect the "AI wrote JSON as file content" mistake β the error will mention | |
| // an unexpected token like ':' or '"' at line 1-3. No point trying to fix it β | |
| // the whole file content is wrong, not just a syntax typo. | |
| const looksLikeJsonBlob = /Unexpected token [':"{[\]]/i.test(check.stderr) && | |
| (() => { try { const t = fs.readFileSync(path.join(HOME_DIR, filePath), "utf8").trim(); return t.startsWith("{") || t.startsWith("["); } catch { return false; } })(); | |
| if (looksLikeJsonBlob) { | |
| send("status", { text: "Content is JSON not source code β skipping fix loop" }); | |
| return false; | |
| } | |
| send("status", { text: `Syntax error (attempt ${attempt}/${maxAttempts}) β fixing...` }); | |
| const absPath = path.join(HOME_DIR, filePath); | |
| const broken = fs.readFileSync(absPath, "utf8"); | |
| const fixedRaw = await callAI( | |
| `Fix this JavaScript syntax error. Return ONLY a JSON object with no extra text:\n{"content":"<complete fixed file here>","fix":"<one line description of fix>"}`, | |
| `SYNTAX ERROR:\n${check.stderr}\n\nFILE CONTENT:\n${broken.slice(0, 3000)}`, | |
| modelKey, | |
| transcript | |
| ); | |
| const fixed = parseJSON(fixedRaw); | |
| if (fixed?.content) { | |
| fs.writeFileSync(absPath, fixed.content, "utf8"); | |
| send("status", { text: `Fix applied: ${fixed.fix || "syntax correction"}` }); | |
| } else { | |
| // Try extracting content from markdown if JSON parse failed | |
| const extracted = extractContentFromText(fixedRaw || ""); | |
| if (extracted) { | |
| fs.writeFileSync(absPath, extracted, "utf8"); | |
| send("status", { text: "Extracted fix from response" }); | |
| } else break; | |
| } | |
| } | |
| return false; | |
| } | |
| // ββ VERIFY TASK COMPLETION ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function verifyTaskCompletion(filePath, tasks, request, modelKey, transcript = []) { | |
| const abs = path.join(HOME_DIR, filePath); | |
| if (!fs.existsSync(abs)) return { tasks, allDone: false, missing: [] }; | |
| const content = fs.readFileSync(abs, "utf8"); | |
| const raw = await callAI( | |
| `Check if the user's request is fully implemented in the file content. | |
| Return ONLY raw JSON: | |
| {"tasks":[{"task":"...","done":true,"note":"where it is or why missing"}],"allDone":true,"missing":["still needed"]}`, | |
| `REQUEST: "${request}"\nFILE ${filePath}:\n${content.substring(0,3000)}\nTASKS:\n${tasks.map((t,i) => (i+1) + ". " + t.task).join("\n")}`, | |
| modelKey, | |
| transcript | |
| ); | |
| return parseJSON(raw) || { tasks, allDone: false, missing: [] }; | |
| } | |
| // ββ ITERATIVE COMMAND LOOP βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // Architecture mirrors Claude Code's actual agent loop: | |
| // 1. Loop exits when the model produces output with NO tool calls (stop_reason != "tool_use") | |
| // β not when the model self-reports done:true (that's unreliable). | |
| // 2. max_turns counts tool-use turns only, matching Claude Code's max_turns semantics. | |
| // 3. TODO state is injected as a reminder after every tool call, matching Claude Code's | |
| // behaviour of keeping the task list visible at every step. | |
| // 4. Context compaction fires when accumulated command log exceeds ~92% of a threshold, | |
| // summarising older entries so the loop can continue beyond context limits. | |
| async function runAgentLoop(opts) { | |
| const { filePath, fileContent: initialContent, message, modelKey, send, maxRounds = 10, transcript = [] } = opts; | |
| // max_turns counts tool-use turns only (matches Claude Code semantics) | |
| const MAX_TOOL_TURNS = maxRounds; | |
| const absPath = path.join(HOME_DIR, filePath); | |
| const allCommandResults = []; | |
| const historyEntries = []; | |
| let cmdIndex = 0; | |
| let fileContent = initialContent; | |
| let sessionTasks = []; | |
| let pendingReadResults = []; // read_file results waiting to be shown to AI in next round | |
| let toolTurnCount = 0; // counts tool-use turns only β matches Claude Code max_turns semantics | |
| // Context compaction: when command log grows beyond this entry count, summarise older | |
| // entries so the loop can continue past context limits (mirrors Claude Code's ~92% compaction) | |
| const COMPACTION_THRESHOLD = 40; | |
| // ββ Helper: run a batch of commands and collect results βββββββββββββββ | |
| // Deduplicates against already-run commands, structures output, guards retries | |
| const cmdRetryCount = {}; // tracks per-cmd-pattern retry attempts | |
| async function runBatch(commands) { | |
| // Normalise before anything else β AI sometimes returns nulls, objects, nested arrays | |
| const normalised = (Array.isArray(commands) ? commands : []) | |
| .flat(3) | |
| .map(c => (c != null && typeof c === "object" && typeof c.cmd === "string") ? c.cmd : c) | |
| .filter(c => c != null && typeof c === "string" && c.trim().length > 0); | |
| const alreadyRunCmds = allCommandResults.map(r => r.cmd); | |
| const { safe, skipped } = filterCommands(normalised, alreadyRunCmds); | |
| // Notify about skipped commands so AI sees them in history | |
| for (const s of skipped) { | |
| const reason = s.cmd.trim().startsWith("tee ") | |
| ? "BANNED (tee cannot write JS source β use full_rewrite)" | |
| : `Skipped (${s.reason})`; | |
| send("status", { text: `${reason}: ${s.cmd.slice(0, 80)}` }); | |
| } | |
| for (const cmd of safe) { | |
| // Per-command retry cap: same base command (first 40 chars) max 3 times | |
| const cmdKey = cmd.slice(0, 40); | |
| cmdRetryCount[cmdKey] = (cmdRetryCount[cmdKey] || 0) + 1; | |
| if (cmdRetryCount[cmdKey] > 3) { | |
| send("status", { text: `Max retries reached for: ${cmd.slice(0, 60)}` }); | |
| continue; | |
| } | |
| // Count this as a tool-use turn (matches Claude Code max_turns semantics) | |
| toolTurnCount++; | |
| const idx = cmdIndex++; | |
| send("status", { text: `[${idx + 1}] ${cmd}` }); | |
| send("command", { cmd, index: idx }); | |
| const result = await runCmd(cmd); | |
| const structured = structureResult(cmd, result.stdout, result.stderr, result.code); | |
| allCommandResults.push({ cmd, ...result, structured }); | |
| historyEntries.push({ cmd, stdout: result.stdout.slice(0, 500), stderr: result.stderr.slice(0, 200), code: result.code, ts: Date.now() }); | |
| // Emit full structured result β frontend sees stdout, stderr, exit code | |
| send("command_result", { | |
| cmd, | |
| stdout: result.stdout, | |
| stderr: result.stderr, | |
| exit: result.code, | |
| ok: result.code === 0, | |
| structured, | |
| index: idx, | |
| isError: result.code !== 0 && !!result.stderr, | |
| }); | |
| // ββ TODO reminder after every tool use (mirrors Claude Code architecture) ββ | |
| // Claude Code injects the current TODO state as a system reminder after each tool | |
| // call to prevent the model losing track of objectives in long conversations. | |
| const pendingAfterTool = sessionTasks.filter(t => !t.done); | |
| if (sessionTasks.length > 0) { | |
| send("task_reminder", { | |
| tasks: sessionTasks, | |
| pending: pendingAfterTool.length, | |
| text: pendingAfterTool.length > 0 | |
| ? `π TODO: ${pendingAfterTool.map(t => t.task).join(" | ")}` | |
| : "β All tasks complete" | |
| }); | |
| } | |
| // Auto-fix syntax errors in JS files | |
| if (cmd.includes("node --check") && result.code !== 0 && result.stderr) { | |
| const target = cmd.trim().split(/\s+/).pop().replace(/\s*\(.*\)$/, ""); | |
| send("status", { text: `Syntax error in ${target} β auto-fixing...` }); | |
| await syntaxFixLoop(target, send, modelKey, 3); | |
| } | |
| fileContent = fs.existsSync(absPath) ? fs.readFileSync(absPath, "utf8") : fileContent; | |
| } | |
| } | |
| // ββ Build the thread context block β passed to EVERY AI call ββββββββββ | |
| // This IS the agent's memory β conversation history serialised into prompt. | |
| // Following the sheet: include original request, full task state, every | |
| // command result (what was FOUND, not just exit code), and line-numbered | |
| // file context so the AI can write precise sed line-number commands. | |
| function buildThreadContext(round) { | |
| const fileLines = fileContent.split("\n").length; | |
| // Line-numbered snippet β the AI needs line numbers for sed -i 'Ns/...' | |
| const lines = fileContent.split("\n"); | |
| const smartLines = getSmartContext(fileContent, message).split("\n"); | |
| // Attach line numbers to the smart context section so AI can reference them | |
| const numberedCtx = (() => { | |
| // Find where our smartCtx starts inside the full file | |
| const headCount = Math.min(40, lines.length); | |
| const headBlock = lines.slice(0, headCount) | |
| .map((l, i) => `${i + 1}\t${l}`).join("\n"); | |
| const tailStart = Math.max(headCount, lines.length - 15); | |
| const tailBlock = lines.slice(tailStart) | |
| .map((l, i) => `${tailStart + i + 1}\t${l}`).join("\n"); | |
| // Middle: find relevant lines by query keywords | |
| const words = message.toLowerCase().split(/\s+/).filter(w => w.length > 3); | |
| let rs = -1, re = -1; | |
| for (let i = headCount; i < tailStart; i++) { | |
| if (words.some(w => lines[i].toLowerCase().includes(w))) { | |
| if (rs === -1) rs = Math.max(headCount, i - 4); | |
| re = Math.min(tailStart - 1, i + 20); | |
| } | |
| } | |
| const middleBlock = rs !== -1 | |
| ? `\n...[lines ${headCount + 1}β${rs} omitted]...\n` + | |
| lines.slice(rs, re + 1).map((l, i) => `${rs + i + 1}\t${l}`).join("\n") + | |
| `\n...[lines ${re + 1}β${tailStart} omitted]...` | |
| : `\n...[lines ${headCount + 1}β${tailStart} omitted]...`; | |
| return (headBlock + middleBlock + "\n" + tailBlock).substring(0, 2200); | |
| })(); | |
| // ββ Context compaction (mirrors Claude Code's ~92% context window compaction) ββ | |
| // When the command log grows large, summarise older entries to prevent context overflow. | |
| // Claude Code does this automatically; we replicate the behaviour here. | |
| let compactionNote = ""; | |
| let cmdLogEntries = allCommandResults; | |
| if (allCommandResults.length > COMPACTION_THRESHOLD) { | |
| const kept = allCommandResults.slice(-14); | |
| const compacted = allCommandResults.slice(0, allCommandResults.length - 14); | |
| const editCount = compacted.filter(r => r.cmd.startsWith("sed -i") || r.cmd.startsWith("[full_rewrite]")).length; | |
| const failCount = compacted.filter(r => r.code !== 0).length; | |
| compactionNote = `[Context compacted: ${compacted.length} earlier entries β ${editCount} edits, ${failCount} failures]\n`; | |
| cmdLogEntries = kept; | |
| } | |
| // Full command history with structured outputs β AI knows exactly what grep found, | |
| // what sed changed, what errors occurred, without guessing from raw text | |
| const cmdLog = cmdLogEntries.slice(-14).map((r, i) => { | |
| const s = r.structured || {}; | |
| const exitLabel = r.code === 0 ? "β" : `β(${r.code})`; | |
| let detail = ""; | |
| if (s.matches) detail = `\n MATCHES: ${s.matches.slice(0,10).join(" | ")}`; | |
| else if (s.content) detail = `\n CONTENT: ${s.content.slice(0, 300)}`; | |
| else if (r.stdout.trim()) detail = `\n OUT: ${r.stdout.trim().slice(0, 300)}`; | |
| if (r.stderr.trim()) detail += `\n ERR: ${r.stderr.trim().slice(0, 150)}`; | |
| if (s.linesChanged) detail += `\n EDIT: ${s.linesChanged}`; | |
| if (s.syntaxError) detail += `\n SYNTAX: ${s.syntaxError.slice(0, 120)}`; | |
| return `[${i + 1}] ${exitLabel} $ ${r.cmd}${detail}`; | |
| }).join("\n"); | |
| // Highlight any commands that failed β the AI must not repeat them blindly | |
| const failedCmds = allCommandResults.filter(r => r.code !== 0); | |
| const skippedTee = allCommandResults.filter(r => r.cmd.startsWith("[BANNED]")); | |
| const failureNote = (failedCmds.length > 0 || skippedTee.length > 0) | |
| ? `\nFAILED/BANNED COMMANDS (do NOT repeat these patterns):\n` + | |
| failedCmds.slice(-4).map(r => ` β ${r.cmd} β ${r.stderr.trim().slice(0, 120)}`).join("\n") + | |
| (skippedTee.length > 0 ? `\n β tee was blocked β use full_rewrite to write code` : "") | |
| : ""; | |
| const taskStatus = sessionTasks.length > 0 | |
| ? sessionTasks.map((t, i) => ` ${t.done ? "[x]" : "[ ]"} ${i + 1}. ${t.task}`).join("\n") | |
| : " (none planned yet)"; | |
| const pending = sessionTasks.filter(t => !t.done); | |
| return `=== CRYO AGENT β Round ${round}/${MAX_TOOL_TURNS} | Tool turns used: ${toolTurnCount}/${MAX_TOOL_TURNS} === | |
| ORIGINAL REQUEST (never lose this): "${message}" | |
| FILE: ${filePath} | TOTAL LINES: ${fileLines} | |
| ${compactionNote}${failureNote} | |
| TASK LIST: | |
| ${taskStatus} | |
| PENDING (${pending.length} remaining): | |
| ${pending.length > 0 ? pending.map((t, i) => ` ${i + 1}. ${t.task}`).join("\n") : " ALL TASKS DONE"} | |
| COMMAND HISTORY (${allCommandResults.length} total β includes grep results, sed outputs, check results): | |
| ${cmdLog || " (none yet β start with read_file map)"} | |
| ${pendingReadResults.length > 0 ? ` | |
| FILE READ RESULTS (from your read_file requests last round): | |
| ${formatReadResults(pendingReadResults)} | |
| ` : ""} | |
| FILE CONTENT WITH LINE NUMBERS (use these line numbers in sed -i 'Ns/...' commands): | |
| ${numberedCtx} | |
| === END CONTEXT ===`; | |
| } | |
| // ββ ROUND 1: Master plan ββββββββββββββββββββββββββββββββββββββββββββββ | |
| send("status", { text: "Analyzing request..." }); | |
| const fileLines = initialContent.split("\n").length; | |
| // Build line-numbered initial context for Round 1 (same logic as buildThreadContext) | |
| const initLines = initialContent.split("\n"); | |
| const initHeadBlock = initLines.slice(0, Math.min(40, initLines.length)) | |
| .map((l, i) => `${i + 1}\t${l}`).join("\n"); | |
| const initTailStart = Math.max(40, initLines.length - 15); | |
| const initTailBlock = initLines.slice(initTailStart) | |
| .map((l, i) => `${initTailStart + i + 1}\t${l}`).join("\n"); | |
| const initCtx = (initHeadBlock + `\n...[lines 41β${initTailStart} omitted β use grep -n to find sections]...\n` + initTailBlock).substring(0, 2200); | |
| // ββ Auto file map for Round 1 β AI gets full structure before planning ββ | |
| // This is exactly what Claude does: understand structure before touching anything. | |
| const fileMap = buildFileMap(filePath); | |
| const fileMapStr = fileMap ? formatReadResults([{ action: "map", result: fileMap }]) : ""; | |
| const planPrompt = `You are Cryo - a developer AI agent. You edit files using shell commands. | |
| AGENT LOOP MODEL (exactly how Claude Code works): | |
| Phase 1 β GATHER CONTEXT: read the file, search for relevant sections, understand structure. | |
| Phase 2 β TAKE ACTION: make the changes requested. Use sed for small edits, full_rewrite for large ones. | |
| Phase 3 β VERIFY RESULTS: grep to confirm changes landed, node --check for syntax, re-read if needed. | |
| These phases BLEND and REPEAT. After verifying, you may need to gather more context for the next task. | |
| The loop exits when you produce a response with NO commands and NO read_file actions. | |
| DO NOT self-terminate with done:true unless you have already VERIFIED the change is in the file. | |
| ${COMMANDS_REFERENCE} | |
| ${fileMapStr ? `=== FILE STRUCTURE (auto-read before you start) ===\n${fileMapStr}\n` : ""} | |
| FILE: ${filePath} | TOTAL LINES: ${fileLines} | |
| FILE CONTENT HEAD+TAIL (with line numbers): | |
| ${initCtx} | |
| USER REQUEST: "${message}" | |
| βββ IRON LAW β APPLY BEFORE EVERY DECISION βββ | |
| β CHANGE ONLY what the request explicitly asks. | |
| β DO NOT add, fix, reformat, or "improve" anything else. | |
| β Treat the request LITERALLY β no assumptions, no extras. | |
| β WHAT NOT TO CHANGE: any code the request does not mention. | |
| β SELF-CHECK before responding: "Am I touching anything unrequested?" | |
| ββββββββββββββββββββββββββββββββββββββββββββββ | |
| WRITING CODE RULES: | |
| β BANNED: tee, echo -e for multiline. These cannot handle JS source (backticks, $vars fail). | |
| β TO ADD MULTI-LINE CODE: use full_rewrite with complete new_content. | |
| β TO CHANGE 1-3 LINES: use sed -i 'Ns/exact-old/new/' after reading with chunk. | |
| For your FIRST response, output read_file actions to read the sections you'll need, | |
| OR output commands if you already have enough context from the file map above. | |
| Respond with ONLY raw JSON: | |
| { | |
| "task_type": "query" | "edit" | "create", | |
| "status": "brief status for the user", | |
| "tasks": [{"task": "specific subtask", "done": false}], | |
| "read_file": [{ "action": "chunk", "start": N, "end": M }, ...], | |
| "commands": ["cp ${filePath} ${filePath}.bak", "..."], | |
| "reasoning": "what sections you need to read and why, and what you will NOT change", | |
| "done": false | |
| } | |
| COMMANDS FORMAT RULES β VIOLATIONS WILL CRASH THE SYSTEM: | |
| - "commands" MUST be a flat array of plain strings only | |
| - NEVER put null, objects, or nested arrays inside "commands" | |
| - NEVER omit "commands" β use [] if no commands needed this round | |
| - NEVER include tee or echo -e for writing code | |
| - Every entry must be a complete, executable bash command string`; | |
| const planRaw = await callAI(planPrompt, "", modelKey, transcript); | |
| let plan = sanitizeAIResponse(parseJSON(planRaw)); | |
| if (!plan || (!plan.commands && !plan.read_file)) { | |
| const extracted = extractCommandsFromText(planRaw || ""); | |
| plan = { | |
| task_type: "edit", | |
| status: "Running commands...", | |
| tasks: [{ task: message, done: false }], | |
| commands: extracted, | |
| reasoning: "Extracted from response", | |
| done: false | |
| }; | |
| } | |
| sessionTasks = plan.tasks || [{ task: message, done: false }]; | |
| send("status", { text: plan.status || "Processing..." }); | |
| send("task_update", { tasks: sessionTasks }); | |
| // Process any read_file actions from the plan (Round 1 reads) | |
| if (plan.read_file && Array.isArray(plan.read_file) && plan.read_file.length > 0) { | |
| send("status", { text: `Reading file structure (${plan.read_file.map(a => a.action).join(", ")})...` }); | |
| const readResults = processReadActions(filePath, plan.read_file); | |
| pendingReadResults = readResults; | |
| for (const r of readResults) { | |
| const label = r.action === "map" ? "file map" : r.action === "chunk" ? `lines ${r.start}β${r.end}` : `search "${r.pattern}"`; | |
| send("command_result", { cmd: `[read_file:${r.action}] ${filePath} ${label}`, stdout: formatReadResults([r]), stderr: "", exit: 0, ok: true, isError: false, index: cmdIndex++ }); | |
| } | |
| } | |
| await runBatch(plan.commands || []); | |
| // ββ ROUNDS 2βN: Continue until model stops calling tools βββββββββββββββββ | |
| // Exit condition mirrors Claude Code exactly: | |
| // "Turns continue until Claude produces output with no tool calls" | |
| // self-reported done:true is used as a HINT only β the real exit is no-tools output. | |
| // max_turns caps tool-use turns (not rounds), matching Claude Code's max_turns semantics. | |
| for (let round = 2; round <= MAX_TOOL_TURNS * 3; round++) { // outer round cap is generous; tool turns cap is enforced below | |
| // Enforce tool-turn budget (like Claude Code's max_turns / maxBudgetUsd) | |
| if (toolTurnCount >= MAX_TOOL_TURNS) { | |
| send("status", { text: `Tool turn budget reached (${toolTurnCount}/${MAX_TOOL_TURNS}) β stopping` }); | |
| break; | |
| } | |
| // Re-read fresh file content | |
| fileContent = fs.existsSync(absPath) ? fs.readFileSync(absPath, "utf8") : fileContent; | |
| const threadCtx = buildThreadContext(round); | |
| send("status", { text: `Round ${round}: checking progress...` }); | |
| const continuePrompt = `You are Cryo - a developer AI agent. You are mid-task. Study the full context below. | |
| ${threadCtx} | |
| ${COMMANDS_REFERENCE} | |
| AGENT LOOP β HOW TO EXIT (matches Claude Code exactly): | |
| The loop stops when you return a response with NO commands AND NO read_file. | |
| That is the "no tool calls" exit β do not use done:true to stop unless you have zero remaining actions. | |
| If you still have work to do, include commands or read_file. The loop will continue. | |
| done:true is a HINT that verification passed β it does NOT stop the loop by itself. | |
| THREE PHASES (blend and repeat as needed): | |
| 1. GATHER CONTEXT β read_file map/search/chunk to understand the exact target | |
| 2. TAKE ACTION β sed -i for 1-3 line edits, full_rewrite for large additions | |
| 3. VERIFY β grep to confirm new text is present, node --check for syntax | |
| β BANNED THIS SESSION: tee, echo -e for multiline. They CANNOT write JS source. | |
| Use full_rewrite to add multi-line code. Use sed -i for 1-3 line changes. | |
| β READ THE COMMAND HISTORY β it shows what grep found, what lines exist, what failed. | |
| β USE THE LINE NUMBERS in the file content to write precise sed -i 'Ns/...' commands. | |
| β NEVER repeat a command already in COMMAND HISTORY with the same arguments. | |
| DECISION TREE THIS ROUND: | |
| - If you haven't read the target section β read_file chunk/search first | |
| - If grep found lines β sed -n 'START,ENDp' to read exact text before editing | |
| - If you have exact text β sed -i 'Ns/old/new/' for 1-3 lines, OR full_rewrite for multi-line | |
| - If you just edited β grep -n to prove the NEW text is in the file (change detection) | |
| - If JS file was written β node --check to verify syntax | |
| - If sed pattern failed twice β switch to full_rewrite immediately | |
| - If all tasks verified in file β set done:true | |
| CHANGE DETECTION (mandatory before done:true): | |
| After EVERY edit, run grep to confirm: | |
| 1. The OLD text is gone: grep -c "old pattern" file β should be 0 | |
| 2. The NEW text exists: grep -n "new pattern" file β should show your line | |
| Respond with ONLY raw JSON (choose A or B): | |
| A) Normal commands: | |
| { | |
| "done": true | false, | |
| "status": "what you are doing", | |
| "tasks": [{"task": "...", "done": true|false}], | |
| "commands": ["grep -n ...", "sed -n ...", "sed -i ...", "node --check ..."], | |
| "reasoning": "what you found, what you changed, what you VERIFIED is in the file" | |
| } | |
| B) Full file rewrite (for adding code blocks or when sed fails): | |
| { | |
| "done": true, | |
| "full_rewrite": true, | |
| "new_content": "COMPLETE new file content here", | |
| "status": "Rewrote file to add X", | |
| "tasks": [{"task": "...", "done": true}], | |
| "reasoning": "why full_rewrite was needed; confirm request was implemented" | |
| } | |
| COMMANDS FORMAT RULES β VIOLATIONS WILL CRASH THE SYSTEM: | |
| - "commands" MUST be a flat array of plain strings. Example: ["sed -i '7s/get/head/' bot.js"] | |
| - NEVER put objects, null, arrays-within-arrays, or non-strings in "commands" | |
| - NEVER omit "commands" β use [] if you have no commands to run | |
| - NEVER include tee β it is blocked and will be skipped | |
| - EVERY command must be a complete shell command runnable in bash | |
| - NEVER include comments, explanations, or step labels inside the commands array | |
| CRITICAL: done:true only when grep CONFIRMS the change is physically in the file, | |
| OR full_rewrite:true is set and the complete file was provided in new_content. | |
| NEVER set done:true based only on the fact that sed ran β sed exits 0 even on no-match.`; | |
| const contRaw = await callAI(continuePrompt, "", modelKey, transcript); | |
| let cont = sanitizeAIResponse(parseJSON(contRaw)); | |
| if (!cont) { | |
| const extracted = extractCommandsFromText(contRaw || ""); | |
| if (extracted.length === 0) break; | |
| cont = { done: false, commands: extracted, tasks: sessionTasks, status: "Continuing...", reasoning: "Extracted" }; | |
| } | |
| // ββ READ_FILE: AI requested structured file inspection βββββββββββββ | |
| // Process immediately and store results for the NEXT round's context. | |
| // This is the "Claude reads file in chunks" pattern β map β search β chunk β edit. | |
| if (cont.read_file && Array.isArray(cont.read_file) && cont.read_file.length > 0) { | |
| send("status", { text: `Reading file (${cont.read_file.map(a => a.action).join(", ")})...` }); | |
| const readResults = processReadActions(filePath, cont.read_file); | |
| pendingReadResults = readResults; // will appear in next round's context | |
| // Also emit each result to the frontend so user can see what was read | |
| for (const r of readResults) { | |
| const label = r.action === "map" ? "file map" | |
| : r.action === "chunk" ? `lines ${r.start}β${r.end}` | |
| : `search "${r.pattern}"`; | |
| send("command_result", { | |
| cmd: `[read_file:${r.action}] ${filePath} ${label}`, | |
| stdout: formatReadResults([r]), | |
| stderr: "", | |
| exit: 0, | |
| ok: true, | |
| isError: false, | |
| index: cmdIndex++, | |
| }); | |
| } | |
| // Update task list if AI provided one | |
| if (cont.tasks && cont.tasks.length > 0) { | |
| sessionTasks = cont.tasks; | |
| send("task_update", { tasks: sessionTasks }); | |
| } | |
| send("status", { text: cont.status || `Read complete β round ${round + 1} will use results` }); | |
| // Also run any commands the AI included alongside read_file | |
| if (cont.commands && cont.commands.length > 0) await runBatch(cont.commands); | |
| continue; // go to next round with read results in context | |
| } | |
| // Clear pending reads once the AI has seen them (they were in this round's context) | |
| pendingReadResults = []; | |
| if (cont.full_rewrite && cont.new_content) { | |
| send("status", { text: "Staging full rewrite..." }); | |
| const stageName = `__cryo_stage_${Date.now()}${path.extname(filePath)}`; | |
| const stagePath = path.join(HOME_DIR, stageName); | |
| fs.writeFileSync(stagePath, cont.new_content, "utf8"); | |
| // Validate staged file before overwriting | |
| let stageOk = true; | |
| if (filePath.endsWith(".js")) { | |
| const chk = await runCmd(`node --check ${stageName}`); | |
| send("command_result", { cmd: `node --check ${stageName}`, stdout: chk.stdout, stderr: chk.stderr, exit: chk.code, ok: chk.code === 0, isError: chk.code !== 0, index: cmdIndex++ }); | |
| if (chk.code !== 0) { | |
| send("status", { text: "Staged rewrite has syntax errors β fixing..." }); | |
| await syntaxFixLoop(stageName, send, modelKey, 3, transcript); | |
| stageOk = (await runCmd(`node --check ${stageName}`)).code === 0; | |
| } | |
| } | |
| if (stageOk) { | |
| versionFile(filePath); // backup current before overwrite | |
| fs.copyFileSync(stagePath, absPath); | |
| fs.unlinkSync(stagePath); | |
| fileContent = fs.readFileSync(absPath, "utf8"); | |
| send("command_result", { cmd: `[full_rewrite] ${filePath}`, stdout: "β File replaced atomically", stderr: "", exit: 0, ok: true, isError: false, index: cmdIndex++ }); | |
| allCommandResults.push({ cmd: `[full_rewrite] ${filePath}`, stdout: "replaced", stderr: "", code: 0 }); | |
| send("status", { text: "Full rewrite applied β" }); | |
| if (cont.tasks && cont.tasks.length > 0) { sessionTasks = cont.tasks; send("task_update", { tasks: sessionTasks }); } | |
| if (cont.done) break; | |
| continue; | |
| } else { | |
| fs.unlinkSync(stagePath); | |
| send("status", { text: "Staged rewrite failed validation β continuing with commands" }); | |
| } | |
| } | |
| if (!cont) { | |
| const extracted = extractCommandsFromText(contRaw || ""); | |
| if (extracted.length === 0) break; | |
| cont = { done: false, commands: extracted, tasks: sessionTasks, status: "Continuing...", reasoning: "Extracted" }; | |
| } | |
| // Update task list from AI's assessment | |
| if (cont.tasks && cont.tasks.length > 0) { | |
| sessionTasks = cont.tasks; | |
| send("task_update", { tasks: sessionTasks }); | |
| } | |
| if (cont.done || !cont.commands || cont.commands.length === 0) { | |
| // ββ This is the Claude Code stop_reason != "tool_use" exit point ββ | |
| // If the model returned no commands AND no read_file actions, it has | |
| // decided there is nothing more to call β this IS the loop's natural exit. | |
| const hasNoToolCalls = (!cont.commands || cont.commands.length === 0) && | |
| (!cont.read_file || cont.read_file.length === 0); | |
| if (hasNoToolCalls) { | |
| // Model stopped calling tools β loop exits (mirrors Claude Code exactly) | |
| send("status", { text: cont.status || "Model finished β no more tool calls" }); | |
| if (cont.tasks && cont.tasks.length > 0) { sessionTasks = cont.tasks; send("task_update", { tasks: sessionTasks }); } | |
| break; | |
| } | |
| // done:true with tasks still marked pending β used as hint only, verify below | |
| const stillPending = sessionTasks.filter(t => !t.done); | |
| // ββ MANDATORY GROUND-TRUTH CHECK βββββββββββββββββββββββββββββββββ | |
| // Never trust the AI's self-reported done:true without verifying | |
| // the actual file on disk. The AI cannot see its own sed output β | |
| // it only knows what it sent, not what actually landed. | |
| if (cont.done && stillPending.length === 0) { | |
| fileContent = fs.existsSync(absPath) ? fs.readFileSync(absPath, "utf8") : fileContent; | |
| const checkRaw = await callAI( | |
| `Check if the user's request is fully present in the file content. | |
| Return ONLY JSON: {"verified": true | false, "reason": "what is present or missing"}`, | |
| `REQUEST: "${message}"\nFILE CONTENT:\n${fileContent.substring(0, 3000)}`, | |
| modelKey, | |
| transcript | |
| ); | |
| const check = parseJSON(checkRaw); | |
| if (check?.verified === false) { | |
| // AI claimed done but the change isn't in the file β keep going | |
| send("status", { text: `Verification failed: ${check.reason} β retrying...` }); | |
| // Force one more round | |
| sessionTasks = sessionTasks.map(t => ({ ...t, done: false })); | |
| send("task_update", { tasks: sessionTasks }); | |
| if (round < maxRounds) continue; | |
| } | |
| send("status", { text: cont.status || "All tasks complete β" }); | |
| break; | |
| } | |
| if (stillPending.length === 0 || cont.done) { | |
| send("status", { text: cont.status || "All tasks complete β" }); | |
| break; | |
| } | |
| // AI said done but tasks aren't β push one more round | |
| if (round >= maxRounds - 1) break; | |
| send("status", { text: `Verifying ${stillPending.length} pending task(s)...` }); | |
| continue; | |
| } | |
| send("status", { text: cont.status || `Round ${round}...` }); | |
| await runBatch(cont.commands); | |
| } | |
| // Final file read | |
| fileContent = fs.existsSync(absPath) ? fs.readFileSync(absPath, "utf8") : fileContent; | |
| appendCmdHistory(historyEntries); | |
| return { | |
| allCommandResults, | |
| fileContent, | |
| plan, | |
| sessionTasks, | |
| toolTurnCount, | |
| // A sed edit only "happened" if it ran AND produced output or was verified β exit 0 alone is not enough | |
| // because `sed -i` returns 0 even when the pattern didn't match anything on some systems. | |
| // tee is blocked entirely β no tee detection needed. | |
| hadSedEdits: allCommandResults.some(r => | |
| r.cmd.startsWith("sed -i") && r.code === 0 && | |
| (r.structured?.linesChanged === "applied" || !r.stderr) | |
| ), | |
| hadTeeWrites: false, // tee is blocked β this is always false now | |
| hadFullRewrite: allCommandResults.some(r => r.cmd.startsWith("[full_rewrite]") && r.code === 0), | |
| }; | |
| } | |
| // ββ FILE INSPECT API (map / chunk / search) βββββββββββββββββββββββββββββββ | |
| app.post("/api/inspect", (req, res) => { | |
| const { filePath, actions } = req.body; | |
| if (!filePath) return res.status(400).json({ error: "filePath required" }); | |
| const abs = path.join(HOME_DIR, filePath); | |
| if (!abs.startsWith(HOME_DIR)) return res.status(403).json({ error: "Forbidden" }); | |
| const results = processReadActions(filePath, actions || [{ action: "map" }]); | |
| res.json({ results, formatted: formatReadResults(results) }); | |
| }); | |
| // ββ FILE TREE ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| app.get("/api/tree", (req, res) => { | |
| const buildTree = (dir, base = HOME_DIR) => { | |
| try { | |
| return fs.readdirSync(dir, { withFileTypes: true }) | |
| .filter(e => !e.name.startsWith(".cryo_") && !e.name.startsWith(".versions") && !e.name.startsWith("__cryo")) | |
| .map(e => { | |
| const relPath = path.relative(base, path.join(dir, e.name)); | |
| if (e.isDirectory()) return { type: "dir", name: e.name, path: relPath, children: buildTree(path.join(dir, e.name), base) }; | |
| const stats = fs.statSync(path.join(dir, e.name)); | |
| return { type: "file", name: e.name, path: relPath, size: stats.size }; | |
| }); | |
| } catch { return []; } | |
| }; | |
| try { res.json({ tree: buildTree(HOME_DIR) }); } | |
| catch { res.json({ tree: [] }); } | |
| }); | |
| app.post("/api/upload", upload.single("file"), (req, res) => { | |
| res.json({ success: true, path: req.file?.filename || "index.js" }); | |
| }); | |
| app.get("/api/file", (req, res) => { | |
| const fp = path.join(HOME_DIR, req.query.path || "index.js"); | |
| if (!fp.startsWith(HOME_DIR)) return res.status(403).json({ error: "Forbidden" }); | |
| if (!fs.existsSync(fp)) return res.status(404).json({ error: "Not found" }); | |
| res.json({ content: fs.readFileSync(fp, "utf8") }); | |
| }); | |
| app.post("/api/file", (req, res) => { | |
| const { filePath, content } = req.body; | |
| const abs = path.join(HOME_DIR, filePath); | |
| if (!abs.startsWith(HOME_DIR)) return res.status(403).json({ error: "Forbidden" }); | |
| versionFile(filePath); | |
| fs.mkdirSync(path.dirname(abs), { recursive: true }); | |
| fs.writeFileSync(abs, content, "utf8"); | |
| res.json({ success: true }); | |
| }); | |
| app.delete("/api/file", (req, res) => { | |
| const { filePath } = req.body; | |
| const abs = path.join(HOME_DIR, filePath); | |
| if (!abs.startsWith(HOME_DIR)) return res.status(403).json({ error: "Forbidden" }); | |
| if (fs.existsSync(abs)) fs.unlinkSync(abs); | |
| res.json({ success: true }); | |
| }); | |
| app.get("/api/download", (req, res) => { | |
| const fp = path.join(HOME_DIR, req.query.path); | |
| if (!fp.startsWith(HOME_DIR)) return res.status(403).json({ error: "Forbidden" }); | |
| if (!fs.existsSync(fp)) return res.status(404).json({ error: "Not found" }); | |
| res.download(fp); | |
| }); | |
| app.get("/api/state", (req, res) => res.json(readState())); | |
| app.get("/api/tasks", (req, res) => res.json({ tasks: readTasks() })); | |
| app.get("/api/history", (req, res) => { | |
| if (!fs.existsSync(CMD_HISTORY_FILE)) return res.json({ history: [] }); | |
| try { res.json({ history: JSON.parse(fs.readFileSync(CMD_HISTORY_FILE, "utf8")) }); } | |
| catch { res.json({ history: [] }); } | |
| }); | |
| app.get("/api/versions", (req, res) => { | |
| const { file } = req.query; | |
| const versions = readVersions(); | |
| res.json({ versions: file ? (versions[file] || []) : versions }); | |
| }); | |
| app.post("/api/rollback", (req, res) => { | |
| const { file, versionPath } = req.body; | |
| const src = path.join(HOME_DIR, versionPath); | |
| const dst = path.join(HOME_DIR, file); | |
| if (!src.startsWith(HOME_DIR) || !dst.startsWith(HOME_DIR)) return res.status(403).json({ error: "Forbidden" }); | |
| if (!fs.existsSync(src)) return res.status(404).json({ error: "Version not found" }); | |
| versionFile(file); | |
| fs.copyFileSync(src, dst); | |
| res.json({ success: true, content: fs.readFileSync(dst, "utf8") }); | |
| }); | |
| app.get("/api/models", (req, res) => { | |
| res.json({ models: Object.entries(MODELS).map(([k, v]) => ({ key: k, name: v.name, description: v.description })) }); | |
| }); | |
| app.post("/api/exec", (req, res) => { | |
| const { command } = req.body; | |
| if (!isSafeCmd(command)) { | |
| return res.json({ stdout: "", stderr: `Blocked: ${command.trim().split(/\s+/)[0]}`, code: 1, ok: false }); | |
| } | |
| exec(command, { cwd: HOME_DIR, timeout: 30000, shell: "/bin/bash" }, (err, stdout, stderr) => { | |
| const code = err ? (err.code || 1) : 0; | |
| res.json({ | |
| ...structureResult(command, stdout || "", stderr || "", code), | |
| code, | |
| }); | |
| }); | |
| }); | |
| // ββ MAIN CHAT ENDPOINT βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| app.post("/api/chat", async (req, res) => { | |
| res.setHeader("Content-Type", "text/event-stream"); | |
| res.setHeader("Cache-Control", "no-cache"); | |
| res.setHeader("Connection", "keep-alive"); | |
| const send = (type, data) => { | |
| try { res.write(`data: ${JSON.stringify({ type, ...data })}\n\n`); } catch {} | |
| }; | |
| const { message, currentFile, model: modelKey = "cryo2", transcript = [] } = req.body; | |
| try { | |
| // ββ Web search (no file context) βββββββββββββββββββββββββββββββββββββββ | |
| if (needsWebSearch(message) && !currentFile) { | |
| send("status", { text: "Searching the web..." }); | |
| send("web_search", { query: message }); | |
| try { | |
| const result = await webSearch(message); | |
| send("web_search_result", { query: message, citations: result.citations }); | |
| send("message", { text: result.answer }); | |
| } catch (e) { | |
| send("message", { text: "Web search failed: " + e.message }); | |
| } | |
| res.write("data: [DONE]\n\n"); | |
| res.end(); | |
| return; | |
| } | |
| let filePath = resolveFile(currentFile); | |
| const absPath = filePath ? path.join(HOME_DIR, filePath) : path.join(HOME_DIR, "index.js"); | |
| if (!filePath) filePath = "index.js"; | |
| let fileContent = fs.existsSync(absPath) ? fs.readFileSync(absPath, "utf8") : ""; | |
| const hasFile = fileContent.length > 0; | |
| const existingTasks = readTasks(); | |
| const pendingTasks = existingTasks.filter(t => !t.done); | |
| // ββ NO FILE: Create from scratch βββββββββββββββββββββββββββββββββββββββ | |
| if (!hasFile) { | |
| send("status", { text: "Planning what to build..." }); | |
| const createPrompt = `You are Cryo - a precise developer AI. Create the code the user requested. | |
| IRON LAW: Create ONLY what the request asks for. Nothing more, nothing less. | |
| Interpret the request LITERALLY. No bonus features. No unrequested "improvements". | |
| USER REQUEST: "${message}" | |
| Respond with ONLY a raw JSON object (no markdown, no prose around it): | |
| { | |
| "filename": "appropriate filename like bot.js or app.py", | |
| "description": "one line description of what this is", | |
| "tasks": [{"task": "what you built", "done": true}], | |
| "content": "COMPLETE SOURCE CODE HERE β this must be raw source code, NOT JSON. Use \\n for newlines.", | |
| "status": "Created - brief description" | |
| } | |
| CRITICAL RULES FOR THE "content" FIELD: | |
| - "content" must contain the raw source code of the file (JavaScript, Python, etc.) | |
| - "content" must NOT be a JSON object, array, or any JSON value β it is plain source code as a JSON string | |
| - "content" must be the complete, working file β every import, every function, ready to run | |
| - Use \\n for line breaks inside the string value | |
| - Escape all double-quotes inside the code as \\" | |
| - filename must have the correct extension (.js, .py, etc.) | |
| WRONG β content that is JSON: "content": {"code": "..."} | |
| WRONG β content that is the full response again: "content": "{\\"filename\\":\\"bot.js\\"..." | |
| RIGHT β content that is source code: "content": "const x = require('y');\\n\\nmodule.exports = x;"`; | |
| const aiRaw = await callAI(createPrompt, "", modelKey, transcript); | |
| let parsed = parseJSON(aiRaw); | |
| // Guard: if parsed.content looks like JSON (the AI put the wrapper inside content), | |
| // try to extract the nested "content" field, then fall back to markdown extraction. | |
| if (parsed?.content) { | |
| const c = parsed.content.trim(); | |
| // Detect if content is itself a JSON blob (starts with { or [) | |
| if (c.startsWith("{") || c.startsWith("[")) { | |
| const inner = parseJSON(c); | |
| if (inner?.content && typeof inner.content === "string" && !inner.content.trim().startsWith("{")) { | |
| // The real code was nested one level deeper | |
| parsed = { ...parsed, ...inner }; | |
| } else { | |
| // Can't recover from JSON-as-content β force fallback | |
| parsed.content = null; | |
| } | |
| } | |
| } | |
| // Fallback: extract code from markdown response or raw text | |
| if (!parsed?.content || parsed.content.trim().startsWith("{")) { | |
| const extracted = extractContentFromText(aiRaw || ""); | |
| const fnMatch = aiRaw.match(/(?:file|save|name)[^a-z]*?([a-z0-9_-]+\.[a-z]{2,4})/i); | |
| parsed = { | |
| filename: (parsed?.filename) || fnMatch?.[1] || "index.js", | |
| content: extracted || aiRaw, | |
| description: parsed?.description || "Generated code", | |
| tasks: parsed?.tasks || [{ task: "Generate file", done: true }], | |
| status: parsed?.status || "Created" | |
| }; | |
| } | |
| send("status", { text: `Plan ready β ${parsed.tasks?.length || 1} task(s)` }); | |
| send("task_update", { tasks: parsed.tasks || [] }); | |
| send("status", { text: "Checking generated code..." }); | |
| const tmpName = `__cryo_new_${Date.now()}.js`; | |
| const tmpPath = path.join(HOME_DIR, tmpName); | |
| fs.writeFileSync(tmpPath, parsed.content, "utf8"); | |
| // Only syntax-check JS files | |
| if (parsed.filename.endsWith(".js") || parsed.filename.endsWith(".ts")) { | |
| await syntaxFixLoop(tmpName, send, modelKey, 3, transcript); | |
| parsed.content = fs.readFileSync(tmpPath, "utf8"); | |
| } | |
| fs.unlinkSync(tmpPath); | |
| send("status", { text: `Saving ${parsed.filename}...` }); | |
| const savePath = path.join(HOME_DIR, parsed.filename); | |
| fs.mkdirSync(path.dirname(savePath), { recursive: true }); | |
| fs.writeFileSync(savePath, parsed.content, "utf8"); | |
| if (parsed.filename.endsWith(".js")) { | |
| const finalCheck = await runCmd(`node --check ${parsed.filename}`); | |
| send("command", { cmd: `node --check ${parsed.filename}`, index: 0 }); | |
| send("command_result", { | |
| cmd: `node --check ${parsed.filename}`, | |
| stdout: finalCheck.code === 0 ? "β Syntax OK β file saved!" : "", | |
| stderr: finalCheck.stderr, index: 0, isError: finalCheck.code !== 0 | |
| }); | |
| } | |
| updateTasks(parsed.tasks || []); | |
| updateMeta({ last_ai_edit: new Date().toISOString(), languages: [parsed.filename.split(".").pop()] }); | |
| saveState({ activeFile: parsed.filename }); | |
| send("file_created", { filename: parsed.filename, content: parsed.content, description: parsed.description }); | |
| send("message", { text: parsed.status || `Created \`${parsed.filename}\`` }); | |
| send("tree_update", {}); | |
| res.write("data: [DONE]\n\n"); | |
| res.end(); | |
| return; | |
| } | |
| // ββ FILE EXISTS: Iterative agent loop βββββββββββββββββββββββββββββββββ | |
| send("status", { text: "Analyzing..." }); | |
| const loopResult = await runAgentLoop({ | |
| filePath, fileContent, message, modelKey, send, maxRounds: 6, transcript | |
| }); | |
| const { allCommandResults, hadSedEdits, hadFullRewrite, plan } = loopResult; | |
| fileContent = loopResult.fileContent; | |
| const resultsText = allCommandResults | |
| .map(r => `$ ${r.cmd}\n${r.stdout.substring(0, 400)}${r.stderr ? `\nSTDERR: ${r.stderr.substring(0, 150)}` : ""}`) | |
| .join("\n\n"); | |
| send("status", { text: "Synthesizing..." }); | |
| // ββ QUERY: just answer βββββββββββββββββββββββββββββββββββββββββββββ | |
| if (plan.task_type === "query") { | |
| if (needsWebSearch(message)) { | |
| send("web_search", { query: message }); | |
| try { | |
| const wsResult = await webSearch(message); | |
| send("web_search_result", { query: message, citations: wsResult.citations }); | |
| send("message", { text: wsResult.answer }); | |
| } catch { | |
| const ansRaw = await callAI( | |
| `You are Cryo. Answer the question based on the file content and command results. Be concise and direct. Return ONLY JSON: {"answer":"your answer here"}`, | |
| `Question: "${message}"\nCommand results:\n${resultsText}\nFile context:\n${fileContent.substring(0, 1500)}`, | |
| modelKey, | |
| transcript | |
| ); | |
| const ans = parseJSON(ansRaw); | |
| send("message", { text: ans?.answer || ansRaw || "Could not generate answer." }); | |
| } | |
| } else { | |
| const ansRaw = await callAI( | |
| `You are Cryo. Answer the question using the command results and file content below. Return ONLY JSON: {"answer":"your answer"}`, | |
| `QUESTION: "${message}"\nCOMMAND RESULTS:\n${resultsText.substring(0, 1200)}\nFILE (${filePath}):\n${fileContent.substring(0, 1200)}`, | |
| modelKey, | |
| transcript | |
| ); | |
| const ans = parseJSON(ansRaw); | |
| send("message", { text: ans?.answer || ansRaw || "Could not generate answer." }); | |
| } | |
| } else { | |
| // ββ EDIT/CREATE: sed/tee was used β verify & summarize ββββββββββββββ | |
| if (hadSedEdits || loopResult.hadTeeWrites || hadFullRewrite) { | |
| send("status", { text: "Verifying edits..." }); | |
| if (filePath.endsWith(".js")) { | |
| await syntaxFixLoop(filePath, send, modelKey, 3, transcript); | |
| } | |
| fileContent = fs.existsSync(absPath) ? fs.readFileSync(absPath, "utf8") : fileContent; | |
| // ββ GROUND-TRUTH VERIFICATION ββββββββββββββββββββββββββββββββββββ | |
| // Ask the AI to check the ACTUAL file content against the request. | |
| // This is the only honest source β not the command log. | |
| const verifyRaw = await callAI( | |
| `You are a code reviewer applying the IRON LAW. | |
| The request was: "${message}" | |
| Check: (1) Is EXACTLY what was requested implemented? (2) Was anything NOT in the request changed? | |
| Return ONLY JSON (no markdown): | |
| { | |
| "implemented": true | false, | |
| "summary": "one honest sentence: what changed, or what is MISSING if not implemented", | |
| "missing": "describe what is still missing, or null if fully done", | |
| "extra_changes": "describe any changes made BEYOND the request, or null if clean" | |
| }`, | |
| `ORIGINAL REQUEST: "${message}" | |
| ACTUAL FILE CONTENT (this is the real file on disk β trust this, not the command log): | |
| ${fileContent.substring(0, 3500)}`, | |
| modelKey, | |
| transcript | |
| ); | |
| const verify = parseJSON(verifyRaw); | |
| const allTasks = [...existingTasks.map(t => ({ ...t, done: true })), ...(plan.tasks || []).map(t => ({ ...t, done: verify?.implemented !== false }))]; | |
| saveTasks(allTasks); | |
| send("task_update", { tasks: allTasks }); | |
| versionFile(filePath); | |
| updateMeta({ last_ai_edit: new Date().toISOString() }); | |
| send("file_updated", { filename: filePath, content: fileContent, description: verify?.summary || "Updated" }); | |
| if (verify?.implemented === false) { | |
| // Change did NOT land β report honestly, do not claim success | |
| send("message", { text: `β οΈ Edit incomplete: ${verify.summary}${verify.missing ? ` β still missing: ${verify.missing}` : ""}` }); | |
| } else { | |
| send("message", { text: verify?.summary || "Changes applied successfully." }); | |
| } | |
| } else { | |
| // ββ No sed edits were made β ask AI for complete file content βββ | |
| // Include full file context + command history so the AI has the same | |
| // "read the file first" context that the sheet mandates. | |
| const editRaw = await callAI( | |
| `You are Cryo. The shell-command agent ran but made no file edits. | |
| Now produce the COMPLETE updated file that fulfills the user's request. | |
| IRON LAW: Change ONLY what the request asks for. Preserve everything else EXACTLY. | |
| SELF-CHECK before writing: "Am I only changing what was requested?" | |
| RULES: | |
| - Preserve EVERYTHING not changed by the request β same structure, same order, same style | |
| - Output must be the complete working file | |
| - Apply ONLY what the request asked for β nothing more | |
| - Do NOT add features, fix other things, or reformat existing code | |
| - Return ONLY raw JSON (no markdown fences): | |
| {"content":"COMPLETE file here","description":"one line of what changed","status":"Done - brief summary"}`, | |
| `ORIGINAL REQUEST: "${message}" | |
| FILE: ${filePath} (${fileContent.split("\n").length} lines) | |
| COMMAND RESULTS:\n${resultsText.substring(0, 800)} | |
| CURRENT FILE CONTENT (line numbers for reference):\n${ | |
| fileContent.split("\n").slice(0, 300).map((l, i) => `${i + 1}\t${l}`).join("\n").substring(0, 3500) | |
| }\nProduce the complete updated file now.`, | |
| modelKey, | |
| transcript | |
| ); | |
| let editParsed = parseJSON(editRaw); | |
| // Fallback: extract code from markdown | |
| if (!editParsed?.content) { | |
| const extracted = extractContentFromText(editRaw || ""); | |
| editParsed = extracted | |
| ? { content: extracted, description: "Updated", status: "Done" } | |
| : { content: fileContent, description: "No changes made", status: "Done" }; | |
| } | |
| versionFile(filePath); | |
| // Pre-save syntax check | |
| send("status", { text: "Checking before saving..." }); | |
| const tmpName = `__cryo_presave_${Date.now()}.js`; | |
| const tmpPath = path.join(HOME_DIR, tmpName); | |
| fs.writeFileSync(tmpPath, editParsed.content, "utf8"); | |
| if (filePath.endsWith(".js")) { | |
| await syntaxFixLoop(tmpName, send, modelKey, 3, transcript); | |
| editParsed.content = fs.readFileSync(tmpPath, "utf8"); | |
| } | |
| fs.unlinkSync(tmpPath); | |
| fs.writeFileSync(absPath, editParsed.content, "utf8"); | |
| send("status", { text: `Saved ${filePath}` }); | |
| if (filePath.endsWith(".js")) { | |
| await syntaxFixLoop(filePath, send, modelKey, 3, transcript); | |
| } | |
| fileContent = fs.readFileSync(absPath, "utf8"); | |
| // Verify honestly against real file | |
| const verifyFbRaw = await callAI( | |
| `You are a code reviewer. Check if the user's request is implemented in the actual file. | |
| Return ONLY JSON: | |
| {"implemented": true | false, "summary": "one honest sentence", "missing": "what is missing or null"}`, | |
| `REQUEST: "${message}"\nACTUAL FILE (trust this):\n${fileContent.substring(0, 3500)}`, | |
| modelKey, | |
| transcript | |
| ); | |
| const verifyFb = parseJSON(verifyFbRaw); | |
| const allTasks = [ | |
| ...existingTasks.map(t => ({ ...t, done: true })), | |
| ...(plan.tasks || []).map(t => ({ ...t, done: verifyFb?.implemented !== false })) | |
| ]; | |
| saveTasks(allTasks); | |
| updateMeta({ last_ai_edit: new Date().toISOString() }); | |
| send("task_update", { tasks: allTasks }); | |
| send("file_updated", { filename: filePath, content: fileContent, description: verifyFb?.summary || editParsed.description }); | |
| if (verifyFb?.implemented === false) { | |
| send("message", { text: `β οΈ Edit incomplete: ${verifyFb.summary}${verifyFb.missing ? ` β still missing: ${verifyFb.missing}` : ""}` }); | |
| } else { | |
| send("message", { text: verifyFb?.summary || editParsed.status || editParsed.description }); | |
| } | |
| send("tree_update", {}); | |
| } | |
| } | |
| // ββ Pending tasks check ββββββββββββββββββββββββββββββββββββββββββββ | |
| const latestTasks = readTasks(); | |
| const stillPending = latestTasks.filter(t => !t.done); | |
| if (stillPending.length > 0) { | |
| send("pending_tasks", { tasks: stillPending, count: stillPending.length }); | |
| } | |
| res.write("data: [DONE]\n\n"); | |
| res.end(); | |
| } catch (err) { | |
| console.error("Chat error:", err); | |
| send("error", { text: err.message }); | |
| res.write("data: [DONE]\n\n"); | |
| res.end(); | |
| } | |
| }); | |
| app.listen(PORT, () => { | |
| console.log(`\nβοΈ Cryo Dev Server β http://localhost:${PORT}`); | |
| console.log(`π Home dir: ${HOME_DIR}\n`); | |
| }); |