Update server.js
Browse files
server.js
CHANGED
|
@@ -71,7 +71,9 @@ const RECOMMENDED_CMDS = [
|
|
| 71 |
];
|
| 72 |
const BLOCKED_CMDS = ["sudo","su","reboot","shutdown","mkfs","fdisk"];
|
| 73 |
function isSafeCmd(cmd) {
|
|
|
|
| 74 |
const name = cmd.trim().split(/\s+/)[0];
|
|
|
|
| 75 |
return !BLOCKED_CMDS.includes(name);
|
| 76 |
}
|
| 77 |
|
|
@@ -162,8 +164,13 @@ const DESTRUCTIVE_PATTERNS = [
|
|
| 162 |
function filterCommands(commands, alreadyRun = []) {
|
| 163 |
const safe = [];
|
| 164 |
const skipped = [];
|
| 165 |
-
const runSet = new Set(alreadyRun.map(c => c.trim()));
|
| 166 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
const trimmed = cmd.trim();
|
| 168 |
if (!trimmed) continue;
|
| 169 |
if (runSet.has(trimmed)) { skipped.push({ cmd: trimmed, reason: "duplicate" }); continue; }
|
|
@@ -235,7 +242,31 @@ function parseJSON(raw) {
|
|
| 235 |
return null;
|
| 236 |
}
|
| 237 |
|
| 238 |
-
// ββ
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 239 |
// When AI returns markdown/prose instead of JSON, pull shell commands from it
|
| 240 |
function extractCommandsFromText(text) {
|
| 241 |
const cmds = [];
|
|
@@ -629,8 +660,13 @@ async function runAgentLoop(opts) {
|
|
| 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(
|
| 634 |
|
| 635 |
// Notify about skipped commands so AI sees them in history
|
| 636 |
for (const s of skipped) {
|
|
@@ -809,10 +845,16 @@ Respond with ONLY raw JSON:
|
|
| 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 || "");
|
|
@@ -892,10 +934,17 @@ B) Full file rewrite (for major changes like library swaps):
|
|
| 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);
|
| 899 |
|
| 900 |
if (!cont) {
|
| 901 |
const extracted = extractCommandsFromText(contRaw || "");
|
|
|
|
| 71 |
];
|
| 72 |
const BLOCKED_CMDS = ["sudo","su","reboot","shutdown","mkfs","fdisk"];
|
| 73 |
function isSafeCmd(cmd) {
|
| 74 |
+
if (cmd == null || typeof cmd !== "string") return false;
|
| 75 |
const name = cmd.trim().split(/\s+/)[0];
|
| 76 |
+
if (!name) return false;
|
| 77 |
return !BLOCKED_CMDS.includes(name);
|
| 78 |
}
|
| 79 |
|
|
|
|
| 164 |
function filterCommands(commands, alreadyRun = []) {
|
| 165 |
const safe = [];
|
| 166 |
const skipped = [];
|
| 167 |
+
const runSet = new Set(alreadyRun.map(c => (typeof c === "string" ? c.trim() : "")));
|
| 168 |
+
// Normalise input: flatten, unwrap {cmd} objects, drop anything not a string
|
| 169 |
+
const flat = (Array.isArray(commands) ? commands : [])
|
| 170 |
+
.flat(3)
|
| 171 |
+
.map(c => (c != null && typeof c === "object" && typeof c.cmd === "string") ? c.cmd : c)
|
| 172 |
+
.filter(c => c != null && typeof c === "string");
|
| 173 |
+
for (const cmd of flat) {
|
| 174 |
const trimmed = cmd.trim();
|
| 175 |
if (!trimmed) continue;
|
| 176 |
if (runSet.has(trimmed)) { skipped.push({ cmd: trimmed, reason: "duplicate" }); continue; }
|
|
|
|
| 242 |
return null;
|
| 243 |
}
|
| 244 |
|
| 245 |
+
// ββ SANITIZE AI RESPONSE βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 246 |
+
// Ensures "commands" is always a flat array of non-empty strings.
|
| 247 |
+
// The AI occasionally returns nulls, objects, nested arrays, or step-label strings.
|
| 248 |
+
function sanitizeAIResponse(parsed) {
|
| 249 |
+
if (!parsed) return parsed;
|
| 250 |
+
if (parsed.commands !== undefined) {
|
| 251 |
+
const raw = Array.isArray(parsed.commands) ? parsed.commands : [];
|
| 252 |
+
parsed.commands = raw
|
| 253 |
+
.flat(5)
|
| 254 |
+
.map(c => {
|
| 255 |
+
if (c == null) return null;
|
| 256 |
+
if (typeof c === "string") return c.trim() || null;
|
| 257 |
+
// Unwrap {cmd: "..."} or {command: "..."} objects
|
| 258 |
+
if (typeof c === "object") {
|
| 259 |
+
const v = c.cmd || c.command || c.shell || c.run || c.exec;
|
| 260 |
+
return typeof v === "string" ? v.trim() || null : null;
|
| 261 |
+
}
|
| 262 |
+
return null;
|
| 263 |
+
})
|
| 264 |
+
.filter(c => c != null && c.length > 0 && isSafeCmd(c));
|
| 265 |
+
}
|
| 266 |
+
return parsed;
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
|
| 270 |
// When AI returns markdown/prose instead of JSON, pull shell commands from it
|
| 271 |
function extractCommandsFromText(text) {
|
| 272 |
const cmds = [];
|
|
|
|
| 660 |
// Deduplicates against already-run commands, structures output, guards retries
|
| 661 |
const cmdRetryCount = {}; // tracks per-cmd-pattern retry attempts
|
| 662 |
async function runBatch(commands) {
|
| 663 |
+
// Normalise before anything else β AI sometimes returns nulls, objects, nested arrays
|
| 664 |
+
const normalised = (Array.isArray(commands) ? commands : [])
|
| 665 |
+
.flat(3)
|
| 666 |
+
.map(c => (c != null && typeof c === "object" && typeof c.cmd === "string") ? c.cmd : c)
|
| 667 |
+
.filter(c => c != null && typeof c === "string" && c.trim().length > 0);
|
| 668 |
const alreadyRunCmds = allCommandResults.map(r => r.cmd);
|
| 669 |
+
const { safe, skipped } = filterCommands(normalised, alreadyRunCmds);
|
| 670 |
|
| 671 |
// Notify about skipped commands so AI sees them in history
|
| 672 |
for (const s of skipped) {
|
|
|
|
| 845 |
"commands": ["cp ${filePath} ${filePath}.bak", "..."],
|
| 846 |
"reasoning": "what sections you need to read and why",
|
| 847 |
"done": false
|
| 848 |
+
}
|
| 849 |
+
|
| 850 |
+
COMMANDS FORMAT RULES β VIOLATIONS WILL CRASH THE SYSTEM:
|
| 851 |
+
- "commands" MUST be a flat array of plain strings only
|
| 852 |
+
- NEVER put null, objects, or nested arrays inside "commands"
|
| 853 |
+
- NEVER omit "commands" β use [] if no commands needed this round
|
| 854 |
+
- Every entry must be a complete, executable bash command string`;
|
| 855 |
|
| 856 |
const planRaw = await callAI(planPrompt, "", modelKey, transcript);
|
| 857 |
+
let plan = sanitizeAIResponse(parseJSON(planRaw));
|
| 858 |
|
| 859 |
if (!plan || (!plan.commands && !plan.read_file)) {
|
| 860 |
const extracted = extractCommandsFromText(planRaw || "");
|
|
|
|
| 934 |
"reasoning": "why a full rewrite is needed"
|
| 935 |
}
|
| 936 |
|
| 937 |
+
COMMANDS FORMAT RULES β VIOLATIONS WILL CRASH THE SYSTEM:
|
| 938 |
+
- "commands" MUST be a flat array of plain strings. Example: ["sed -i '7s/get/head/' bot.js"]
|
| 939 |
+
- NEVER put objects, null, arrays-within-arrays, or non-strings in "commands"
|
| 940 |
+
- NEVER omit "commands" β use [] if you have no commands to run
|
| 941 |
+
- EVERY command must be a complete shell command runnable in bash
|
| 942 |
+
- NEVER include comments, explanations, or step labels inside the commands array
|
| 943 |
+
|
| 944 |
CRITICAL: done:true only when sed -n CONFIRMS the change is physically in the file, OR full_rewrite:true is set.`;
|
| 945 |
|
| 946 |
const contRaw = await callAI(continuePrompt, "", modelKey, transcript);
|
| 947 |
+
let cont = sanitizeAIResponse(parseJSON(contRaw));
|
| 948 |
|
| 949 |
if (!cont) {
|
| 950 |
const extracted = extractCommandsFromText(contRaw || "");
|