Reaperxxxx commited on
Commit
91bb7f0
Β·
verified Β·
1 Parent(s): ea2f6bd

Create server.js

Browse files
Files changed (1) hide show
  1. server.js +595 -0
server.js ADDED
@@ -0,0 +1,595 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const express = require("express");
2
+ const multer = require("multer");
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const { exec } = require("child_process");
6
+ const cors = require("cors");
7
+ const axios = require("axios");
8
+
9
+ const app = express();
10
+ const PORT = 3000;
11
+ const HOME_DIR = path.join(__dirname, "home");
12
+ const TASK_FILE = path.join(HOME_DIR, ".zoro_tasks.txt");
13
+
14
+ if (!fs.existsSync(HOME_DIR)) fs.mkdirSync(HOME_DIR, { recursive: true });
15
+
16
+ app.use(cors());
17
+ app.use(express.json({ limit: "10mb" }));
18
+ app.use(express.static(path.join(__dirname, "public")));
19
+
20
+ // ── Multer ─────────────────────────────────────────────────────────────────
21
+ const storage = multer.diskStorage({
22
+ destination: HOME_DIR,
23
+ filename: (req, file, cb) => cb(null, "index.js"),
24
+ });
25
+ const upload = multer({ storage });
26
+
27
+ // ── ALLOWED COMMANDS ───────────────────────────────────────────────────────
28
+ const ALLOWED_CMDS = [
29
+ "grep", "sed", "awk", "head", "tail", "cat", "wc",
30
+ "sort", "uniq", "cut", "tr", "find", "ls", "echo",
31
+ "diff", "patch", "node", "npm", "file", "stat",
32
+ ];
33
+
34
+ const COMMANDS_REFERENCE = `
35
+ === SHELL COMMANDS AVAILABLE (cwd = /home) ===
36
+
37
+ FILE EXPLORATION:
38
+ ls -l List files & directories
39
+ find . -name "*.js" Find JS files recursively
40
+ find . -name "*.json" Find config files
41
+ file filename Show file type
42
+ stat filename File metadata (size, dates)
43
+
44
+ READING CONTENT:
45
+ cat file Read entire file
46
+ head -n N file First N lines
47
+ tail -n N file Last N lines
48
+ sed -n 'X,Yp' file Lines X to Y (surgical)
49
+ head -c N file First N bytes
50
+ grep -n "pattern" file Lines matching pattern (with line numbers)
51
+ grep -A5 "pattern" file Pattern + 5 lines after
52
+ grep -B3 "pattern" file Pattern + 3 lines before
53
+ grep -C3 "pattern" file Pattern + 3 lines context (before+after)
54
+ grep -rn "pattern" . Recursive search all files
55
+
56
+ COUNTING / STATS:
57
+ wc -l file Count lines
58
+ grep -c "pattern" file Count occurrences
59
+ awk 'END{print NR}' file Alternative line count
60
+
61
+ EDITING (surgical, safe):
62
+ sed -i 's/old/new/g' file Replace ALL occurrences
63
+ sed -i '12s/old/new/' file Replace on line 12 ONLY
64
+ sed -i 'X,Ys/old/new/g' file Replace in lines X–Y
65
+ awk '/pattern/' file Extract matching lines
66
+ cut -d',' -f2 file Extract 2nd CSV column
67
+ tr 'a-z' 'A-Z' Transform chars
68
+
69
+ COMPARING:
70
+ diff old.js new.js Show differences between files
71
+ patch file < patchfile Apply a patch file
72
+
73
+ SYNTAX / ERROR CHECKING:
74
+ node --check file.js JavaScript syntax validation
75
+ npm test Run project tests
76
+
77
+ PROJECT INFO:
78
+ cat package.json Show project dependencies
79
+ npm run scriptname Run npm scripts
80
+
81
+ UTILITIES:
82
+ echo "text" Print text
83
+ sort file Sort lines alphabetically
84
+ uniq file Remove duplicate lines
85
+
86
+ === AGENT RULES ===
87
+ 1. ALWAYS run "node --check file.js" BEFORE any edit
88
+ 2. ALWAYS run "node --check file.js" AFTER any edit
89
+ 3. Use grep -C3 to understand context BEFORE inserting new code
90
+ 4. Match existing code style β€” grep for similar patterns first
91
+ 5. Use sed -n 'X,Yp' to read specific sections before modifying them
92
+ 6. Back up important sections mentally before sed -i changes
93
+ 7. For adding new features: grep for existing similar features first
94
+ ===============================================
95
+ `;
96
+
97
+ // ── File Tree ──────────────────────────────────────────────────────────────
98
+ app.get("/api/tree", (req, res) => {
99
+ const buildTree = (dir, base = HOME_DIR) => {
100
+ try {
101
+ return fs.readdirSync(dir, { withFileTypes: true })
102
+ .filter(e => !e.name.startsWith(".zoro_"))
103
+ .map(e => {
104
+ const relPath = path.relative(base, path.join(dir, e.name));
105
+ if (e.isDirectory()) {
106
+ return { type: "dir", name: e.name, path: relPath, children: buildTree(path.join(dir, e.name), base) };
107
+ }
108
+ const stats = fs.statSync(path.join(dir, e.name));
109
+ return { type: "file", name: e.name, path: relPath, size: stats.size };
110
+ });
111
+ } catch { return []; }
112
+ };
113
+ try { res.json({ tree: buildTree(HOME_DIR) }); }
114
+ catch { res.json({ tree: [] }); }
115
+ });
116
+
117
+ app.post("/api/upload", upload.single("file"), (req, res) => {
118
+ res.json({ success: true, path: "index.js" });
119
+ });
120
+
121
+ app.get("/api/file", (req, res) => {
122
+ const fp = path.join(HOME_DIR, req.query.path || "index.js");
123
+ if (!fp.startsWith(HOME_DIR)) return res.status(403).json({ error: "Forbidden" });
124
+ if (!fs.existsSync(fp)) return res.status(404).json({ error: "Not found" });
125
+ res.json({ content: fs.readFileSync(fp, "utf8") });
126
+ });
127
+
128
+ app.post("/api/file", (req, res) => {
129
+ const { filePath, content } = req.body;
130
+ const abs = path.join(HOME_DIR, filePath);
131
+ if (!abs.startsWith(HOME_DIR)) return res.status(403).json({ error: "Forbidden" });
132
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
133
+ fs.writeFileSync(abs, content, "utf8");
134
+ res.json({ success: true });
135
+ });
136
+
137
+ app.delete("/api/file", (req, res) => {
138
+ const { filePath } = req.body;
139
+ const abs = path.join(HOME_DIR, filePath);
140
+ if (!abs.startsWith(HOME_DIR)) return res.status(403).json({ error: "Forbidden" });
141
+ if (fs.existsSync(abs)) fs.unlinkSync(abs);
142
+ res.json({ success: true });
143
+ });
144
+
145
+ app.get("/api/download", (req, res) => {
146
+ const fp = path.join(HOME_DIR, req.query.path);
147
+ if (!fp.startsWith(HOME_DIR)) return res.status(403).json({ error: "Forbidden" });
148
+ if (!fs.existsSync(fp)) return res.status(404).json({ error: "Not found" });
149
+ res.download(fp);
150
+ });
151
+
152
+ app.get("/api/tasks", (req, res) => {
153
+ if (!fs.existsSync(TASK_FILE)) return res.json({ tasks: [] });
154
+ res.json({ tasks: readTaskLog() });
155
+ });
156
+
157
+ // ── Execute command ────────────────────────────────────────────────────────
158
+ app.post("/api/exec", (req, res) => {
159
+ const { command } = req.body;
160
+ const cmdName = command.trim().split(/\s+/)[0];
161
+ if (!ALLOWED_CMDS.includes(cmdName)) {
162
+ return res.json({ stdout: "", stderr: `'${cmdName}' not in allowed commands.`, code: 1 });
163
+ }
164
+ exec(command, { cwd: HOME_DIR, timeout: 15000 }, (err, stdout, stderr) => {
165
+ res.json({ stdout: stdout || "", stderr: stderr || "", code: err ? (err.code || 1) : 0 });
166
+ });
167
+ });
168
+
169
+ // ── Core Helpers ───────────────────────────────────────────────────────────
170
+
171
+ async function callAI(system, query) {
172
+ const params = new URLSearchParams({ BK9: system, q: query, model: "compound-beta-mini" });
173
+ const { data } = await axios.get(`https://api.bk9.dev/ai/BK94?${params}`, { timeout: 30000 });
174
+ return data.BK9 || data.response || data.message || "";
175
+ }
176
+
177
+ function parseJSON(raw) {
178
+ if (!raw) return null;
179
+ let s = raw.replace(/```json\s*/gi, "").replace(/```\s*/g, "").trim();
180
+ const m = s.match(/\{[\s\S]*\}/);
181
+ if (m) s = m[0];
182
+ try { return JSON.parse(s); } catch { return null; }
183
+ }
184
+
185
+ function runCmd(command) {
186
+ return new Promise(resolve => {
187
+ const name = command.trim().split(/\s+/)[0];
188
+ if (!ALLOWED_CMDS.includes(name)) {
189
+ return resolve({ stdout: "", stderr: `Not allowed: ${name}`, code: 1 });
190
+ }
191
+ exec(command, { cwd: HOME_DIR, timeout: 15000 }, (err, stdout, stderr) => {
192
+ resolve({ stdout: stdout || "", stderr: stderr || "", code: err ? (err.code || 1) : 0 });
193
+ });
194
+ });
195
+ }
196
+
197
+ function backupFile(filePath) {
198
+ const src = path.join(HOME_DIR, filePath);
199
+ if (!fs.existsSync(src)) return;
200
+ const dst = path.join(HOME_DIR, filePath.replace(/(\.\w+)?$/, "_backup$1"));
201
+ fs.copyFileSync(src, dst);
202
+ }
203
+
204
+ // Smart context: for big files, find relevant section using query keywords
205
+ function getSmartContext(content, query, maxLines = 120) {
206
+ const lines = content.split("\n");
207
+ if (lines.length <= maxLines) return content;
208
+
209
+ const head = lines.slice(0, 40).join("\n");
210
+ const tail = lines.slice(-15).join("\n");
211
+ const words = query.toLowerCase().split(/\s+/).filter(w => w.length > 3);
212
+
213
+ let rs = -1, re = -1;
214
+ for (let i = 0; i < lines.length; i++) {
215
+ const lower = lines[i].toLowerCase();
216
+ if (words.some(w => lower.includes(w))) {
217
+ if (rs === -1) rs = Math.max(0, i - 5);
218
+ re = Math.min(lines.length - 1, i + 35);
219
+ }
220
+ }
221
+
222
+ const middle = rs !== -1
223
+ ? `\n// ... [lines 41–${rs} omitted] ...\n` + lines.slice(rs, re + 1).join("\n") + `\n// ... [lines ${re + 1} onward omitted] ...\n`
224
+ : `\n// ... [${lines.length - 55} lines omitted] ...\n`;
225
+
226
+ return head + middle + tail;
227
+ }
228
+
229
+ // Task log
230
+ function updateTaskLog(tasks) {
231
+ const lines = ["=== ZORO TASK LOG ===", `Updated: ${new Date().toISOString()}`, ""];
232
+ for (const t of tasks) {
233
+ lines.push(`[${t.done ? "βœ…" : "⬜"}] ${t.task}`);
234
+ if (t.note) lines.push(` β†’ ${t.note}`);
235
+ }
236
+ fs.writeFileSync(TASK_FILE, lines.join("\n"), "utf8");
237
+ }
238
+
239
+ function readTaskLog() {
240
+ if (!fs.existsSync(TASK_FILE)) return [];
241
+ return fs.readFileSync(TASK_FILE, "utf8").split("\n")
242
+ .map(l => l.match(/^\[(βœ…|⬜)\] (.+)/))
243
+ .filter(Boolean)
244
+ .map(m => ({ done: m[1] === "βœ…", task: m[2] }));
245
+ }
246
+
247
+ // Run syntax check, auto-fix loop up to maxAttempts times
248
+ async function syntaxFixLoop(filePath, send, maxAttempts = 3) {
249
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
250
+ const check = await runCmd(`node --check ${filePath}`);
251
+ const cmdIdx = 800 + attempt;
252
+
253
+ send("command", { cmd: `node --check ${filePath} (attempt ${attempt})`, index: cmdIdx });
254
+ send("command_result", {
255
+ cmd: `node --check ${filePath}`,
256
+ stdout: check.code === 0 ? "βœ“ Syntax OK" : "",
257
+ stderr: check.stderr,
258
+ index: cmdIdx,
259
+ isError: check.code !== 0
260
+ });
261
+
262
+ if (check.code === 0) {
263
+ if (attempt === 1) send("status", { text: `βœ… Syntax OK` });
264
+ else send("status", { text: `βœ… Syntax fixed on attempt ${attempt}` });
265
+ return true;
266
+ }
267
+
268
+ send("status", { text: `⚠️ Syntax error (attempt ${attempt}/${maxAttempts}) β€” fixing...` });
269
+
270
+ const absPath = path.join(HOME_DIR, filePath);
271
+ const broken = fs.readFileSync(absPath, "utf8");
272
+
273
+ const fixedRaw = await callAI(
274
+ `You are Zoro - a precise JS developer. Fix the syntax error. Return ONLY valid JSON with no extra text: {"content":"fixed file here","fix":"what was wrong"}`,
275
+ `File: ${filePath}\nError:\n${check.stderr}\n\nFile content:\n${broken}`
276
+ );
277
+
278
+ const fixed = parseJSON(fixedRaw);
279
+ if (fixed?.content) {
280
+ fs.writeFileSync(absPath, fixed.content, "utf8");
281
+ send("status", { text: `πŸ”¨ Applied fix: ${fixed.fix || "syntax correction"}` });
282
+ } else {
283
+ send("status", { text: `⚠️ Could not auto-fix on attempt ${attempt}` });
284
+ break;
285
+ }
286
+ }
287
+ return false;
288
+ }
289
+
290
+ // ── MAIN CHAT ENDPOINT ─────────────────────────────────────────────────────
291
+ app.post("/api/chat", async (req, res) => {
292
+ res.setHeader("Content-Type", "text/event-stream");
293
+ res.setHeader("Cache-Control", "no-cache");
294
+ res.setHeader("Connection", "keep-alive");
295
+
296
+ const send = (type, data) => {
297
+ try { res.write(`data: ${JSON.stringify({ type, ...data })}\n\n`); } catch {}
298
+ };
299
+
300
+ const { message, currentFile } = req.body;
301
+
302
+ try {
303
+ let filePath = currentFile || "index.js";
304
+ const absPath = path.join(HOME_DIR, filePath);
305
+ let fileContent = fs.existsSync(absPath) ? fs.readFileSync(absPath, "utf8") : "";
306
+ const hasFile = fileContent.length > 0;
307
+ const fileLines = fileContent.split("\n").length;
308
+
309
+ // ══════════════════════════════════════════════════════════════════════
310
+ // CASE 1: No file β†’ create from scratch
311
+ // ══════════════════════════════════════════════════════════════════════
312
+ if (!hasFile) {
313
+ send("status", { text: "🧠 Planning what to build..." });
314
+
315
+ const aiRaw = await callAI(
316
+ `You are Zoro from One Piece - a sharp, direct developer AI.
317
+ Create the code the user asks for. First plan tasks, then generate the complete file.
318
+ Return ONLY valid JSON (no markdown fences, no backticks, no extra text):
319
+ {
320
+ "filename": "index.js",
321
+ "description": "what this file is",
322
+ "tasks": [{"task": "what you did", "done": true}],
323
+ "content": "complete file content",
324
+ "status": "Done - what was built"
325
+ }`,
326
+ message
327
+ );
328
+
329
+ let parsed = parseJSON(aiRaw);
330
+ if (!parsed?.content) {
331
+ parsed = {
332
+ filename: "index.js",
333
+ content: aiRaw,
334
+ description: "Generated",
335
+ tasks: [{ task: "Generate file from request", done: true }],
336
+ status: "Created file",
337
+ };
338
+ }
339
+
340
+ send("status", { text: `πŸ“‹ Plan ready β€” ${parsed.tasks?.length || 1} tasks` });
341
+ send("task_update", { tasks: parsed.tasks || [] });
342
+
343
+ // Syntax check generated code
344
+ send("status", { text: "πŸ” Checking generated code..." });
345
+ const tmpName = "__zoro_new_check.js";
346
+ const tmpPath = path.join(HOME_DIR, tmpName);
347
+ fs.writeFileSync(tmpPath, parsed.content, "utf8");
348
+
349
+ const syntaxOk = await syntaxFixLoop(tmpName, send, 3);
350
+ // Read back (may have been fixed)
351
+ parsed.content = fs.readFileSync(tmpPath, "utf8");
352
+ fs.unlinkSync(tmpPath);
353
+
354
+ send("status", { text: `πŸ’Ύ Saving ${parsed.filename}...` });
355
+ const savePath = path.join(HOME_DIR, parsed.filename);
356
+ fs.mkdirSync(path.dirname(savePath), { recursive: true });
357
+ fs.writeFileSync(savePath, parsed.content, "utf8");
358
+
359
+ // Final verify on saved path
360
+ const finalCheck = await runCmd(`node --check ${parsed.filename}`);
361
+ send("command", { cmd: `node --check ${parsed.filename}`, index: 0 });
362
+ send("command_result", {
363
+ cmd: `node --check ${parsed.filename}`,
364
+ stdout: finalCheck.code === 0 ? "βœ“ Syntax OK β€” file saved!" : "",
365
+ stderr: finalCheck.stderr,
366
+ index: 0,
367
+ isError: finalCheck.code !== 0
368
+ });
369
+
370
+ updateTaskLog(parsed.tasks || []);
371
+ send("file_created", { filename: parsed.filename, content: parsed.content, description: parsed.description });
372
+ send("message", { text: `βœ… Created \`${parsed.filename}\` β€” ${parsed.description}` });
373
+ send("tree_update", {});
374
+ res.write("data: [DONE]\n\n");
375
+ res.end();
376
+ return;
377
+ }
378
+
379
+ // ══════════════════════════════════════════════════════════════════════
380
+ // CASE 2: File exists β€” analyze + command loop + edit/answer
381
+ // ══════════════════════════════════════════════════════════════════════
382
+ send("status", { text: "πŸ—ΊοΈ Analyzing request..." });
383
+
384
+ const smartCtx = getSmartContext(fileContent, message);
385
+ const existingTasks = readTaskLog();
386
+ const pendingTasks = existingTasks.filter(t => !t.done);
387
+
388
+ const masterPlanRaw = await callAI(
389
+ `You are Zoro - a razor-sharp developer AI agent operating on code files.
390
+ You use shell commands to explore, understand, then modify code.
391
+
392
+ ${COMMANDS_REFERENCE}
393
+
394
+ WORKFLOW FOR EDITS:
395
+ 1. EXPLORE: grep for existing patterns similar to what user wants
396
+ 2. LOCATE: find exact line numbers (grep -n, wc -l, sed -n to read sections)
397
+ 3. UNDERSTAND: read context around insertion points (grep -C5)
398
+ 4. CHECK: node --check before any edit
399
+ 5. EDIT: surgical sed -i on specific lines
400
+ 6. VERIFY: node --check after edit
401
+
402
+ WORKFLOW FOR QUERIES:
403
+ 1. grep for relevant functions/patterns
404
+ 2. read surrounding context
405
+ 3. answer precisely
406
+
407
+ File: ${filePath} (${fileLines} lines)
408
+ Current content preview:
409
+ ${smartCtx.substring(0, 2500)}
410
+
411
+ ${pendingTasks.length > 0 ? `UNFINISHED TASKS FROM LAST SESSION:\n${pendingTasks.map(t => ` ⬜ ${t.task}`).join("\n")}\n` : ""}
412
+
413
+ Respond ONLY with valid JSON (no markdown):
414
+ {
415
+ "task_type": "query" | "edit" | "create",
416
+ "status": "brief description shown to user",
417
+ "tasks": [{"task": "subtask name", "done": false}],
418
+ "commands": ["cmd1", "cmd2"],
419
+ "reasoning": "why these commands in this order"
420
+ }`,
421
+ message
422
+ );
423
+
424
+ let plan = parseJSON(masterPlanRaw);
425
+ if (!plan) plan = { task_type: "query", status: "Analyzing...", tasks: [], commands: [], reasoning: "" };
426
+
427
+ send("status", { text: `⚑ ${plan.status || "Processing..."}` });
428
+ if (plan.tasks?.length > 0) send("task_update", { tasks: plan.tasks });
429
+
430
+ // Run all planned commands
431
+ const commandResults = [];
432
+ for (let i = 0; i < (plan.commands || []).length; i++) {
433
+ const cmd = plan.commands[i];
434
+ send("status", { text: `πŸ”§ [${i + 1}/${plan.commands.length}] \`${cmd}\`` });
435
+ send("command", { cmd, index: i });
436
+
437
+ const result = await runCmd(cmd);
438
+ commandResults.push({ cmd, ...result });
439
+
440
+ send("command_result", {
441
+ cmd,
442
+ stdout: result.stdout.substring(0, 1000),
443
+ stderr: result.stderr.substring(0, 500),
444
+ index: i,
445
+ isError: result.code !== 0 && !!result.stderr
446
+ });
447
+
448
+ // If node --check failed here, enter fix loop immediately
449
+ if (cmd.includes("node --check") && result.code !== 0 && result.stderr) {
450
+ const targetFile = cmd.split(/\s+/).pop();
451
+ send("status", { text: `⚠️ Syntax error found in ${targetFile} β€” entering fix loop...` });
452
+ await syntaxFixLoop(targetFile, send, 3);
453
+ // Re-read file after fixes
454
+ fileContent = fs.existsSync(absPath) ? fs.readFileSync(absPath, "utf8") : fileContent;
455
+ }
456
+ }
457
+
458
+ // Re-read file (sed -i may have modified it)
459
+ fileContent = fs.existsSync(absPath) ? fs.readFileSync(absPath, "utf8") : fileContent;
460
+
461
+ const resultsText = commandResults
462
+ .map(r => `$ ${r.cmd}\n${r.stdout.substring(0, 600)}${r.stderr ? `\nSTDERR: ${r.stderr.substring(0, 300)}` : ""}`)
463
+ .join("\n\n");
464
+
465
+ // ── Final step: synthesize answer or produce edited file ───────────────
466
+ send("status", { text: "🧠 Synthesizing..." });
467
+
468
+ if (plan.task_type === "query") {
469
+ const ansRaw = await callAI(
470
+ `You are Zoro from One Piece - direct, precise, no-nonsense developer.
471
+ Answer the user's question based on file content and command results.
472
+ Return ONLY valid JSON: {"answer": "clear precise answer", "status": "Done"}`,
473
+ `Question: "${message}"\nFile: ${filePath}\nResults:\n${resultsText}\nContext:\n${smartCtx.substring(0, 2000)}`
474
+ );
475
+ const ans = parseJSON(ansRaw);
476
+ send("message", { text: ans?.answer || ansRaw || "No answer generated." });
477
+
478
+ } else {
479
+ // Edit or create β€” produce full updated file
480
+ const hadSedEdits = commandResults.some(r => r.cmd.startsWith("sed -i") && r.code === 0);
481
+
482
+ if (hadSedEdits) {
483
+ // File was already modified by sed β€” run fix loop, then report
484
+ send("status", { text: "πŸ” Verifying sed edits..." });
485
+ await syntaxFixLoop(filePath, send, 3);
486
+ fileContent = fs.readFileSync(absPath, "utf8");
487
+
488
+ const sumRaw = await callAI(
489
+ `You are Zoro. Summarize what changed in the file. Return ONLY JSON: {"summary": "what changed", "tasks_completed": ["t1", "t2"]}`,
490
+ `User asked: ${message}\nCommands:\n${resultsText}`
491
+ );
492
+ const sum = parseJSON(sumRaw);
493
+
494
+ const allTasks = [
495
+ ...existingTasks.map(t => ({ ...t, done: true })),
496
+ ...(plan.tasks || []).map(t => ({ ...t, done: true }))
497
+ ];
498
+ updateTaskLog(allTasks);
499
+ send("task_update", { tasks: allTasks });
500
+
501
+ send("file_updated", { filename: filePath, content: fileContent, description: sum?.summary || "Updated" });
502
+ send("message", { text: `βœ… ${sum?.summary || "Changes applied via sed"}` });
503
+
504
+ } else {
505
+ // AI produces the full new file content
506
+ const editRaw = await callAI(
507
+ `You are Zoro - a precise developer AI.
508
+ You have explored the file with commands. Now produce the complete updated file.
509
+
510
+ KEY RULES:
511
+ - Study command results carefully β€” they show existing code patterns
512
+ - Match the EXACT style, indentation, naming conventions of existing code
513
+ - For new features/commands: model them exactly after existing similar ones
514
+ - Make ONLY the necessary changes, preserve everything else
515
+ - The output must be a complete, working file
516
+
517
+ Return ONLY valid JSON (no markdown):
518
+ {
519
+ "content": "COMPLETE updated file content here",
520
+ "description": "what changed",
521
+ "tasks_completed": ["task1", "task2"],
522
+ "status": "Done β€” what was accomplished"
523
+ }`,
524
+ `User request: "${message}"
525
+ File: ${filePath} (${fileLines} lines)
526
+ Current file:
527
+ ${fileContent.substring(0, 5000)}
528
+ Command exploration results:
529
+ ${resultsText}
530
+ Produce the complete updated file.`
531
+ );
532
+
533
+ let editParsed = parseJSON(editRaw);
534
+ if (!editParsed?.content) {
535
+ editParsed = { content: fileContent, description: "No changes", status: "Done", tasks_completed: [] };
536
+ }
537
+
538
+ // Back up before saving
539
+ backupFile(filePath);
540
+
541
+ // Pre-save syntax check
542
+ send("status", { text: "πŸ” Checking new content before saving..." });
543
+ const tmpName = `__zoro_presave_${Date.now()}.js`;
544
+ const tmpPath = path.join(HOME_DIR, tmpName);
545
+ fs.writeFileSync(tmpPath, editParsed.content, "utf8");
546
+
547
+ const preOk = await syntaxFixLoop(tmpName, send, 3);
548
+ editParsed.content = fs.readFileSync(tmpPath, "utf8");
549
+ fs.unlinkSync(tmpPath);
550
+
551
+ // Save
552
+ fs.writeFileSync(absPath, editParsed.content, "utf8");
553
+ send("status", { text: `πŸ’Ύ Saved ${filePath}` });
554
+
555
+ // Post-save verify
556
+ await syntaxFixLoop(filePath, send, 3);
557
+ fileContent = fs.readFileSync(absPath, "utf8");
558
+
559
+ // Update task log
560
+ const allTasks = [
561
+ ...existingTasks.map(t => ({ ...t, done: true })),
562
+ ...(plan.tasks || []).map(t => ({ ...t, done: true, note: "done this session" }))
563
+ ];
564
+ updateTaskLog(allTasks);
565
+ send("task_update", { tasks: allTasks });
566
+
567
+ send("file_updated", { filename: filePath, content: fileContent, description: editParsed.description });
568
+ send("message", { text: `βœ… ${editParsed.status || editParsed.description}` });
569
+ send("tree_update", {});
570
+ }
571
+ }
572
+
573
+ // Check for unfinished tasks
574
+ const latestTasks = readTaskLog();
575
+ const stillPending = latestTasks.filter(t => !t.done);
576
+ if (stillPending.length > 0) {
577
+ send("status", { text: `πŸ“‹ ${stillPending.length} tasks still pending` });
578
+ send("pending_tasks", { tasks: stillPending, count: stillPending.length });
579
+ }
580
+
581
+ res.write("data: [DONE]\n\n");
582
+ res.end();
583
+
584
+ } catch (err) {
585
+ console.error("Chat error:", err);
586
+ send("error", { text: err.message });
587
+ res.write("data: [DONE]\n\n");
588
+ res.end();
589
+ }
590
+ });
591
+
592
+ app.listen(PORT, () => {
593
+ console.log(`\nπŸ—‘οΈ Zoro Dev Server β†’ http://localhost:${PORT}`);
594
+ console.log(`πŸ“ Home dir: ${HOME_DIR}\n`);
595
+ });