| 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"); |
|
|
| |
| 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"))); |
|
|
| |
| const LONGCAT_API_URL = "https://api.longcat.chat/anthropic/v1/messages"; |
| const LONGCAT_API_KEY = "ak_2ep6ba2Ww0pn5cH09U2Mq3Eo0ez1M"; |
| const LONGCAT_API_KEY_BACKUP = "ak_2k06yh7h44iH7g77T46kB3uJ6b89l"; |
|
|
| |
| 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" }, |
| }; |
|
|
| |
| |
| 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", |
| |
| ]; |
| const BLOCKED_CMDS = ["sudo","su","reboot","shutdown","mkfs","fdisk"]; |
| |
| |
| 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; |
| |
| 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 |
| `; |
|
|
| |
| |
| |
| const DESTRUCTIVE_PATTERNS = [ |
| /^rm\s+-rf?\s+\//, |
| /^>\s*\//, |
| /:(){ :|:& };:/, |
| ]; |
| function filterCommands(commands, alreadyRun = []) { |
| const safe = []; |
| const skipped = []; |
| const runSet = new Set(alreadyRun.map(c => (typeof c === "string" ? c.trim() : ""))); |
| |
| 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 }; |
| } |
|
|
| |
| |
| function structureResult(cmd, stdout, stderr, code) { |
| const lines = stdout.split("\n").filter(Boolean); |
| const isEdit = /^sed\s+-i/.test(cmd); |
| 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), |
| |
| ...(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, |
| }; |
| } |
|
|
| |
| const storage = multer.diskStorage({ |
| destination: HOME_DIR, |
| filename: (req, file, cb) => cb(null, file.originalname || "index.js"), |
| }); |
| const upload = multer({ storage }); |
|
|
| |
| |
| function parseJSON(raw) { |
| if (!raw) return null; |
| let s = String(raw).trim(); |
|
|
| |
| s = s.replace(/^```(?:json)?\s*/i, "").replace(/\s*```\s*$/, "").trim(); |
|
|
| |
| try { return JSON.parse(s); } catch {} |
|
|
| |
| 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 { |
| 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; |
| } |
|
|
| |
| |
| |
| 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; |
| |
| 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; |
| } |
|
|
|
|
| |
| function extractCommandsFromText(text) { |
| const cmds = []; |
| |
| 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); |
| } |
| |
| 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)); |
| } |
|
|
| |
| |
| function extractContentFromText(text) { |
| |
| const m = text.match(/```(?:js|javascript|typescript|ts|python|py|sh|bash)?\s*([\s\S]+?)```/i); |
| if (m) return m[1].trim(); |
| return null; |
| } |
|
|
| |
| 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 }); |
| }); |
| }); |
| } |
|
|
| |
| 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; |
| } |
|
|
| |
| |
| |
|
|
| |
| |
| 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(); |
|
|
| |
| 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), |
| exports: exportLines.slice(0, 5), |
| functions: fnLines.slice(0, 30), |
| classes: classLines.slice(0, 10), |
| sections: commentSections.slice(0, 20), |
| 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"), |
| }; |
| } |
|
|
| |
| |
| 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"), |
| }; |
| } |
|
|
| |
| 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) }; |
| } |
|
|
| |
| |
| 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; |
| } |
|
|
| |
| 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"); |
| } |
|
|
| |
| 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; |
| } |
|
|
| |
| 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; |
| } |
|
|
| |
| 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"); |
| } |
|
|
| |
| 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; |
| } |
|
|
| |
| 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" }; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| async function callAI(system, query, _modelKey, history = []) { |
| const userMessage = query ? `${system}\n\n${query}` : system; |
|
|
| |
| const safeHistory = (Array.isArray(history) ? history : []) |
| .filter(h => h && (h.role === "user" || h.role === "assistant") && h.content) |
| .slice(-20) |
| .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, |
| }; |
|
|
| |
| 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", |
| }, |
| }); |
| |
| 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) { |
| |
| const status = err.response?.status; |
| const errCode = err.response?.data?.error?.code; |
| if (status === 429 && errCode === "rate_limit_exceeded") { |
| |
| return await attempt(LONGCAT_API_KEY_BACKUP); |
| } |
| throw err; |
| } |
| } |
|
|
| |
| 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; |
| } |
|
|
| |
| |
| |
| 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 { |
| |
| const extracted = extractContentFromText(fixedRaw || ""); |
| if (extracted) { |
| fs.writeFileSync(absPath, extracted, "utf8"); |
| send("status", { text: "Extracted fix from response" }); |
| } else break; |
| } |
| } |
| return false; |
| } |
|
|
|
|
| |
| 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: [] }; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| async function runAgentLoop(opts) { |
| const { filePath, fileContent: initialContent, message, modelKey, send, maxRounds = 10, transcript = [] } = opts; |
| |
| 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 = []; |
| let toolTurnCount = 0; |
| |
| |
| const COMPACTION_THRESHOLD = 40; |
|
|
| |
| |
| const cmdRetryCount = {}; |
| async function runBatch(commands) { |
| |
| 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); |
|
|
| |
| 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) { |
| |
| 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; |
| } |
|
|
| |
| 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() }); |
|
|
| |
| 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, |
| }); |
|
|
| |
| |
| |
| 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" |
| }); |
| } |
|
|
| |
| 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; |
|
|
| } |
| } |
|
|
| |
| |
| |
| |
| |
| function buildThreadContext(round) { |
| const fileLines = fileContent.split("\n").length; |
|
|
| |
| const lines = fileContent.split("\n"); |
| const smartLines = getSmartContext(fileContent, message).split("\n"); |
| |
| const numberedCtx = (() => { |
| |
| 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"); |
| |
| 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); |
| })(); |
|
|
| |
| |
| |
| 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; |
| } |
|
|
| |
| |
| 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"); |
|
|
| |
| 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 ===`; |
| } |
|
|
| |
| send("status", { text: "Analyzing request..." }); |
|
|
| const fileLines = initialContent.split("\n").length; |
|
|
| |
| 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); |
|
|
| |
| |
| 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 }); |
|
|
| |
| 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 || []); |
|
|
| |
| |
| |
| |
| |
| for (let round = 2; round <= MAX_TOOL_TURNS * 3; round++) { |
| |
| if (toolTurnCount >= MAX_TOOL_TURNS) { |
| send("status", { text: `Tool turn budget reached (${toolTurnCount}/${MAX_TOOL_TURNS}) β stopping` }); |
| break; |
| } |
| |
| 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" }; |
| } |
|
|
| |
| |
| |
| 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; |
|
|
| |
| 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++, |
| }); |
| } |
|
|
| |
| 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` }); |
| |
| if (cont.commands && cont.commands.length > 0) await runBatch(cont.commands); |
| continue; |
| } |
|
|
| |
| 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"); |
|
|
| |
| 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); |
| 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" }; |
| } |
|
|
| |
| if (cont.tasks && cont.tasks.length > 0) { |
| sessionTasks = cont.tasks; |
| send("task_update", { tasks: sessionTasks }); |
| } |
|
|
| if (cont.done || !cont.commands || cont.commands.length === 0) { |
| |
| |
| |
| const hasNoToolCalls = (!cont.commands || cont.commands.length === 0) && |
| (!cont.read_file || cont.read_file.length === 0); |
| if (hasNoToolCalls) { |
| |
| 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; |
| } |
|
|
| |
| const stillPending = sessionTasks.filter(t => !t.done); |
|
|
| |
| |
| |
| |
| 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) { |
| |
| send("status", { text: `Verification failed: ${check.reason} β retrying...` }); |
| |
| 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; |
| } |
| |
| 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); |
| } |
|
|
| |
| fileContent = fs.existsSync(absPath) ? fs.readFileSync(absPath, "utf8") : fileContent; |
| appendCmdHistory(historyEntries); |
|
|
| return { |
| allCommandResults, |
| fileContent, |
| plan, |
| sessionTasks, |
| toolTurnCount, |
| |
| |
| |
| hadSedEdits: allCommandResults.some(r => |
| r.cmd.startsWith("sed -i") && r.code === 0 && |
| (r.structured?.linesChanged === "applied" || !r.stderr) |
| ), |
| hadTeeWrites: false, |
| hadFullRewrite: allCommandResults.some(r => r.cmd.startsWith("[full_rewrite]") && r.code === 0), |
| }; |
| } |
|
|
| |
| 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) }); |
| }); |
|
|
| |
| 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, |
| }); |
| }); |
| }); |
|
|
| |
| 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 { |
| |
| 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); |
|
|
| |
| 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); |
|
|
| |
| |
| if (parsed?.content) { |
| const c = parsed.content.trim(); |
| |
| if (c.startsWith("{") || c.startsWith("[")) { |
| const inner = parseJSON(c); |
| if (inner?.content && typeof inner.content === "string" && !inner.content.trim().startsWith("{")) { |
| |
| parsed = { ...parsed, ...inner }; |
| } else { |
| |
| parsed.content = null; |
| } |
| } |
| } |
|
|
| |
| 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"); |
|
|
| |
| 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; |
| } |
|
|
| |
| 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..." }); |
|
|
| |
| 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 { |
| |
| 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; |
|
|
| |
| |
| |
| 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) { |
| |
| send("message", { text: `β οΈ Edit incomplete: ${verify.summary}${verify.missing ? ` β still missing: ${verify.missing}` : ""}` }); |
| } else { |
| send("message", { text: verify?.summary || "Changes applied successfully." }); |
| } |
|
|
| } else { |
| |
| |
| |
| 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); |
|
|
| |
| 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); |
|
|
| |
| 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"); |
|
|
| |
| 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", {}); |
| } |
| } |
|
|
| |
| 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`); |
| }); |