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":"","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`); });