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