Update server.js
Browse files
server.js
CHANGED
|
@@ -51,9 +51,10 @@ app.use(express.static(path.join(__dirname, "public")));
|
|
| 51 |
|
| 52 |
// ββ SINGLE MODEL: LongCat API (Anthropic format) ββββββββββββββββββββββββββββ
|
| 53 |
const LONGCAT_API_URL = "https://api.longcat.chat/anthropic/v1/messages";
|
| 54 |
-
const LONGCAT_API_KEY
|
|
|
|
| 55 |
|
| 56 |
-
// All model keys from the UI
|
| 57 |
const MODELS = {
|
| 58 |
"cryo1": { name: "Cryo 1", label: "cryo1", description: "Fast & light" },
|
| 59 |
"cryo2": { name: "Cryo 2", label: "cryo2", description: "Reliable & smart" },
|
|
@@ -78,9 +79,8 @@ const COMMANDS_REFERENCE = `
|
|
| 78 |
=== SHELL COMMANDS (cwd = /home) β ALL commands permitted except: sudo su reboot shutdown mkfs fdisk ===
|
| 79 |
|
| 80 |
EXPLORE: wc -l file | ls -la | find . -name "*.js" | stat filename | which node | env
|
| 81 |
-
READ: sed -n 'X,Yp' file β
|
| 82 |
-
grep -n "pattern" file β
|
| 83 |
-
grep -C3 "pattern" file β FIND with 3 lines of context
|
| 84 |
head -n N file | tail -n N file | cat file
|
| 85 |
EDIT: sed -i 's/old/new/g' file β replace all occurrences
|
| 86 |
sed -i 'Ns/old/new/' file β replace on line N only
|
|
@@ -92,24 +92,111 @@ RUN: node file.js | node --check file.js | npm install pkg | python3 file.
|
|
| 92 |
NETWORK: curl -s URL | wget -q -O out URL
|
| 93 |
UTILS: jq . file.json | wc -l file | diff a b
|
| 94 |
|
| 95 |
-
===
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
|
| 103 |
=== STRICT RULES ===
|
| 104 |
-
1. ALWAYS
|
| 105 |
-
2. ALWAYS
|
| 106 |
-
3. ALWAYS cp file.bak before
|
| 107 |
-
4. ALWAYS node --check after every JS
|
| 108 |
5. NEVER echo -e for multiline β always tee with heredoc
|
| 109 |
-
6. Mark done:true ONLY after
|
| 110 |
-
7. If
|
|
|
|
|
|
|
| 111 |
`;
|
| 112 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
// ββ Multer βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 114 |
const storage = multer.diskStorage({
|
| 115 |
destination: HOME_DIR,
|
|
@@ -187,7 +274,8 @@ function runCmd(command) {
|
|
| 187 |
return resolve({ stdout: "", stderr: `Blocked: ${command.trim().split(/\s+/)[0]}`, code: 1 });
|
| 188 |
}
|
| 189 |
exec(command, { cwd: HOME_DIR, timeout: 30000, shell: "/bin/bash" }, (err, stdout, stderr) => {
|
| 190 |
-
|
|
|
|
| 191 |
});
|
| 192 |
});
|
| 193 |
}
|
|
@@ -213,6 +301,120 @@ function getSmartContext(content, query, maxLines = 120) {
|
|
| 213 |
return head + middle + tail;
|
| 214 |
}
|
| 215 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 216 |
// ββ VERSIONING βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 217 |
function readVersions() {
|
| 218 |
if (!fs.existsSync(VERSIONS_FILE)) return {};
|
|
@@ -296,8 +498,11 @@ async function webSearch(query) {
|
|
| 296 |
// ββ callAI β LongCat API (Anthropic format) ββββββββββββββββββββββββββββββββ
|
| 297 |
// system = role/task definition + original user request for context anchoring
|
| 298 |
// query = specific data for this call (file snippet, cmd results, etc.)
|
| 299 |
-
// _modelKey accepted but ignored β all calls route to LongCat-Flash-
|
| 300 |
// history = [{role:"user"|"assistant", content:"..."}] β conversation transcript
|
|
|
|
|
|
|
|
|
|
| 301 |
async function callAI(system, query, _modelKey, history = []) {
|
| 302 |
const userMessage = query ? `${system}\n\n${query}` : system;
|
| 303 |
|
|
@@ -305,36 +510,50 @@ async function callAI(system, query, _modelKey, history = []) {
|
|
| 305 |
const safeHistory = (Array.isArray(history) ? history : [])
|
| 306 |
.filter(h => h && (h.role === "user" || h.role === "assistant") && h.content)
|
| 307 |
.slice(-20) // last 10 user+assistant pairs
|
| 308 |
-
.map(h => ({ role: h.role, content: String(h.content).slice(0, 800) }));
|
| 309 |
|
| 310 |
-
// Build messages array: inject history then the new user message
|
| 311 |
const messages = [
|
| 312 |
...safeHistory,
|
| 313 |
{ role: "user", content: userMessage.slice(0, 5500) },
|
| 314 |
];
|
| 315 |
|
| 316 |
const payload = {
|
| 317 |
-
model: "LongCat-Flash-
|
| 318 |
max_tokens: 2000,
|
| 319 |
system: "You are Cryo, a precise developer AI. Respond with valid JSON only β no markdown fences, no prose. The original user request is embedded in the message; never lose sight of it.",
|
| 320 |
messages,
|
| 321 |
};
|
| 322 |
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 331 |
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 336 |
}
|
| 337 |
-
return data.reply || data.text || data.message || "";
|
| 338 |
}
|
| 339 |
|
| 340 |
// ββ SYNTAX FIX LOOP ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -404,22 +623,49 @@ async function runAgentLoop(opts) {
|
|
| 404 |
let cmdIndex = 0;
|
| 405 |
let fileContent = initialContent;
|
| 406 |
let sessionTasks = [];
|
|
|
|
| 407 |
|
| 408 |
// ββ Helper: run a batch of commands and collect results βββββββββββββββ
|
|
|
|
|
|
|
| 409 |
async function runBatch(commands) {
|
| 410 |
-
|
| 411 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 412 |
const idx = cmdIndex++;
|
| 413 |
send("status", { text: `[${idx + 1}] ${cmd}` });
|
| 414 |
send("command", { cmd, index: idx });
|
| 415 |
const result = await runCmd(cmd);
|
| 416 |
-
|
|
|
|
| 417 |
historyEntries.push({ cmd, stdout: result.stdout.slice(0, 500), stderr: result.stderr.slice(0, 200), code: result.code, ts: Date.now() });
|
|
|
|
|
|
|
| 418 |
send("command_result", {
|
| 419 |
-
cmd,
|
| 420 |
-
|
| 421 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 422 |
});
|
|
|
|
| 423 |
// Auto-fix syntax errors in JS files
|
| 424 |
if (cmd.includes("node --check") && result.code !== 0 && result.stderr) {
|
| 425 |
const target = cmd.trim().split(/\s+/).pop().replace(/\s*\(.*\)$/, "");
|
|
@@ -467,13 +713,19 @@ async function runAgentLoop(opts) {
|
|
| 467 |
return (headBlock + middleBlock + "\n" + tailBlock).substring(0, 2200);
|
| 468 |
})();
|
| 469 |
|
| 470 |
-
// Full command history with
|
| 471 |
-
//
|
| 472 |
const cmdLog = allCommandResults.slice(-14).map((r, i) => {
|
| 473 |
-
const
|
| 474 |
-
const err = r.stderr.trim().slice(0, 200);
|
| 475 |
const exitLabel = r.code === 0 ? "β" : `β(${r.code})`;
|
| 476 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 477 |
}).join("\n");
|
| 478 |
|
| 479 |
// Highlight any commands that failed β the AI must not repeat them blindly
|
|
@@ -500,8 +752,11 @@ PENDING (${pending.length} remaining):
|
|
| 500 |
${pending.length > 0 ? pending.map((t, i) => ` ${i + 1}. ${t.task}`).join("\n") : " ALL TASKS DONE"}
|
| 501 |
|
| 502 |
COMMAND HISTORY (${allCommandResults.length} total β includes grep results, sed outputs, check results):
|
| 503 |
-
${cmdLog || " (none yet β start with
|
| 504 |
-
|
|
|
|
|
|
|
|
|
|
| 505 |
FILE CONTENT WITH LINE NUMBERS (use these line numbers in sed -i 'Ns/...' commands):
|
| 506 |
${numberedCtx}
|
| 507 |
=== END CONTEXT ===`;
|
|
@@ -521,40 +776,45 @@ ${numberedCtx}
|
|
| 521 |
.map((l, i) => `${initTailStart + i + 1}\t${l}`).join("\n");
|
| 522 |
const initCtx = (initHeadBlock + `\n...[lines 41β${initTailStart} omitted β use grep -n to find sections]...\n` + initTailBlock).substring(0, 2200);
|
| 523 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 524 |
const planPrompt = `You are Cryo - a developer AI agent. You edit files using shell commands.
|
| 525 |
|
| 526 |
${COMMANDS_REFERENCE}
|
| 527 |
|
|
|
|
| 528 |
FILE: ${filePath} | TOTAL LINES: ${fileLines}
|
| 529 |
-
FILE CONTENT (with line numbers
|
| 530 |
${initCtx}
|
| 531 |
|
| 532 |
USER REQUEST: "${message}"
|
| 533 |
|
| 534 |
β Complete ONLY what was requested. Do not add unrequested features.
|
|
|
|
|
|
|
|
|
|
| 535 |
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
2. grep -n "keyword" ${filePath} β locate the exact lines to change
|
| 539 |
-
3. sed -n 'START,ENDp' ${filePath} β read that section to see exact text
|
| 540 |
-
4. cp ${filePath} ${filePath}.bak β backup before editing
|
| 541 |
-
5. sed -i 'Ns/exact-old-text/new-text/' ${filePath} β make the targeted edit
|
| 542 |
-
6. node --check ${filePath} β verify syntax (if JS)
|
| 543 |
|
| 544 |
Respond with ONLY raw JSON:
|
| 545 |
{
|
| 546 |
"task_type": "query" | "edit" | "create",
|
| 547 |
"status": "brief status for the user",
|
| 548 |
"tasks": [{"task": "specific subtask", "done": false}],
|
| 549 |
-
"
|
| 550 |
-
"
|
|
|
|
| 551 |
"done": false
|
| 552 |
}`;
|
| 553 |
|
| 554 |
const planRaw = await callAI(planPrompt, "", modelKey, transcript);
|
| 555 |
let plan = parseJSON(planRaw);
|
| 556 |
|
| 557 |
-
if (!plan || !plan.commands) {
|
| 558 |
const extracted = extractCommandsFromText(planRaw || "");
|
| 559 |
plan = {
|
| 560 |
task_type: "edit",
|
|
@@ -570,6 +830,17 @@ Respond with ONLY raw JSON:
|
|
| 570 |
send("status", { text: plan.status || "Processing..." });
|
| 571 |
send("task_update", { tasks: sessionTasks });
|
| 572 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 573 |
await runBatch(plan.commands || []);
|
| 574 |
|
| 575 |
// ββ ROUNDS 2βN: Continue until all tasks done βββββββββββββββββββββββββ
|
|
@@ -589,6 +860,7 @@ ${COMMANDS_REFERENCE}
|
|
| 589 |
β YOUR ONLY PURPOSE: complete the ORIGINAL REQUEST shown at the top.
|
| 590 |
β READ THE COMMAND HISTORY β it shows what grep found, what lines exist, what failed.
|
| 591 |
β USE THE LINE NUMBERS in the file content to write precise sed -i 'Ns/...' commands.
|
|
|
|
| 592 |
|
| 593 |
DECISION TREE THIS ROUND:
|
| 594 |
- If you haven't grepped yet β grep -n first to locate lines
|
|
@@ -596,9 +868,12 @@ DECISION TREE THIS ROUND:
|
|
| 596 |
- If you have the exact text β sed -i 'Ns/old/new/' to make the targeted change
|
| 597 |
- If you just edited β sed -n 'START,ENDp' to verify the change is in the file
|
| 598 |
- If JS file was written β node --check to verify syntax
|
|
|
|
| 599 |
- If all tasks verified in file β set done:true
|
| 600 |
|
| 601 |
-
Respond with ONLY raw JSON:
|
|
|
|
|
|
|
| 602 |
{
|
| 603 |
"done": true | false,
|
| 604 |
"status": "what you are doing",
|
|
@@ -607,7 +882,17 @@ Respond with ONLY raw JSON:
|
|
| 607 |
"reasoning": "what the grep/sed output told you and what you are doing next"
|
| 608 |
}
|
| 609 |
|
| 610 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 611 |
|
| 612 |
const contRaw = await callAI(continuePrompt, "", modelKey, transcript);
|
| 613 |
let cont = parseJSON(contRaw);
|
|
@@ -618,6 +903,84 @@ CRITICAL: done:true only when sed -n CONFIRMS the change is physically in the fi
|
|
| 618 |
cont = { done: false, commands: extracted, tasks: sessionTasks, status: "Continuing...", reasoning: "Extracted" };
|
| 619 |
}
|
| 620 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 621 |
// Update task list from AI's assessment
|
| 622 |
if (cont.tasks && cont.tasks.length > 0) {
|
| 623 |
sessionTasks = cont.tasks;
|
|
@@ -651,10 +1014,21 @@ CRITICAL: done:true only when sed -n CONFIRMS the change is physically in the fi
|
|
| 651 |
plan,
|
| 652 |
sessionTasks,
|
| 653 |
hadSedEdits: allCommandResults.some(r => r.cmd.startsWith("sed -i") && r.code === 0),
|
| 654 |
-
hadTeeWrites: allCommandResults.some(r => (r.cmd.startsWith("tee ") || r.cmd.includes(" > ")) && r.code === 0)
|
|
|
|
| 655 |
};
|
| 656 |
}
|
| 657 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 658 |
// ββ FILE TREE ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 659 |
app.get("/api/tree", (req, res) => {
|
| 660 |
const buildTree = (dir, base = HOME_DIR) => {
|
|
@@ -743,10 +1117,14 @@ app.get("/api/models", (req, res) => {
|
|
| 743 |
app.post("/api/exec", (req, res) => {
|
| 744 |
const { command } = req.body;
|
| 745 |
if (!isSafeCmd(command)) {
|
| 746 |
-
return res.json({ stdout: "", stderr: `Blocked: ${command.trim().split(/\s+/)[0]}`, code: 1 });
|
| 747 |
}
|
| 748 |
exec(command, { cwd: HOME_DIR, timeout: 30000, shell: "/bin/bash" }, (err, stdout, stderr) => {
|
| 749 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 750 |
});
|
| 751 |
});
|
| 752 |
|
|
@@ -874,7 +1252,7 @@ RULES:
|
|
| 874 |
filePath, fileContent, message, modelKey, send, maxRounds: 6, transcript
|
| 875 |
});
|
| 876 |
|
| 877 |
-
const { allCommandResults, hadSedEdits, plan } = loopResult;
|
| 878 |
fileContent = loopResult.fileContent;
|
| 879 |
|
| 880 |
const resultsText = allCommandResults
|
|
@@ -914,7 +1292,7 @@ RULES:
|
|
| 914 |
|
| 915 |
} else {
|
| 916 |
// ββ EDIT/CREATE: sed/tee was used β verify & summarize βββββοΏ½οΏ½ββββββββ
|
| 917 |
-
if (hadSedEdits ||
|
| 918 |
send("status", { text: "Verifying edits..." });
|
| 919 |
if (filePath.endsWith(".js")) {
|
| 920 |
await syntaxFixLoop(filePath, send, modelKey, 3, transcript);
|
|
|
|
| 51 |
|
| 52 |
// ββ SINGLE MODEL: LongCat API (Anthropic format) ββββββββββββββββββββββββββββ
|
| 53 |
const LONGCAT_API_URL = "https://api.longcat.chat/anthropic/v1/messages";
|
| 54 |
+
const LONGCAT_API_KEY = "ak_2ep6ba2Ww0pn5cH09U2Mq3Eo0ez1M";
|
| 55 |
+
const LONGCAT_API_KEY_BACKUP = "ak_2k06yh7h44iH7g77T46kB3uJ6b89l";
|
| 56 |
|
| 57 |
+
// All model keys from the UI map to LongCat-Flash-Lite β one API, no confusion
|
| 58 |
const MODELS = {
|
| 59 |
"cryo1": { name: "Cryo 1", label: "cryo1", description: "Fast & light" },
|
| 60 |
"cryo2": { name: "Cryo 2", label: "cryo2", description: "Reliable & smart" },
|
|
|
|
| 79 |
=== SHELL COMMANDS (cwd = /home) β ALL commands permitted except: sudo su reboot shutdown mkfs fdisk ===
|
| 80 |
|
| 81 |
EXPLORE: wc -l file | ls -la | find . -name "*.js" | stat filename | which node | env
|
| 82 |
+
READ: sed -n 'X,Yp' file β read a section by line range
|
| 83 |
+
grep -n "pattern" file β find with line numbers
|
|
|
|
| 84 |
head -n N file | tail -n N file | cat file
|
| 85 |
EDIT: sed -i 's/old/new/g' file β replace all occurrences
|
| 86 |
sed -i 'Ns/old/new/' file β replace on line N only
|
|
|
|
| 92 |
NETWORK: curl -s URL | wget -q -O out URL
|
| 93 |
UTILS: jq . file.json | wc -l file | diff a b
|
| 94 |
|
| 95 |
+
=== SMART FILE READING β USE THIS BEFORE EDITING (same strategy Claude uses) ===
|
| 96 |
+
Instead of raw commands, you can request structured file reads via "read_file" in your JSON.
|
| 97 |
+
The system reads the file server-side and returns results in your next round's context.
|
| 98 |
+
|
| 99 |
+
ACTIONS:
|
| 100 |
+
{ "action": "map" }
|
| 101 |
+
β Returns: file structure overview β total lines, all section headers, all function/route
|
| 102 |
+
signatures with line numbers, imports list, first 30 lines, last 15 lines.
|
| 103 |
+
USE THIS FIRST on any file you haven't fully seen yet.
|
| 104 |
+
|
| 105 |
+
{ "action": "chunk", "start": N, "end": M }
|
| 106 |
+
β Returns: lines NβM with line numbers (like sed -n 'N,Mp').
|
| 107 |
+
USE THIS to read any section before editing it.
|
| 108 |
+
|
| 109 |
+
{ "action": "search", "pattern": "someSymbol" }
|
| 110 |
+
β Returns: all lines matching the pattern with line numbers (like grep -n).
|
| 111 |
+
USE THIS to locate all uses of a symbol before changing it.
|
| 112 |
+
|
| 113 |
+
HOW TO USE: Add a "read_file" array to your JSON response (alongside or instead of "commands"):
|
| 114 |
+
{
|
| 115 |
+
"read_file": [
|
| 116 |
+
{ "action": "map" },
|
| 117 |
+
{ "action": "search", "pattern": "bot.on" },
|
| 118 |
+
{ "action": "chunk", "start": 45, "end": 90 }
|
| 119 |
+
],
|
| 120 |
+
"commands": [],
|
| 121 |
+
"done": false,
|
| 122 |
+
"status": "Reading file structure before editing"
|
| 123 |
+
}
|
| 124 |
+
The results will appear in your NEXT round's context under FILE READ RESULTS.
|
| 125 |
+
You can combine read_file and commands in the same response.
|
| 126 |
+
|
| 127 |
+
=== WHEN TO USE FULL-FILE REWRITE vs SURGICAL EDITS ===
|
| 128 |
+
SURGICAL EDITS (sed): use when changing β€ 30% of the file β targeted, safe, precise.
|
| 129 |
+
FULL REWRITE: use when switching libraries/frameworks, restructuring >30%, or file is broken.
|
| 130 |
+
Set "full_rewrite": true and provide "new_content": "<complete file>".
|
| 131 |
+
System will stage β validate β write atomically. DO NOT use tee for this.
|
| 132 |
+
|
| 133 |
+
=== MANDATORY WORKFLOW ===
|
| 134 |
+
STEP 1 β READ MAP: { "read_file": [{ "action": "map" }] } β understand structure first
|
| 135 |
+
STEP 2 β SEARCH: { "read_file": [{ "action": "search", "pattern": "target" }] }
|
| 136 |
+
STEP 3 β CHUNK: { "read_file": [{ "action": "chunk", "start": N, "end": M }] }
|
| 137 |
+
STEP 4 β BACKUP: cp file file.bak
|
| 138 |
+
STEP 5 β EDIT: sed -i 'Ns/exact-old/new/' file (use line numbers from step 2)
|
| 139 |
+
STEP 6 β VERIFY: { "read_file": [{ "action": "chunk", "start": N, "end": M }] }
|
| 140 |
+
STEP 7 β CHECK: node --check file.js
|
| 141 |
|
| 142 |
=== STRICT RULES ===
|
| 143 |
+
1. ALWAYS map or search before editing β NEVER edit blind
|
| 144 |
+
2. ALWAYS chunk-read a section BEFORE editing β verify the exact text
|
| 145 |
+
3. ALWAYS cp file.bak before any rewrite
|
| 146 |
+
4. ALWAYS node --check after every JS change
|
| 147 |
5. NEVER echo -e for multiline β always tee with heredoc
|
| 148 |
+
6. Mark done:true ONLY after a chunk read CONFIRMS the change is in the file
|
| 149 |
+
7. If sed produced linesChanged:no-match β search again, pattern was wrong
|
| 150 |
+
8. NEVER repeat a command already in COMMAND HISTORY with the same args
|
| 151 |
+
9. If sed failed twice on same pattern β use full_rewrite
|
| 152 |
`;
|
| 153 |
|
| 154 |
+
// ββ COMMAND DEDUP & SAFETY FILTER βββββββββββββββββββββββββββββββββββββββββ
|
| 155 |
+
// Strips commands already run this session (exact match) and known-dangerous patterns.
|
| 156 |
+
// Returns { safe: [...], skipped: [...] }
|
| 157 |
+
const DESTRUCTIVE_PATTERNS = [
|
| 158 |
+
/^rm\s+-rf?\s+\//, // rm -rf /
|
| 159 |
+
/^>\s*\//, // redirect to root path
|
| 160 |
+
/:(){ :|:& };:/, // fork bomb
|
| 161 |
+
];
|
| 162 |
+
function filterCommands(commands, alreadyRun = []) {
|
| 163 |
+
const safe = [];
|
| 164 |
+
const skipped = [];
|
| 165 |
+
const runSet = new Set(alreadyRun.map(c => c.trim()));
|
| 166 |
+
for (const cmd of commands) {
|
| 167 |
+
const trimmed = cmd.trim();
|
| 168 |
+
if (!trimmed) continue;
|
| 169 |
+
if (runSet.has(trimmed)) { skipped.push({ cmd: trimmed, reason: "duplicate" }); continue; }
|
| 170 |
+
if (!isSafeCmd(trimmed)) { skipped.push({ cmd: trimmed, reason: "blocked" }); continue; }
|
| 171 |
+
if (DESTRUCTIVE_PATTERNS.some(p => p.test(trimmed))) { skipped.push({ cmd: trimmed, reason: "destructive" }); continue; }
|
| 172 |
+
safe.push(trimmed);
|
| 173 |
+
}
|
| 174 |
+
return { safe, skipped };
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
// ββ STRUCTURED COMMAND RESULT ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 178 |
+
// Wraps raw stdout/stderr into a structured object the AI can reason about
|
| 179 |
+
function structureResult(cmd, stdout, stderr, code) {
|
| 180 |
+
const lines = stdout.split("\n").filter(Boolean);
|
| 181 |
+
const isEdit = /^sed\s+-i/.test(cmd);
|
| 182 |
+
const isGrep = /^grep/.test(cmd);
|
| 183 |
+
const isRead = /^sed\s+-n/.test(cmd) || /^(head|tail|cat)\b/.test(cmd);
|
| 184 |
+
const isCheck = cmd.includes("node --check") || cmd.includes("python3 -m py_compile");
|
| 185 |
+
return {
|
| 186 |
+
cmd,
|
| 187 |
+
exit: code,
|
| 188 |
+
ok: code === 0,
|
| 189 |
+
stdout: stdout.substring(0, 1200),
|
| 190 |
+
stderr: stderr.substring(0, 400),
|
| 191 |
+
// Structured fields for common operations
|
| 192 |
+
...(isGrep && { matches: lines.slice(0, 30) }),
|
| 193 |
+
...(isEdit && { linesChanged: code === 0 ? "applied" : "no-match" }),
|
| 194 |
+
...(isRead && { content: stdout.substring(0, 2000) }),
|
| 195 |
+
...(isCheck && { syntaxOk: code === 0, syntaxError: stderr || null }),
|
| 196 |
+
warning: code !== 0 ? (stderr || "non-zero exit") : null,
|
| 197 |
+
};
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
// ββ Multer βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 201 |
const storage = multer.diskStorage({
|
| 202 |
destination: HOME_DIR,
|
|
|
|
| 274 |
return resolve({ stdout: "", stderr: `Blocked: ${command.trim().split(/\s+/)[0]}`, code: 1 });
|
| 275 |
}
|
| 276 |
exec(command, { cwd: HOME_DIR, timeout: 30000, shell: "/bin/bash" }, (err, stdout, stderr) => {
|
| 277 |
+
const code = err ? (err.code || 1) : 0;
|
| 278 |
+
resolve({ stdout: stdout || "", stderr: stderr || "", code });
|
| 279 |
});
|
| 280 |
});
|
| 281 |
}
|
|
|
|
| 301 |
return head + middle + tail;
|
| 302 |
}
|
| 303 |
|
| 304 |
+
// ββ FILE INSPECTION β Claude-style chunked reading βββββββββββββββββββββββββ
|
| 305 |
+
// Mirrors exactly how Claude reads files: overview first, then targeted chunks.
|
| 306 |
+
// The AI requests inspections; the system resolves them and feeds results back.
|
| 307 |
+
|
| 308 |
+
// 1. Build a file map: line count, imports, exports, top-level symbols, section headers.
|
| 309 |
+
// This is the "wc -l + grep structure" step Claude does first.
|
| 310 |
+
function buildFileMap(filePath) {
|
| 311 |
+
const abs = path.join(HOME_DIR, filePath);
|
| 312 |
+
if (!fs.existsSync(abs)) return null;
|
| 313 |
+
const content = fs.readFileSync(abs, "utf8");
|
| 314 |
+
const lines = content.split("\n");
|
| 315 |
+
const ext = path.extname(filePath).toLowerCase();
|
| 316 |
+
|
| 317 |
+
// Find structural landmarks by line number
|
| 318 |
+
const landmarks = [];
|
| 319 |
+
const importLines = [], exportLines = [], fnLines = [], classLines = [], commentSections = [];
|
| 320 |
+
|
| 321 |
+
lines.forEach((line, i) => {
|
| 322 |
+
const n = i + 1;
|
| 323 |
+
const t = line.trim();
|
| 324 |
+
if (/^(import|require|from)\b/.test(t) || /\brequire\s*\(/.test(t)) importLines.push(n);
|
| 325 |
+
if (/^(export|module\.exports)/.test(t)) exportLines.push(n);
|
| 326 |
+
if (/^(async\s+)?function\s+\w+|const\s+\w+\s*=\s*(async\s+)?\(/.test(t) ||
|
| 327 |
+
/^(app|router)\.(get|post|put|delete|patch|use)\s*\(/.test(t)) fnLines.push({ n, preview: t.slice(0, 80) });
|
| 328 |
+
if (/^class\s+\w+/.test(t)) classLines.push({ n, preview: t.slice(0, 60) });
|
| 329 |
+
if (/^\/\/\s*[ββ=]{3,}/.test(t) || /^#{1,3}\s/.test(t)) commentSections.push({ n, text: t.slice(0, 60) });
|
| 330 |
+
});
|
| 331 |
+
|
| 332 |
+
return {
|
| 333 |
+
file: filePath,
|
| 334 |
+
totalLines: lines.length,
|
| 335 |
+
ext,
|
| 336 |
+
imports: importLines.slice(0, 5), // first 5 import lines
|
| 337 |
+
exports: exportLines.slice(0, 5),
|
| 338 |
+
functions: fnLines.slice(0, 30), // up to 30 function signatures
|
| 339 |
+
classes: classLines.slice(0, 10),
|
| 340 |
+
sections: commentSections.slice(0, 20), // section comment headers
|
| 341 |
+
head: lines.slice(0, 30).map((l, i) => `${i+1}\t${l}`).join("\n"),
|
| 342 |
+
tail: lines.slice(-15).map((l, i) => `${lines.length-15+i+1}\t${l}`).join("\n"),
|
| 343 |
+
};
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
// 2. Read a specific chunk of a file by line range β like sed -n 'X,Yp'
|
| 347 |
+
// Returns line-numbered content so the AI can use exact line numbers in sed.
|
| 348 |
+
function readFileChunk(filePath, startLine, endLine) {
|
| 349 |
+
const abs = path.join(HOME_DIR, filePath);
|
| 350 |
+
if (!fs.existsSync(abs)) return null;
|
| 351 |
+
const lines = fs.readFileSync(abs, "utf8").split("\n");
|
| 352 |
+
const s = Math.max(1, startLine) - 1;
|
| 353 |
+
const e = Math.min(lines.length, endLine);
|
| 354 |
+
return {
|
| 355 |
+
file: filePath,
|
| 356 |
+
startLine: s + 1,
|
| 357 |
+
endLine: e,
|
| 358 |
+
totalLines: lines.length,
|
| 359 |
+
content: lines.slice(s, e).map((l, i) => `${s + i + 1}\t${l}`).join("\n"),
|
| 360 |
+
};
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
// 3. Search file for a pattern β like grep -n, returns structured matches
|
| 364 |
+
function searchFile(filePath, pattern) {
|
| 365 |
+
const abs = path.join(HOME_DIR, filePath);
|
| 366 |
+
if (!fs.existsSync(abs)) return null;
|
| 367 |
+
const lines = fs.readFileSync(abs, "utf8").split("\n");
|
| 368 |
+
let re;
|
| 369 |
+
try { re = new RegExp(pattern, "i"); } catch { re = new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i"); }
|
| 370 |
+
const matches = [];
|
| 371 |
+
lines.forEach((line, i) => {
|
| 372 |
+
if (re.test(line)) matches.push({ line: i + 1, text: line.trimEnd().slice(0, 120) });
|
| 373 |
+
});
|
| 374 |
+
return { file: filePath, pattern, totalLines: lines.length, matches: matches.slice(0, 40) };
|
| 375 |
+
}
|
| 376 |
+
|
| 377 |
+
// 4. Process an array of read_file actions the AI requested:
|
| 378 |
+
// [{ action: "map" }, { action: "chunk", start, end }, { action: "search", pattern }]
|
| 379 |
+
function processReadActions(filePath, actions = []) {
|
| 380 |
+
const results = [];
|
| 381 |
+
for (const a of actions) {
|
| 382 |
+
if (a.action === "map") {
|
| 383 |
+
results.push({ action: "map", result: buildFileMap(filePath) });
|
| 384 |
+
} else if (a.action === "chunk" && a.start && a.end) {
|
| 385 |
+
results.push({ action: "chunk", start: a.start, end: a.end, result: readFileChunk(filePath, a.start, a.end) });
|
| 386 |
+
} else if (a.action === "search" && a.pattern) {
|
| 387 |
+
results.push({ action: "search", pattern: a.pattern, result: searchFile(filePath, a.pattern) });
|
| 388 |
+
}
|
| 389 |
+
}
|
| 390 |
+
return results;
|
| 391 |
+
}
|
| 392 |
+
|
| 393 |
+
// 5. Format read results into a concise string the AI can parse in its next prompt
|
| 394 |
+
function formatReadResults(readResults) {
|
| 395 |
+
return readResults.map(r => {
|
| 396 |
+
if (r.action === "map" && r.result) {
|
| 397 |
+
const m = r.result;
|
| 398 |
+
const fns = m.functions.map(f => ` L${f.n}: ${f.preview}`).join("\n");
|
| 399 |
+
const secs = m.sections.map(s => ` L${s.n}: ${s.text}`).join("\n");
|
| 400 |
+
return `=== FILE MAP: ${m.file} (${m.totalLines} lines) ===
|
| 401 |
+
IMPORTS at lines: ${m.imports.join(", ") || "none"}
|
| 402 |
+
SECTIONS:\n${secs || " (none)"}
|
| 403 |
+
FUNCTIONS/ROUTES:\n${fns || " (none)"}
|
| 404 |
+
HEAD (lines 1-30):\n${m.head}
|
| 405 |
+
TAIL (last 15):\n${m.tail}`;
|
| 406 |
+
}
|
| 407 |
+
if (r.action === "chunk" && r.result) {
|
| 408 |
+
return `=== CHUNK ${r.result.file} lines ${r.result.startLine}-${r.result.endLine} (of ${r.result.totalLines}) ===\n${r.result.content}`;
|
| 409 |
+
}
|
| 410 |
+
if (r.action === "search" && r.result) {
|
| 411 |
+
const hits = r.result.matches.map(m => ` L${m.line}: ${m.text}`).join("\n");
|
| 412 |
+
return `=== SEARCH "${r.result.pattern}" in ${r.result.file} β ${r.result.matches.length} hits ===\n${hits || " (no matches)"}`;
|
| 413 |
+
}
|
| 414 |
+
return "";
|
| 415 |
+
}).filter(Boolean).join("\n\n");
|
| 416 |
+
}
|
| 417 |
+
|
| 418 |
// ββ VERSIONING βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 419 |
function readVersions() {
|
| 420 |
if (!fs.existsSync(VERSIONS_FILE)) return {};
|
|
|
|
| 498 |
// ββ callAI β LongCat API (Anthropic format) ββββββββββββββββββββββββββββββββ
|
| 499 |
// system = role/task definition + original user request for context anchoring
|
| 500 |
// query = specific data for this call (file snippet, cmd results, etc.)
|
| 501 |
+
// _modelKey accepted but ignored β all calls route to LongCat-Flash-Lite
|
| 502 |
// history = [{role:"user"|"assistant", content:"..."}] β conversation transcript
|
| 503 |
+
//
|
| 504 |
+
// On HTTP 429 / rate_limit_exceeded the call is transparently retried once
|
| 505 |
+
// using the backup API key β the caller never sees the error.
|
| 506 |
async function callAI(system, query, _modelKey, history = []) {
|
| 507 |
const userMessage = query ? `${system}\n\n${query}` : system;
|
| 508 |
|
|
|
|
| 510 |
const safeHistory = (Array.isArray(history) ? history : [])
|
| 511 |
.filter(h => h && (h.role === "user" || h.role === "assistant") && h.content)
|
| 512 |
.slice(-20) // last 10 user+assistant pairs
|
| 513 |
+
.map(h => ({ role: h.role, content: String(h.content).slice(0, 800) }));
|
| 514 |
|
|
|
|
| 515 |
const messages = [
|
| 516 |
...safeHistory,
|
| 517 |
{ role: "user", content: userMessage.slice(0, 5500) },
|
| 518 |
];
|
| 519 |
|
| 520 |
const payload = {
|
| 521 |
+
model: "LongCat-Flash-Lite",
|
| 522 |
max_tokens: 2000,
|
| 523 |
system: "You are Cryo, a precise developer AI. Respond with valid JSON only β no markdown fences, no prose. The original user request is embedded in the message; never lose sight of it.",
|
| 524 |
messages,
|
| 525 |
};
|
| 526 |
|
| 527 |
+
// Inner helper β attempt one call with the given key
|
| 528 |
+
async function attempt(apiKey) {
|
| 529 |
+
const { data } = await axios.post(LONGCAT_API_URL, payload, {
|
| 530 |
+
timeout: 60000,
|
| 531 |
+
headers: {
|
| 532 |
+
"Content-Type": "application/json",
|
| 533 |
+
"Authorization": `Bearer ${apiKey}`,
|
| 534 |
+
"anthropic-version": "2023-06-01",
|
| 535 |
+
},
|
| 536 |
+
});
|
| 537 |
+
// Anthropic format: data.content is an array of blocks
|
| 538 |
+
if (data.content && Array.isArray(data.content)) {
|
| 539 |
+
const textBlock = data.content.find(b => b.type === "text");
|
| 540 |
+
return textBlock ? textBlock.text : "";
|
| 541 |
+
}
|
| 542 |
+
return data.reply || data.text || data.message || "";
|
| 543 |
+
}
|
| 544 |
|
| 545 |
+
try {
|
| 546 |
+
return await attempt(LONGCAT_API_KEY);
|
| 547 |
+
} catch (err) {
|
| 548 |
+
// Detect quota exhaustion: HTTP 429 with rate_limit_exceeded code
|
| 549 |
+
const status = err.response?.status;
|
| 550 |
+
const errCode = err.response?.data?.error?.code;
|
| 551 |
+
if (status === 429 && errCode === "rate_limit_exceeded") {
|
| 552 |
+
// Silently retry with backup key β no error propagated to caller
|
| 553 |
+
return await attempt(LONGCAT_API_KEY_BACKUP);
|
| 554 |
+
}
|
| 555 |
+
throw err; // any other error re-thrown normally
|
| 556 |
}
|
|
|
|
| 557 |
}
|
| 558 |
|
| 559 |
// ββ SYNTAX FIX LOOP ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 623 |
let cmdIndex = 0;
|
| 624 |
let fileContent = initialContent;
|
| 625 |
let sessionTasks = [];
|
| 626 |
+
let pendingReadResults = []; // read_file results waiting to be shown to AI in next round
|
| 627 |
|
| 628 |
// ββ Helper: run a batch of commands and collect results βββββββββββββββ
|
| 629 |
+
// Deduplicates against already-run commands, structures output, guards retries
|
| 630 |
+
const cmdRetryCount = {}; // tracks per-cmd-pattern retry attempts
|
| 631 |
async function runBatch(commands) {
|
| 632 |
+
const alreadyRunCmds = allCommandResults.map(r => r.cmd);
|
| 633 |
+
const { safe, skipped } = filterCommands(commands, alreadyRunCmds);
|
| 634 |
+
|
| 635 |
+
// Notify about skipped commands so AI sees them in history
|
| 636 |
+
for (const s of skipped) {
|
| 637 |
+
send("status", { text: `Skipped (${s.reason}): ${s.cmd}` });
|
| 638 |
+
}
|
| 639 |
+
|
| 640 |
+
for (const cmd of safe) {
|
| 641 |
+
// Per-command retry cap: same base command (first 40 chars) max 3 times
|
| 642 |
+
const cmdKey = cmd.slice(0, 40);
|
| 643 |
+
cmdRetryCount[cmdKey] = (cmdRetryCount[cmdKey] || 0) + 1;
|
| 644 |
+
if (cmdRetryCount[cmdKey] > 3) {
|
| 645 |
+
send("status", { text: `Max retries reached for: ${cmd.slice(0, 60)}` });
|
| 646 |
+
continue;
|
| 647 |
+
}
|
| 648 |
+
|
| 649 |
const idx = cmdIndex++;
|
| 650 |
send("status", { text: `[${idx + 1}] ${cmd}` });
|
| 651 |
send("command", { cmd, index: idx });
|
| 652 |
const result = await runCmd(cmd);
|
| 653 |
+
const structured = structureResult(cmd, result.stdout, result.stderr, result.code);
|
| 654 |
+
allCommandResults.push({ cmd, ...result, structured });
|
| 655 |
historyEntries.push({ cmd, stdout: result.stdout.slice(0, 500), stderr: result.stderr.slice(0, 200), code: result.code, ts: Date.now() });
|
| 656 |
+
|
| 657 |
+
// Emit full structured result β frontend sees stdout, stderr, exit code
|
| 658 |
send("command_result", {
|
| 659 |
+
cmd,
|
| 660 |
+
stdout: result.stdout,
|
| 661 |
+
stderr: result.stderr,
|
| 662 |
+
exit: result.code,
|
| 663 |
+
ok: result.code === 0,
|
| 664 |
+
structured,
|
| 665 |
+
index: idx,
|
| 666 |
+
isError: result.code !== 0 && !!result.stderr,
|
| 667 |
});
|
| 668 |
+
|
| 669 |
// Auto-fix syntax errors in JS files
|
| 670 |
if (cmd.includes("node --check") && result.code !== 0 && result.stderr) {
|
| 671 |
const target = cmd.trim().split(/\s+/).pop().replace(/\s*\(.*\)$/, "");
|
|
|
|
| 713 |
return (headBlock + middleBlock + "\n" + tailBlock).substring(0, 2200);
|
| 714 |
})();
|
| 715 |
|
| 716 |
+
// Full command history with structured outputs β AI knows exactly what grep found,
|
| 717 |
+
// what sed changed, what errors occurred, without guessing from raw text
|
| 718 |
const cmdLog = allCommandResults.slice(-14).map((r, i) => {
|
| 719 |
+
const s = r.structured || {};
|
|
|
|
| 720 |
const exitLabel = r.code === 0 ? "β" : `β(${r.code})`;
|
| 721 |
+
let detail = "";
|
| 722 |
+
if (s.matches) detail = `\n MATCHES: ${s.matches.slice(0,10).join(" | ")}`;
|
| 723 |
+
else if (s.content) detail = `\n CONTENT: ${s.content.slice(0, 300)}`;
|
| 724 |
+
else if (r.stdout.trim()) detail = `\n OUT: ${r.stdout.trim().slice(0, 300)}`;
|
| 725 |
+
if (r.stderr.trim()) detail += `\n ERR: ${r.stderr.trim().slice(0, 150)}`;
|
| 726 |
+
if (s.linesChanged) detail += `\n EDIT: ${s.linesChanged}`;
|
| 727 |
+
if (s.syntaxError) detail += `\n SYNTAX: ${s.syntaxError.slice(0, 120)}`;
|
| 728 |
+
return `[${i + 1}] ${exitLabel} $ ${r.cmd}${detail}`;
|
| 729 |
}).join("\n");
|
| 730 |
|
| 731 |
// Highlight any commands that failed β the AI must not repeat them blindly
|
|
|
|
| 752 |
${pending.length > 0 ? pending.map((t, i) => ` ${i + 1}. ${t.task}`).join("\n") : " ALL TASKS DONE"}
|
| 753 |
|
| 754 |
COMMAND HISTORY (${allCommandResults.length} total β includes grep results, sed outputs, check results):
|
| 755 |
+
${cmdLog || " (none yet β start with read_file map)"}
|
| 756 |
+
${pendingReadResults.length > 0 ? `
|
| 757 |
+
FILE READ RESULTS (from your read_file requests last round):
|
| 758 |
+
${formatReadResults(pendingReadResults)}
|
| 759 |
+
` : ""}
|
| 760 |
FILE CONTENT WITH LINE NUMBERS (use these line numbers in sed -i 'Ns/...' commands):
|
| 761 |
${numberedCtx}
|
| 762 |
=== END CONTEXT ===`;
|
|
|
|
| 776 |
.map((l, i) => `${initTailStart + i + 1}\t${l}`).join("\n");
|
| 777 |
const initCtx = (initHeadBlock + `\n...[lines 41β${initTailStart} omitted β use grep -n to find sections]...\n` + initTailBlock).substring(0, 2200);
|
| 778 |
|
| 779 |
+
// ββ Auto file map for Round 1 β AI gets full structure before planning ββ
|
| 780 |
+
// This is exactly what Claude does: understand structure before touching anything.
|
| 781 |
+
const fileMap = buildFileMap(filePath);
|
| 782 |
+
const fileMapStr = fileMap ? formatReadResults([{ action: "map", result: fileMap }]) : "";
|
| 783 |
+
|
| 784 |
const planPrompt = `You are Cryo - a developer AI agent. You edit files using shell commands.
|
| 785 |
|
| 786 |
${COMMANDS_REFERENCE}
|
| 787 |
|
| 788 |
+
${fileMapStr ? `=== FILE STRUCTURE (auto-read before you start) ===\n${fileMapStr}\n` : ""}
|
| 789 |
FILE: ${filePath} | TOTAL LINES: ${fileLines}
|
| 790 |
+
FILE CONTENT HEAD+TAIL (with line numbers):
|
| 791 |
${initCtx}
|
| 792 |
|
| 793 |
USER REQUEST: "${message}"
|
| 794 |
|
| 795 |
β Complete ONLY what was requested. Do not add unrequested features.
|
| 796 |
+
β The FILE STRUCTURE above shows all functions and sections with line numbers.
|
| 797 |
+
β Use read_file chunk/search to read any section you need before editing.
|
| 798 |
+
β For library swaps or >30% restructures, plan to use full_rewrite after reading.
|
| 799 |
|
| 800 |
+
For your FIRST response, output read_file actions to read the sections you'll need,
|
| 801 |
+
OR output commands if you already have enough context from the file map above.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 802 |
|
| 803 |
Respond with ONLY raw JSON:
|
| 804 |
{
|
| 805 |
"task_type": "query" | "edit" | "create",
|
| 806 |
"status": "brief status for the user",
|
| 807 |
"tasks": [{"task": "specific subtask", "done": false}],
|
| 808 |
+
"read_file": [{ "action": "chunk", "start": N, "end": M }, ...],
|
| 809 |
+
"commands": ["cp ${filePath} ${filePath}.bak", "..."],
|
| 810 |
+
"reasoning": "what sections you need to read and why",
|
| 811 |
"done": false
|
| 812 |
}`;
|
| 813 |
|
| 814 |
const planRaw = await callAI(planPrompt, "", modelKey, transcript);
|
| 815 |
let plan = parseJSON(planRaw);
|
| 816 |
|
| 817 |
+
if (!plan || (!plan.commands && !plan.read_file)) {
|
| 818 |
const extracted = extractCommandsFromText(planRaw || "");
|
| 819 |
plan = {
|
| 820 |
task_type: "edit",
|
|
|
|
| 830 |
send("status", { text: plan.status || "Processing..." });
|
| 831 |
send("task_update", { tasks: sessionTasks });
|
| 832 |
|
| 833 |
+
// Process any read_file actions from the plan (Round 1 reads)
|
| 834 |
+
if (plan.read_file && Array.isArray(plan.read_file) && plan.read_file.length > 0) {
|
| 835 |
+
send("status", { text: `Reading file structure (${plan.read_file.map(a => a.action).join(", ")})...` });
|
| 836 |
+
const readResults = processReadActions(filePath, plan.read_file);
|
| 837 |
+
pendingReadResults = readResults;
|
| 838 |
+
for (const r of readResults) {
|
| 839 |
+
const label = r.action === "map" ? "file map" : r.action === "chunk" ? `lines ${r.start}β${r.end}` : `search "${r.pattern}"`;
|
| 840 |
+
send("command_result", { cmd: `[read_file:${r.action}] ${filePath} ${label}`, stdout: formatReadResults([r]), stderr: "", exit: 0, ok: true, isError: false, index: cmdIndex++ });
|
| 841 |
+
}
|
| 842 |
+
}
|
| 843 |
+
|
| 844 |
await runBatch(plan.commands || []);
|
| 845 |
|
| 846 |
// ββ ROUNDS 2βN: Continue until all tasks done βββββββββββββββββββββββββ
|
|
|
|
| 860 |
β YOUR ONLY PURPOSE: complete the ORIGINAL REQUEST shown at the top.
|
| 861 |
β READ THE COMMAND HISTORY β it shows what grep found, what lines exist, what failed.
|
| 862 |
β USE THE LINE NUMBERS in the file content to write precise sed -i 'Ns/...' commands.
|
| 863 |
+
β NEVER repeat a command already in COMMAND HISTORY with the same arguments.
|
| 864 |
|
| 865 |
DECISION TREE THIS ROUND:
|
| 866 |
- If you haven't grepped yet β grep -n first to locate lines
|
|
|
|
| 868 |
- If you have the exact text β sed -i 'Ns/old/new/' to make the targeted change
|
| 869 |
- If you just edited β sed -n 'START,ENDp' to verify the change is in the file
|
| 870 |
- If JS file was written β node --check to verify syntax
|
| 871 |
+
- If the change is major (library swap, full restructure) β use full_rewrite with new_content
|
| 872 |
- If all tasks verified in file β set done:true
|
| 873 |
|
| 874 |
+
Respond with ONLY raw JSON (choose A or B):
|
| 875 |
+
|
| 876 |
+
A) Normal commands:
|
| 877 |
{
|
| 878 |
"done": true | false,
|
| 879 |
"status": "what you are doing",
|
|
|
|
| 882 |
"reasoning": "what the grep/sed output told you and what you are doing next"
|
| 883 |
}
|
| 884 |
|
| 885 |
+
B) Full file rewrite (for major changes like library swaps):
|
| 886 |
+
{
|
| 887 |
+
"done": true,
|
| 888 |
+
"full_rewrite": true,
|
| 889 |
+
"new_content": "COMPLETE new file content here",
|
| 890 |
+
"status": "Rewrote file to use X",
|
| 891 |
+
"tasks": [{"task": "...", "done": true}],
|
| 892 |
+
"reasoning": "why a full rewrite is needed"
|
| 893 |
+
}
|
| 894 |
+
|
| 895 |
+
CRITICAL: done:true only when sed -n CONFIRMS the change is physically in the file, OR full_rewrite:true is set.`;
|
| 896 |
|
| 897 |
const contRaw = await callAI(continuePrompt, "", modelKey, transcript);
|
| 898 |
let cont = parseJSON(contRaw);
|
|
|
|
| 903 |
cont = { done: false, commands: extracted, tasks: sessionTasks, status: "Continuing...", reasoning: "Extracted" };
|
| 904 |
}
|
| 905 |
|
| 906 |
+
// ββ READ_FILE: AI requested structured file inspection βββββββββββββ
|
| 907 |
+
// Process immediately and store results for the NEXT round's context.
|
| 908 |
+
// This is the "Claude reads file in chunks" pattern β map β search β chunk β edit.
|
| 909 |
+
if (cont.read_file && Array.isArray(cont.read_file) && cont.read_file.length > 0) {
|
| 910 |
+
send("status", { text: `Reading file (${cont.read_file.map(a => a.action).join(", ")})...` });
|
| 911 |
+
const readResults = processReadActions(filePath, cont.read_file);
|
| 912 |
+
pendingReadResults = readResults; // will appear in next round's context
|
| 913 |
+
|
| 914 |
+
// Also emit each result to the frontend so user can see what was read
|
| 915 |
+
for (const r of readResults) {
|
| 916 |
+
const label = r.action === "map" ? "file map"
|
| 917 |
+
: r.action === "chunk" ? `lines ${r.start}β${r.end}`
|
| 918 |
+
: `search "${r.pattern}"`;
|
| 919 |
+
send("command_result", {
|
| 920 |
+
cmd: `[read_file:${r.action}] ${filePath} ${label}`,
|
| 921 |
+
stdout: formatReadResults([r]),
|
| 922 |
+
stderr: "",
|
| 923 |
+
exit: 0,
|
| 924 |
+
ok: true,
|
| 925 |
+
isError: false,
|
| 926 |
+
index: cmdIndex++,
|
| 927 |
+
});
|
| 928 |
+
}
|
| 929 |
+
|
| 930 |
+
// Update task list if AI provided one
|
| 931 |
+
if (cont.tasks && cont.tasks.length > 0) {
|
| 932 |
+
sessionTasks = cont.tasks;
|
| 933 |
+
send("task_update", { tasks: sessionTasks });
|
| 934 |
+
}
|
| 935 |
+
send("status", { text: cont.status || `Read complete β round ${round + 1} will use results` });
|
| 936 |
+
// Also run any commands the AI included alongside read_file
|
| 937 |
+
if (cont.commands && cont.commands.length > 0) await runBatch(cont.commands);
|
| 938 |
+
continue; // go to next round with read results in context
|
| 939 |
+
}
|
| 940 |
+
|
| 941 |
+
// Clear pending reads once the AI has seen them (they were in this round's context)
|
| 942 |
+
pendingReadResults = [];
|
| 943 |
+
if (cont.full_rewrite && cont.new_content) {
|
| 944 |
+
send("status", { text: "Staging full rewrite..." });
|
| 945 |
+
const stageName = `__cryo_stage_${Date.now()}${path.extname(filePath)}`;
|
| 946 |
+
const stagePath = path.join(HOME_DIR, stageName);
|
| 947 |
+
fs.writeFileSync(stagePath, cont.new_content, "utf8");
|
| 948 |
+
|
| 949 |
+
// Validate staged file before overwriting
|
| 950 |
+
let stageOk = true;
|
| 951 |
+
if (filePath.endsWith(".js")) {
|
| 952 |
+
const chk = await runCmd(`node --check ${stageName}`);
|
| 953 |
+
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++ });
|
| 954 |
+
if (chk.code !== 0) {
|
| 955 |
+
send("status", { text: "Staged rewrite has syntax errors β fixing..." });
|
| 956 |
+
await syntaxFixLoop(stageName, send, modelKey, 3, transcript);
|
| 957 |
+
stageOk = (await runCmd(`node --check ${stageName}`)).code === 0;
|
| 958 |
+
}
|
| 959 |
+
}
|
| 960 |
+
|
| 961 |
+
if (stageOk) {
|
| 962 |
+
versionFile(filePath); // backup current before overwrite
|
| 963 |
+
fs.copyFileSync(stagePath, absPath);
|
| 964 |
+
fs.unlinkSync(stagePath);
|
| 965 |
+
fileContent = fs.readFileSync(absPath, "utf8");
|
| 966 |
+
send("command_result", { cmd: `[full_rewrite] ${filePath}`, stdout: "β File replaced atomically", stderr: "", exit: 0, ok: true, isError: false, index: cmdIndex++ });
|
| 967 |
+
allCommandResults.push({ cmd: `[full_rewrite] ${filePath}`, stdout: "replaced", stderr: "", code: 0 });
|
| 968 |
+
send("status", { text: "Full rewrite applied β" });
|
| 969 |
+
if (cont.tasks && cont.tasks.length > 0) { sessionTasks = cont.tasks; send("task_update", { tasks: sessionTasks }); }
|
| 970 |
+
if (cont.done) break;
|
| 971 |
+
continue;
|
| 972 |
+
} else {
|
| 973 |
+
fs.unlinkSync(stagePath);
|
| 974 |
+
send("status", { text: "Staged rewrite failed validation β continuing with commands" });
|
| 975 |
+
}
|
| 976 |
+
}
|
| 977 |
+
|
| 978 |
+
if (!cont) {
|
| 979 |
+
const extracted = extractCommandsFromText(contRaw || "");
|
| 980 |
+
if (extracted.length === 0) break;
|
| 981 |
+
cont = { done: false, commands: extracted, tasks: sessionTasks, status: "Continuing...", reasoning: "Extracted" };
|
| 982 |
+
}
|
| 983 |
+
|
| 984 |
// Update task list from AI's assessment
|
| 985 |
if (cont.tasks && cont.tasks.length > 0) {
|
| 986 |
sessionTasks = cont.tasks;
|
|
|
|
| 1014 |
plan,
|
| 1015 |
sessionTasks,
|
| 1016 |
hadSedEdits: allCommandResults.some(r => r.cmd.startsWith("sed -i") && r.code === 0),
|
| 1017 |
+
hadTeeWrites: allCommandResults.some(r => (r.cmd.startsWith("tee ") || r.cmd.includes(" > ")) && r.code === 0),
|
| 1018 |
+
hadFullRewrite: allCommandResults.some(r => r.cmd.startsWith("[full_rewrite]") && r.code === 0),
|
| 1019 |
};
|
| 1020 |
}
|
| 1021 |
|
| 1022 |
+
// ββ FILE INSPECT API (map / chunk / search) βββββββββββββββββββββββββββββββ
|
| 1023 |
+
app.post("/api/inspect", (req, res) => {
|
| 1024 |
+
const { filePath, actions } = req.body;
|
| 1025 |
+
if (!filePath) return res.status(400).json({ error: "filePath required" });
|
| 1026 |
+
const abs = path.join(HOME_DIR, filePath);
|
| 1027 |
+
if (!abs.startsWith(HOME_DIR)) return res.status(403).json({ error: "Forbidden" });
|
| 1028 |
+
const results = processReadActions(filePath, actions || [{ action: "map" }]);
|
| 1029 |
+
res.json({ results, formatted: formatReadResults(results) });
|
| 1030 |
+
});
|
| 1031 |
+
|
| 1032 |
// ββ FILE TREE ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1033 |
app.get("/api/tree", (req, res) => {
|
| 1034 |
const buildTree = (dir, base = HOME_DIR) => {
|
|
|
|
| 1117 |
app.post("/api/exec", (req, res) => {
|
| 1118 |
const { command } = req.body;
|
| 1119 |
if (!isSafeCmd(command)) {
|
| 1120 |
+
return res.json({ stdout: "", stderr: `Blocked: ${command.trim().split(/\s+/)[0]}`, code: 1, ok: false });
|
| 1121 |
}
|
| 1122 |
exec(command, { cwd: HOME_DIR, timeout: 30000, shell: "/bin/bash" }, (err, stdout, stderr) => {
|
| 1123 |
+
const code = err ? (err.code || 1) : 0;
|
| 1124 |
+
res.json({
|
| 1125 |
+
...structureResult(command, stdout || "", stderr || "", code),
|
| 1126 |
+
code,
|
| 1127 |
+
});
|
| 1128 |
});
|
| 1129 |
});
|
| 1130 |
|
|
|
|
| 1252 |
filePath, fileContent, message, modelKey, send, maxRounds: 6, transcript
|
| 1253 |
});
|
| 1254 |
|
| 1255 |
+
const { allCommandResults, hadSedEdits, hadFullRewrite, plan } = loopResult;
|
| 1256 |
fileContent = loopResult.fileContent;
|
| 1257 |
|
| 1258 |
const resultsText = allCommandResults
|
|
|
|
| 1292 |
|
| 1293 |
} else {
|
| 1294 |
// ββ EDIT/CREATE: sed/tee was used β verify & summarize βββββοΏ½οΏ½ββββββββ
|
| 1295 |
+
if (hadSedEdits || loopResult.hadTeeWrites || hadFullRewrite) {
|
| 1296 |
send("status", { text: "Verifying edits..." });
|
| 1297 |
if (filePath.endsWith(".js")) {
|
| 1298 |
await syntaxFixLoop(filePath, send, modelKey, 3, transcript);
|