Reaperxxxx commited on
Commit
bbc4b57
Β·
verified Β·
1 Parent(s): 70e5671

Update server.js

Browse files
Files changed (1) hide show
  1. server.js +389 -212
server.js CHANGED
@@ -32,6 +32,16 @@ const MODELS = {
32
  return data.message || data.text || "";
33
  }
34
  },
 
 
 
 
 
 
 
 
 
 
35
  "cryo3": {
36
  name: "Cryo 3",
37
  label: "cryo3",
@@ -62,66 +72,41 @@ const ALLOWED_CMDS = [
62
  ];
63
 
64
  const COMMANDS_REFERENCE = `
65
- === SHELL COMMANDS AVAILABLE (cwd = /home) ===
66
 
67
- FILE EXPLORATION:
68
- ls -l List files & directories
69
- find . -name "*.js" Find JS files recursively
70
- find . -name "*.json" Find config files
71
- file filename Show file type
72
- stat filename File metadata (size, dates)
73
 
74
- READING CONTENT:
75
- cat file Read entire file
76
  head -n N file First N lines
77
  tail -n N file Last N lines
78
- sed -n 'X,Yp' file Lines X to Y (surgical)
79
- head -c N file First N bytes
80
- grep -n "pattern" file Lines matching pattern (with line numbers)
81
- grep -A5 "pattern" file Pattern + 5 lines after
82
- grep -B3 "pattern" file Pattern + 3 lines before
83
- grep -C3 "pattern" file Pattern + 3 lines context (before+after)
84
- grep -rn "pattern" . Recursive search all files
85
-
86
- COUNTING / STATS:
87
- wc -l file Count lines
88
- grep -c "pattern" file Count occurrences
89
- awk 'END{print NR}' file Alternative line count
90
 
91
- EDITING (surgical, safe):
92
  sed -i 's/old/new/g' file Replace ALL occurrences
93
- sed -i '12s/old/new/' file Replace on line 12 ONLY
94
- sed -i 'X,Ys/old/new/g' file Replace in lines X-Y
95
- awk '/pattern/' file Extract matching lines
96
- cut -d',' -f2 file Extract 2nd CSV column
97
- tr 'a-z' 'A-Z' Transform chars
98
-
99
- COMPARING:
100
- diff old.js new.js Show differences between files
101
- patch file < patchfile Apply a patch file
102
 
103
- SYNTAX / ERROR CHECKING:
104
- node --check file.js JavaScript syntax validation
105
- npm test Run project tests
106
 
107
- PROJECT INFO:
108
- cat package.json Show project dependencies
109
- npm run scriptname Run npm scripts
110
-
111
- UTILITIES:
112
- echo "text" Print text
113
- sort file Sort lines alphabetically
114
- uniq file Remove duplicate lines
115
 
116
  === AGENT RULES ===
117
- 1. ALWAYS run "node --check file.js" BEFORE any edit
118
- 2. ALWAYS run "node --check file.js" AFTER any edit
119
- 3. Use grep -C3 to understand context BEFORE inserting new code
120
- 4. Match existing code style - grep for similar patterns first
121
- 5. Use sed -n 'X,Yp' to read specific sections before modifying them
122
- 6. Back up important sections mentally before sed -i changes
123
- 7. For adding new features: grep for existing similar features first
124
- ===============================================
125
  `;
126
 
127
  // ── Multer ─────────────────────────────────────────────────────────────────
@@ -131,15 +116,70 @@ const storage = multer.diskStorage({
131
  });
132
  const upload = multer({ storage });
133
 
134
- // ── Helpers ────────────────────────────────────────────────────────────────
 
135
  function parseJSON(raw) {
136
  if (!raw) return null;
137
- let s = raw.replace(/```json\s*/gi, "").replace(/```\s*/g, "").trim();
138
- const m = s.match(/\{[\s\S]*\}/);
139
- if (m) s = m[0];
140
- try { return JSON.parse(s); } catch { return null; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
  }
142
 
 
 
 
 
 
 
 
 
 
 
143
  function runCmd(command) {
144
  return new Promise(resolve => {
145
  const name = command.trim().split(/\s+/)[0];
@@ -152,6 +192,7 @@ function runCmd(command) {
152
  });
153
  }
154
 
 
155
  function getSmartContext(content, query, maxLines = 120) {
156
  const lines = content.split("\n");
157
  if (lines.length <= maxLines) return content;
@@ -172,16 +213,14 @@ function getSmartContext(content, query, maxLines = 120) {
172
  return head + middle + tail;
173
  }
174
 
175
- // ── Versioning ─────────────────────────────────────────────────────────────
176
  function readVersions() {
177
  if (!fs.existsSync(VERSIONS_FILE)) return {};
178
  try { return JSON.parse(fs.readFileSync(VERSIONS_FILE, "utf8")); } catch { return {}; }
179
  }
180
-
181
  function saveVersions(v) {
182
  fs.writeFileSync(VERSIONS_FILE, JSON.stringify(v, null, 2), "utf8");
183
  }
184
-
185
  function versionFile(filePath) {
186
  const src = path.join(HOME_DIR, filePath);
187
  if (!fs.existsSync(src)) return;
@@ -200,16 +239,14 @@ function versionFile(filePath) {
200
  return vName;
201
  }
202
 
203
- // ── Task Management ────────────────────────────────────────────────────────
204
  function readTasks() {
205
  if (!fs.existsSync(TASK_FILE)) return [];
206
  try { return JSON.parse(fs.readFileSync(TASK_FILE, "utf8")); } catch { return []; }
207
  }
208
-
209
  function saveTasks(tasks) {
210
  fs.writeFileSync(TASK_FILE, JSON.stringify(tasks, null, 2), "utf8");
211
  }
212
-
213
  function updateTasks(newTasks) {
214
  const existing = readTasks();
215
  const merged = [...existing, ...newTasks.filter(t => !existing.find(e => e.task === t.task))];
@@ -217,7 +254,7 @@ function updateTasks(newTasks) {
217
  return merged;
218
  }
219
 
220
- // ── Command History ────────────────────────────────────────────────────────
221
  function appendCmdHistory(entries) {
222
  let hist = [];
223
  if (fs.existsSync(CMD_HISTORY_FILE)) {
@@ -228,49 +265,42 @@ function appendCmdHistory(entries) {
228
  fs.writeFileSync(CMD_HISTORY_FILE, JSON.stringify(hist, null, 2), "utf8");
229
  }
230
 
231
- // ── Project Metadata ───────────────────────────────────────────────────────
232
  function readMeta() {
233
  if (!fs.existsSync(META_FILE)) return { created_at: new Date().toISOString(), languages: [], last_ai_edit: null };
234
  try { return JSON.parse(fs.readFileSync(META_FILE, "utf8")); } catch { return {}; }
235
  }
236
-
237
  function updateMeta(patch) {
238
  const meta = { ...readMeta(), ...patch, updated_at: new Date().toISOString() };
239
  fs.writeFileSync(META_FILE, JSON.stringify(meta, null, 2), "utf8");
240
  return meta;
241
  }
242
 
243
- // ── Web Search Detection ───────────────────────────────────────────────────
244
  const SEARCH_TRIGGERS = [
245
  /\b(latest|recent|current|today|now|2024|2025|2026|news|trending|new release|just released)\b/i,
246
  /\b(who is|what is the price|when did|how much does|is .* still|does .* exist)\b/i,
247
  /\b(search|look up|find out|google|web|online|internet)\b/i,
248
  /\b(weather|stock|score|result|update|announce|launch)\b/i,
249
  ];
250
-
251
  function needsWebSearch(msg) {
252
  return SEARCH_TRIGGERS.some(r => r.test(msg));
253
  }
254
-
255
  async function webSearch(query) {
256
  const short = query.slice(0, 200);
257
  const url = `https://apis.prexzyvilla.site/ai/gpt-5?text=${encodeURIComponent(short)}`;
258
  const { data } = await axios.get(url, { timeout: 20000 });
259
- return {
260
- answer: data.text || "",
261
- citations: data.citations || [],
262
- model: data.model || "gpt-5"
263
- };
264
  }
265
 
266
  // ── callAI wrapper ─────────────────────────────────────────────────────────
267
- async function callAI(system, query, modelKey = "cryo4") {
268
- const model = MODELS[modelKey] || MODELS["cryo4"];
269
  const prompt = system ? `${system}\n\n${query}` : query;
270
  return model.call(prompt);
271
  }
272
 
273
- // ── Syntax fix loop ────────────────────────────────────────────────────────
274
  async function syntaxFixLoop(filePath, send, modelKey, maxAttempts = 3) {
275
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
276
  const check = await runCmd(`node --check ${filePath}`);
@@ -288,19 +318,202 @@ async function syntaxFixLoop(filePath, send, modelKey, maxAttempts = 3) {
288
  const absPath = path.join(HOME_DIR, filePath);
289
  const broken = fs.readFileSync(absPath, "utf8");
290
  const fixedRaw = await callAI(
291
- `Fix JS syntax. Return ONLY JSON: {"content":"fixed file","fix":"what changed"}`,
292
- `Error:\n${check.stderr}\n\nFile:\n${broken.slice(0, 3000)}`,
293
  modelKey
294
  );
295
  const fixed = parseJSON(fixedRaw);
296
  if (fixed?.content) {
297
  fs.writeFileSync(absPath, fixed.content, "utf8");
298
  send("status", { text: `Fix applied: ${fixed.fix || "syntax correction"}` });
299
- } else break;
 
 
 
 
 
 
 
300
  }
301
  return false;
302
  }
303
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
304
  // ── FILE TREE ──────────────────────────────────────────────────────────────
305
  app.get("/api/tree", (req, res) => {
306
  const buildTree = (dir, base = HOME_DIR) => {
@@ -355,9 +568,7 @@ app.get("/api/download", (req, res) => {
355
  res.download(fp);
356
  });
357
 
358
- app.get("/api/tasks", (req, res) => {
359
- res.json({ tasks: readTasks() });
360
- });
361
 
362
  app.get("/api/history", (req, res) => {
363
  if (!fs.existsSync(CMD_HISTORY_FILE)) return res.json({ history: [] });
@@ -407,10 +618,10 @@ app.post("/api/chat", async (req, res) => {
407
  try { res.write(`data: ${JSON.stringify({ type, ...data })}\n\n`); } catch {}
408
  };
409
 
410
- const { message, currentFile, model: modelKey = "cryo4" } = req.body;
411
 
412
  try {
413
- // ── Web search detection ───────────────────────────────────────────────
414
  if (needsWebSearch(message) && !currentFile) {
415
  send("status", { text: "Searching the web..." });
416
  send("web_search", { query: message });
@@ -430,40 +641,61 @@ app.post("/api/chat", async (req, res) => {
430
  const absPath = path.join(HOME_DIR, filePath);
431
  let fileContent = fs.existsSync(absPath) ? fs.readFileSync(absPath, "utf8") : "";
432
  const hasFile = fileContent.length > 0;
433
- const fileLines = fileContent.split("\n").length;
434
- const meta = readMeta();
435
  const existingTasks = readTasks();
436
  const pendingTasks = existingTasks.filter(t => !t.done);
437
 
438
- // ── No file: create from scratch ───────────────────────────────────────
439
  if (!hasFile) {
440
  send("status", { text: "Planning what to build..." });
441
 
442
- const aiRaw = await callAI(
443
- `You are Cryo - a precise developer AI. Create the code requested.
444
- Return ONLY valid JSON (no markdown):
445
- {"filename":"index.js","description":"what this is","tasks":[{"task":"what you did","done":true}],"content":"complete file content","status":"Done - what was built"}`,
446
- message,
447
- modelKey
448
- );
449
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
450
  let parsed = parseJSON(aiRaw);
 
 
451
  if (!parsed?.content) {
 
 
 
452
  parsed = {
453
- filename: "index.js", content: aiRaw,
454
- description: "Generated", tasks: [{ task: "Generate file", done: true }], status: "Created"
 
 
 
455
  };
456
  }
457
 
458
- send("status", { text: `Plan ready β€” ${parsed.tasks?.length || 1} tasks` });
459
  send("task_update", { tasks: parsed.tasks || [] });
460
  send("status", { text: "Checking generated code..." });
461
 
462
  const tmpName = `__cryo_new_${Date.now()}.js`;
463
  const tmpPath = path.join(HOME_DIR, tmpName);
464
  fs.writeFileSync(tmpPath, parsed.content, "utf8");
465
- await syntaxFixLoop(tmpName, send, modelKey, 3);
466
- parsed.content = fs.readFileSync(tmpPath, "utf8");
 
 
 
 
467
  fs.unlinkSync(tmpPath);
468
 
469
  send("status", { text: `Saving ${parsed.filename}...` });
@@ -471,109 +703,44 @@ Return ONLY valid JSON (no markdown):
471
  fs.mkdirSync(path.dirname(savePath), { recursive: true });
472
  fs.writeFileSync(savePath, parsed.content, "utf8");
473
 
474
- const finalCheck = await runCmd(`node --check ${parsed.filename}`);
475
- send("command", { cmd: `node --check ${parsed.filename}`, index: 0 });
476
- send("command_result", {
477
- cmd: `node --check ${parsed.filename}`,
478
- stdout: finalCheck.code === 0 ? "Syntax OK β€” file saved!" : "",
479
- stderr: finalCheck.stderr, index: 0, isError: finalCheck.code !== 0
480
- });
 
 
481
 
482
  updateTasks(parsed.tasks || []);
483
  updateMeta({ last_ai_edit: new Date().toISOString(), languages: [parsed.filename.split(".").pop()] });
484
  send("file_created", { filename: parsed.filename, content: parsed.content, description: parsed.description });
485
- send("message", { text: `Created \`${parsed.filename}\` β€” ${parsed.description}` });
486
  send("tree_update", {});
487
  res.write("data: [DONE]\n\n");
488
  res.end();
489
  return;
490
  }
491
 
492
- // ── File exists: analyze + command loop + edit/answer ──────────────────
493
- send("status", { text: "Analyzing request..." });
494
- const smartCtx = getSmartContext(fileContent, message);
495
-
496
- const masterPlanRaw = await callAI(
497
- `You are Cryo - a razor-sharp developer AI agent operating on code files.
498
- You use shell commands to explore, understand, then modify code.
499
-
500
- ${COMMANDS_REFERENCE}
501
-
502
- WORKFLOW FOR EDITS:
503
- 1. EXPLORE: grep for existing patterns similar to what user wants
504
- 2. LOCATE: find exact line numbers (grep -n, wc -l, sed -n to read sections)
505
- 3. UNDERSTAND: read context around insertion points (grep -C5)
506
- 4. CHECK: node --check before any edit
507
- 5. EDIT: surgical sed -i on specific lines
508
- 6. VERIFY: node --check after edit
509
-
510
- WORKFLOW FOR QUERIES:
511
- 1. grep for relevant functions/patterns
512
- 2. read surrounding context
513
- 3. answer precisely
514
 
515
- File: ${filePath} (${fileLines} lines)
516
- Current content preview:
517
- ${smartCtx.substring(0, 2500)}
518
-
519
- ${pendingTasks.length > 0 ? `UNFINISHED TASKS FROM LAST SESSION:\n${pendingTasks.map(t => ` - ${t.task}`).join("\n")}\n` : ""}
520
-
521
- Respond ONLY with valid JSON (no markdown):
522
- {
523
- "task_type": "query" | "edit" | "create",
524
- "status": "brief description shown to user",
525
- "tasks": [{"task": "subtask name", "done": false}],
526
- "commands": ["cmd1", "cmd2"],
527
- "reasoning": "why these commands in this order"
528
- }`,
529
- message,
530
- modelKey
531
- );
532
-
533
- let plan = parseJSON(masterPlanRaw);
534
- if (!plan) plan = { task_type: "query", status: "Analyzing...", tasks: [], commands: [], reasoning: "" };
535
-
536
- send("status", { text: plan.status || "Processing..." });
537
- if (plan.tasks?.length > 0) send("task_update", { tasks: plan.tasks });
538
-
539
- // Run all planned commands
540
- const commandResults = [];
541
- const historyEntries = [];
542
-
543
- for (let i = 0; i < (plan.commands || []).length; i++) {
544
- const cmd = plan.commands[i];
545
- send("status", { text: `[${i + 1}/${plan.commands.length}] ${cmd}` });
546
- send("command", { cmd, index: i });
547
-
548
- const result = await runCmd(cmd);
549
- commandResults.push({ cmd, ...result });
550
- historyEntries.push({ cmd, stdout: result.stdout.slice(0, 500), stderr: result.stderr.slice(0, 200), code: result.code, ts: Date.now() });
551
-
552
- send("command_result", {
553
- cmd, stdout: result.stdout.substring(0, 1000),
554
- stderr: result.stderr.substring(0, 500),
555
- index: i, isError: result.code !== 0 && !!result.stderr
556
- });
557
-
558
- if (cmd.includes("node --check") && result.code !== 0 && result.stderr) {
559
- const targetFile = cmd.split(/\s+/).pop();
560
- send("status", { text: `Syntax error in ${targetFile} β€” entering fix loop...` });
561
- await syntaxFixLoop(targetFile, send, modelKey, 3);
562
- fileContent = fs.existsSync(absPath) ? fs.readFileSync(absPath, "utf8") : fileContent;
563
- }
564
- }
565
 
566
- appendCmdHistory(historyEntries);
567
- fileContent = fs.existsSync(absPath) ? fs.readFileSync(absPath, "utf8") : fileContent;
568
 
569
- const resultsText = commandResults
570
- .map(r => `$ ${r.cmd}\n${r.stdout.substring(0, 500)}${r.stderr ? `\nSTDERR: ${r.stderr.substring(0, 200)}` : ""}`)
571
  .join("\n\n");
572
 
573
  send("status", { text: "Synthesizing..." });
574
 
 
575
  if (plan.task_type === "query") {
576
- // Check if answer might need web info
577
  if (needsWebSearch(message)) {
578
  send("web_search", { query: message });
579
  try {
@@ -582,34 +749,35 @@ Respond ONLY with valid JSON (no markdown):
582
  send("message", { text: wsResult.answer });
583
  } catch {
584
  const ansRaw = await callAI(
585
- `You are Cryo. Answer based on file content and command results. Return ONLY JSON: {"answer":"clear answer","status":"Done"}`,
586
- `Question: "${message}"\nResults:\n${resultsText}\nContext:\n${smartCtx.substring(0, 1500)}`,
587
  modelKey
588
  );
589
  const ans = parseJSON(ansRaw);
590
- send("message", { text: ans?.answer || ansRaw || "No answer generated." });
591
  }
592
  } else {
593
  const ansRaw = await callAI(
594
- `You are Cryo. Answer based on file content and command results. Return ONLY JSON: {"answer":"clear answer","status":"Done"}`,
595
- `Question: "${message}"\nResults:\n${resultsText}\nContext:\n${smartCtx.substring(0, 1500)}`,
596
  modelKey
597
  );
598
  const ans = parseJSON(ansRaw);
599
- send("message", { text: ans?.answer || ansRaw || "No answer generated." });
600
  }
601
 
602
  } else {
603
- const hadSedEdits = commandResults.some(r => r.cmd.startsWith("sed -i") && r.code === 0);
604
-
605
  if (hadSedEdits) {
606
- send("status", { text: "Verifying sed edits..." });
607
- await syntaxFixLoop(filePath, send, modelKey, 3);
608
- fileContent = fs.readFileSync(absPath, "utf8");
 
 
609
 
610
  const sumRaw = await callAI(
611
- `Summarize file changes. Return ONLY JSON: {"summary":"what changed","tasks_completed":["t1"]}`,
612
- `User: ${message}\nCommands:\n${resultsText}`,
613
  modelKey
614
  );
615
  const sum = parseJSON(sumRaw);
@@ -619,45 +787,54 @@ Respond ONLY with valid JSON (no markdown):
619
  versionFile(filePath);
620
  updateMeta({ last_ai_edit: new Date().toISOString() });
621
  send("file_updated", { filename: filePath, content: fileContent, description: sum?.summary || "Updated" });
622
- send("message", { text: sum?.summary || "Changes applied via sed" });
623
 
624
  } else {
 
625
  const editRaw = await callAI(
626
- `You are Cryo - a precise developer AI. Produce the complete updated file.
627
- RULES: match existing style, preserve everything not changed, output must be complete working file.
628
- Return ONLY valid JSON:
629
- {"content":"COMPLETE file content","description":"what changed","tasks_completed":["t1"],"status":"Done - what was accomplished"}`,
630
- `User request: "${message}"\nFile: ${filePath} (${fileLines} lines)\nCurrent:\n${fileContent.substring(0, 4000)}\nCommand results:\n${resultsText}\nProduce complete updated file.`,
631
  modelKey
632
  );
633
 
634
  let editParsed = parseJSON(editRaw);
 
 
635
  if (!editParsed?.content) {
636
- editParsed = { content: fileContent, description: "No changes", status: "Done", tasks_completed: [] };
 
 
 
637
  }
638
 
639
- // Add Cryo inline comments
640
- editParsed.content = editParsed.content;
641
-
642
  versionFile(filePath);
643
 
644
- send("status", { text: "Checking new content before saving..." });
 
645
  const tmpName = `__cryo_presave_${Date.now()}.js`;
646
  const tmpPath = path.join(HOME_DIR, tmpName);
647
  fs.writeFileSync(tmpPath, editParsed.content, "utf8");
648
- await syntaxFixLoop(tmpName, send, modelKey, 3);
649
- editParsed.content = fs.readFileSync(tmpPath, "utf8");
 
 
 
650
  fs.unlinkSync(tmpPath);
651
 
652
  fs.writeFileSync(absPath, editParsed.content, "utf8");
653
  send("status", { text: `Saved ${filePath}` });
654
 
655
- await syntaxFixLoop(filePath, send, modelKey, 3);
 
 
656
  fileContent = fs.readFileSync(absPath, "utf8");
657
 
658
  const allTasks = [
659
  ...existingTasks.map(t => ({ ...t, done: true })),
660
- ...(plan.tasks || []).map(t => ({ ...t, done: true, note: "done this session" }))
661
  ];
662
  saveTasks(allTasks);
663
  updateMeta({ last_ai_edit: new Date().toISOString() });
@@ -668,10 +845,10 @@ Return ONLY valid JSON:
668
  }
669
  }
670
 
 
671
  const latestTasks = readTasks();
672
  const stillPending = latestTasks.filter(t => !t.done);
673
  if (stillPending.length > 0) {
674
- send("status", { text: `${stillPending.length} tasks still pending` });
675
  send("pending_tasks", { tasks: stillPending, count: stillPending.length });
676
  }
677
 
 
32
  return data.message || data.text || "";
33
  }
34
  },
35
+ "cryo2": {
36
+ name: "Cryo 2",
37
+ label: "cryo2",
38
+ description: "Reliable & smart",
39
+ call: async (prompt) => {
40
+ const params = new URLSearchParams({ BK9: "You are Cryo, a precise developer AI. Always respond with valid JSON only, no markdown.", q: prompt, model: "compound-beta-mini" });
41
+ const { data } = await axios.get(`https://api.bk9.dev/ai/BK94?${params}`, { timeout: 30000 });
42
+ return data.BK9 || data.response || data.message || "";
43
+ }
44
+ },
45
  "cryo3": {
46
  name: "Cryo 3",
47
  label: "cryo3",
 
72
  ];
73
 
74
  const COMMANDS_REFERENCE = `
75
+ === SHELL COMMANDS (cwd = /home) ===
76
 
77
+ EXPLORE:
78
+ ls -l List files
79
+ find . -name "*.js" Find files
80
+ stat filename File metadata
 
 
81
 
82
+ READ:
83
+ cat file Entire file
84
  head -n N file First N lines
85
  tail -n N file Last N lines
86
+ sed -n 'X,Yp' file Lines X to Y
87
+ grep -n "pattern" file Lines matching (with numbers)
88
+ grep -C3 "pattern" file Match + 3 lines context
 
 
 
 
 
 
 
 
 
89
 
90
+ EDIT (surgical):
91
  sed -i 's/old/new/g' file Replace ALL occurrences
92
+ sed -i '12s/old/new/' file Replace on line 12 only
93
+ sed -i 'X,Ys/old/new/g' file Replace in line range X-Y
94
+ sed -i 'Xd' file Delete line X
95
+ sed -i 'X,Yd' file Delete lines X to Y
 
 
 
 
 
96
 
97
+ BACKUP:
98
+ cp file file.bak Backup before editing
 
99
 
100
+ CHECK:
101
+ node --check file.js JS syntax validation
102
+ wc -l file Count lines
 
 
 
 
 
103
 
104
  === AGENT RULES ===
105
+ 1. Always grep -n BEFORE editing to find exact line numbers
106
+ 2. Always node --check after any sed edit
107
+ 3. Use sed -n to read sections before modifying
108
+ 4. Back up file with cp before major edits
109
+ 5. Use grep -C3 to understand context before insertion
 
 
 
110
  `;
111
 
112
  // ── Multer ─────────────────────────────────────────────────────────────────
 
116
  });
117
  const upload = multer({ storage });
118
 
119
+ // ── ROBUST JSON PARSER ─────────────────────────────────────────────��───────
120
+ // Handles: plain text, markdown fenced, partial JSON, escaped newlines
121
  function parseJSON(raw) {
122
  if (!raw) return null;
123
+ let s = String(raw).trim();
124
+
125
+ // Strip markdown fences
126
+ s = s.replace(/^```(?:json)?\s*/i, "").replace(/\s*```\s*$/, "").trim();
127
+
128
+ // Try direct parse first
129
+ try { return JSON.parse(s); } catch {}
130
+
131
+ // Extract outermost {...} object
132
+ const start = s.indexOf("{");
133
+ const end = s.lastIndexOf("}");
134
+ if (start !== -1 && end !== -1 && end > start) {
135
+ try { return JSON.parse(s.slice(start, end + 1)); } catch {}
136
+ }
137
+
138
+ // Try to fix common issues: unescaped newlines in string values
139
+ try {
140
+ const fixed = s
141
+ .replace(/:\s*"([\s\S]*?)"/g, (m, v) => ': "' + v.replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t") + '"')
142
+ .replace(/,\s*}/g, "}").replace(/,\s*\]/g, "]");
143
+ const fStart = fixed.indexOf("{"), fEnd = fixed.lastIndexOf("}");
144
+ if (fStart !== -1 && fEnd !== -1) return JSON.parse(fixed.slice(fStart, fEnd + 1));
145
+ } catch {}
146
+
147
+ return null;
148
+ }
149
+
150
+ // ── EXTRACT COMMANDS FROM TEXT ─────────────────────────────────────────────
151
+ // When AI returns markdown/prose instead of JSON, pull shell commands from it
152
+ function extractCommandsFromText(text) {
153
+ const cmds = [];
154
+ // Match lines starting with $ or lines inside code blocks
155
+ const codeBlockRe = /```(?:bash|sh|shell)?\s*([\s\S]*?)```/gi;
156
+ let m;
157
+ while ((m = codeBlockRe.exec(text)) !== null) {
158
+ const lines = m[1].split("\n").map(l => l.replace(/^\$\s*/, "").trim()).filter(l => l && !l.startsWith("#"));
159
+ cmds.push(...lines);
160
+ }
161
+ // Also match lines starting with $ outside code blocks
162
+ const lines = text.split("\n");
163
+ for (const line of lines) {
164
+ const stripped = line.replace(/^\$\s*/, "").trim();
165
+ const firstWord = stripped.split(/\s+/)[0];
166
+ if (line.trim().startsWith("$") && ALLOWED_CMDS.includes(firstWord)) {
167
+ if (!cmds.includes(stripped)) cmds.push(stripped);
168
+ }
169
+ }
170
+ return cmds.filter(cmd => ALLOWED_CMDS.includes(cmd.split(/\s+/)[0]));
171
  }
172
 
173
+ // ── EXTRACT CONTENT FROM TEXT ──────────────────────────────────────────────
174
+ // Pull file content from markdown code block in plain-text AI response
175
+ function extractContentFromText(text) {
176
+ // Match ```js ... ``` or ```javascript ... ``` or ``` ... ```
177
+ const m = text.match(/```(?:js|javascript|typescript|ts|python|py|sh|bash)?\s*([\s\S]+?)```/i);
178
+ if (m) return m[1].trim();
179
+ return null;
180
+ }
181
+
182
+ // ── runCmd ─────────────────────────────────────────────────────────────────
183
  function runCmd(command) {
184
  return new Promise(resolve => {
185
  const name = command.trim().split(/\s+/)[0];
 
192
  });
193
  }
194
 
195
+ // ── SMART CONTEXT ──────────────────────────────────────────────────────────
196
  function getSmartContext(content, query, maxLines = 120) {
197
  const lines = content.split("\n");
198
  if (lines.length <= maxLines) return content;
 
213
  return head + middle + tail;
214
  }
215
 
216
+ // ── VERSIONING ─────────────────────────────────────────────────────────────
217
  function readVersions() {
218
  if (!fs.existsSync(VERSIONS_FILE)) return {};
219
  try { return JSON.parse(fs.readFileSync(VERSIONS_FILE, "utf8")); } catch { return {}; }
220
  }
 
221
  function saveVersions(v) {
222
  fs.writeFileSync(VERSIONS_FILE, JSON.stringify(v, null, 2), "utf8");
223
  }
 
224
  function versionFile(filePath) {
225
  const src = path.join(HOME_DIR, filePath);
226
  if (!fs.existsSync(src)) return;
 
239
  return vName;
240
  }
241
 
242
+ // ── TASK MANAGEMENT ────────────────────────────────────────────────────────
243
  function readTasks() {
244
  if (!fs.existsSync(TASK_FILE)) return [];
245
  try { return JSON.parse(fs.readFileSync(TASK_FILE, "utf8")); } catch { return []; }
246
  }
 
247
  function saveTasks(tasks) {
248
  fs.writeFileSync(TASK_FILE, JSON.stringify(tasks, null, 2), "utf8");
249
  }
 
250
  function updateTasks(newTasks) {
251
  const existing = readTasks();
252
  const merged = [...existing, ...newTasks.filter(t => !existing.find(e => e.task === t.task))];
 
254
  return merged;
255
  }
256
 
257
+ // ── COMMAND HISTORY ────────────────────────────────────────────────────────
258
  function appendCmdHistory(entries) {
259
  let hist = [];
260
  if (fs.existsSync(CMD_HISTORY_FILE)) {
 
265
  fs.writeFileSync(CMD_HISTORY_FILE, JSON.stringify(hist, null, 2), "utf8");
266
  }
267
 
268
+ // ── PROJECT METADATA ───────────────────────────────────────────────────────
269
  function readMeta() {
270
  if (!fs.existsSync(META_FILE)) return { created_at: new Date().toISOString(), languages: [], last_ai_edit: null };
271
  try { return JSON.parse(fs.readFileSync(META_FILE, "utf8")); } catch { return {}; }
272
  }
 
273
  function updateMeta(patch) {
274
  const meta = { ...readMeta(), ...patch, updated_at: new Date().toISOString() };
275
  fs.writeFileSync(META_FILE, JSON.stringify(meta, null, 2), "utf8");
276
  return meta;
277
  }
278
 
279
+ // ── WEB SEARCH DETECTION ───────────────────────────────────────────────────
280
  const SEARCH_TRIGGERS = [
281
  /\b(latest|recent|current|today|now|2024|2025|2026|news|trending|new release|just released)\b/i,
282
  /\b(who is|what is the price|when did|how much does|is .* still|does .* exist)\b/i,
283
  /\b(search|look up|find out|google|web|online|internet)\b/i,
284
  /\b(weather|stock|score|result|update|announce|launch)\b/i,
285
  ];
 
286
  function needsWebSearch(msg) {
287
  return SEARCH_TRIGGERS.some(r => r.test(msg));
288
  }
 
289
  async function webSearch(query) {
290
  const short = query.slice(0, 200);
291
  const url = `https://apis.prexzyvilla.site/ai/gpt-5?text=${encodeURIComponent(short)}`;
292
  const { data } = await axios.get(url, { timeout: 20000 });
293
+ return { answer: data.text || "", citations: data.citations || [], model: data.model || "gpt-5" };
 
 
 
 
294
  }
295
 
296
  // ── callAI wrapper ─────────────────────────────────────────────────────────
297
+ async function callAI(system, query, modelKey = "cryo2") {
298
+ const model = MODELS[modelKey] || MODELS["cryo2"];
299
  const prompt = system ? `${system}\n\n${query}` : query;
300
  return model.call(prompt);
301
  }
302
 
303
+ // ── SYNTAX FIX LOOP ────────────────────────────────────────────────────────
304
  async function syntaxFixLoop(filePath, send, modelKey, maxAttempts = 3) {
305
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
306
  const check = await runCmd(`node --check ${filePath}`);
 
318
  const absPath = path.join(HOME_DIR, filePath);
319
  const broken = fs.readFileSync(absPath, "utf8");
320
  const fixedRaw = await callAI(
321
+ `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>"}`,
322
+ `SYNTAX ERROR:\n${check.stderr}\n\nFILE CONTENT:\n${broken.slice(0, 3000)}`,
323
  modelKey
324
  );
325
  const fixed = parseJSON(fixedRaw);
326
  if (fixed?.content) {
327
  fs.writeFileSync(absPath, fixed.content, "utf8");
328
  send("status", { text: `Fix applied: ${fixed.fix || "syntax correction"}` });
329
+ } else {
330
+ // Try extracting content from markdown if JSON parse failed
331
+ const extracted = extractContentFromText(fixedRaw || "");
332
+ if (extracted) {
333
+ fs.writeFileSync(absPath, extracted, "utf8");
334
+ send("status", { text: "Extracted fix from response" });
335
+ } else break;
336
+ }
337
  }
338
  return false;
339
  }
340
 
341
+ // ── ITERATIVE COMMAND LOOP ─────────────────────────────────────────────────
342
+ // Core agentic loop: ask AI for commands β†’ run them β†’ feed results back β†’ repeat until done
343
+ async function runAgentLoop(opts) {
344
+ const { filePath, fileContent: initialContent, message, modelKey, send, maxRounds = 6 } = opts;
345
+ const absPath = path.join(HOME_DIR, filePath);
346
+ const fileLines = initialContent.split("\n").length;
347
+ const smartCtx = getSmartContext(initialContent, message);
348
+ const allCommandResults = [];
349
+ const historyEntries = [];
350
+ let cmdIndex = 0;
351
+ let fileContent = initialContent;
352
+
353
+ // ── ROUND 1: Master plan ───────────────────────────────────────────────
354
+ send("status", { text: "Analyzing request..." });
355
+
356
+ const planPrompt = `You are Cryo - a developer AI agent. You operate on files using shell commands.
357
+
358
+ ${COMMANDS_REFERENCE}
359
+
360
+ FILE: ${filePath} (${fileLines} lines)
361
+ CONTENT PREVIEW:
362
+ ${smartCtx.substring(0, 2000)}
363
+
364
+ USER REQUEST: "${message}"
365
+
366
+ IMPORTANT: You MUST respond with ONLY a raw JSON object. No prose. No markdown. No explanation outside JSON.
367
+ The JSON must have exactly this shape:
368
+ {
369
+ "task_type": "query" or "edit" or "create",
370
+ "status": "brief status shown to user",
371
+ "tasks": [{"task": "subtask description", "done": false}],
372
+ "commands": ["cmd1", "cmd2", "cmd3"],
373
+ "reasoning": "why these commands",
374
+ "done": false
375
+ }
376
+
377
+ RULES:
378
+ - For EDITS: always start with grep -n to find line numbers, then sed -i to edit, then node --check to verify
379
+ - For READS: use cat, head, tail, sed -n, grep
380
+ - For SEARCH/REPLACE: grep -n first to find exact lines, then sed -i 's/old/new/g'
381
+ - For DELETE: grep -n to find lines, then sed -i 'Xd' to delete
382
+ - For VIEW LINES: sed -n 'X,Yp' filename
383
+ - For BACKUP: cp file file.bak before major edits
384
+ - commands array must only contain commands from the ALLOWED list
385
+ - NEVER include node_modules, npm install, or destructive commands`;
386
+
387
+ const planRaw = await callAI(planPrompt, "", modelKey);
388
+ let plan = parseJSON(planRaw);
389
+
390
+ // Fallback: if AI returned markdown/prose, extract commands from it
391
+ if (!plan || !plan.commands) {
392
+ const extracted = extractCommandsFromText(planRaw || "");
393
+ plan = {
394
+ task_type: "edit",
395
+ status: "Running extracted commands...",
396
+ tasks: [{ task: message, done: false }],
397
+ commands: extracted.length > 0 ? extracted : [],
398
+ reasoning: "Extracted from response",
399
+ done: false
400
+ };
401
+ }
402
+
403
+ send("status", { text: plan.status || "Processing..." });
404
+ if (plan.tasks?.length > 0) send("task_update", { tasks: plan.tasks });
405
+
406
+ // ── EXECUTE INITIAL COMMANDS ───────────────────────────────────────────
407
+ for (const cmd of (plan.commands || [])) {
408
+ const idx = cmdIndex++;
409
+ send("status", { text: `[${idx + 1}] ${cmd}` });
410
+ send("command", { cmd, index: idx });
411
+ const result = await runCmd(cmd);
412
+ allCommandResults.push({ cmd, ...result });
413
+ historyEntries.push({ cmd, stdout: result.stdout.slice(0, 500), stderr: result.stderr.slice(0, 200), code: result.code, ts: Date.now() });
414
+ send("command_result", {
415
+ cmd, stdout: result.stdout.substring(0, 1000),
416
+ stderr: result.stderr.substring(0, 500),
417
+ index: idx, isError: result.code !== 0 && !!result.stderr
418
+ });
419
+
420
+ // Auto syntax-fix if node --check fails
421
+ if (cmd.includes("node --check") && result.code !== 0 && result.stderr) {
422
+ const target = cmd.split(/\s+/).pop().replace(/\s*\(.*\)$/, "");
423
+ send("status", { text: `Syntax error in ${target} β€” fixing...` });
424
+ await syntaxFixLoop(target, send, modelKey, 3);
425
+ fileContent = fs.existsSync(absPath) ? fs.readFileSync(absPath, "utf8") : fileContent;
426
+ }
427
+ }
428
+
429
+ fileContent = fs.existsSync(absPath) ? fs.readFileSync(absPath, "utf8") : fileContent;
430
+
431
+ // ── ITERATIVE ROUNDS: keep running until AI says done ─────────────────
432
+ for (let round = 2; round <= maxRounds; round++) {
433
+ const resultsText = allCommandResults.slice(-8)
434
+ .map(r => `$ ${r.cmd}\nSTDOUT: ${r.stdout.substring(0, 400)}${r.stderr ? `\nSTDERR: ${r.stderr.substring(0, 200)}` : ""}\nEXIT: ${r.code}`)
435
+ .join("\n\n");
436
+
437
+ send("status", { text: `Round ${round}: deciding next steps...` });
438
+
439
+ const continuePrompt = `You are Cryo - a developer AI agent completing a task.
440
+
441
+ USER REQUEST: "${message}"
442
+ FILE: ${filePath}
443
+ CURRENT CONTENT (first 60 lines):
444
+ ${fileContent.split("\n").slice(0, 60).join("\n")}
445
+
446
+ COMMANDS ALREADY RUN:
447
+ ${resultsText}
448
+
449
+ Decide what to do NEXT. If the task is complete, set "done": true and "commands": [].
450
+ If more commands are needed, list ONLY the next batch (max 5 commands).
451
+
452
+ IMPORTANT: Respond with ONLY raw JSON, no markdown, no prose:
453
+ {
454
+ "done": true or false,
455
+ "status": "what was accomplished or what's next",
456
+ "commands": ["next cmd1", "next cmd2"],
457
+ "reasoning": "why"
458
+ }
459
+
460
+ If done=true, commands must be empty [].
461
+ Only use commands from: ${ALLOWED_CMDS.join(", ")}`;
462
+
463
+ const contRaw = await callAI(continuePrompt, "", modelKey);
464
+ let cont = parseJSON(contRaw);
465
+
466
+ if (!cont) {
467
+ // Try extracting from text
468
+ const extracted = extractCommandsFromText(contRaw || "");
469
+ if (extracted.length === 0) {
470
+ // AI gave no more commands β€” treat as done
471
+ break;
472
+ }
473
+ cont = { done: false, commands: extracted, status: "Continuing...", reasoning: "Extracted" };
474
+ }
475
+
476
+ if (cont.done || !cont.commands || cont.commands.length === 0) {
477
+ send("status", { text: cont.status || "Task complete" });
478
+ break;
479
+ }
480
+
481
+ send("status", { text: cont.status || `Round ${round}...` });
482
+
483
+ for (const cmd of cont.commands) {
484
+ const idx = cmdIndex++;
485
+ send("status", { text: `[${idx + 1}] ${cmd}` });
486
+ send("command", { cmd, index: idx });
487
+ const result = await runCmd(cmd);
488
+ allCommandResults.push({ cmd, ...result });
489
+ historyEntries.push({ cmd, stdout: result.stdout.slice(0, 500), stderr: result.stderr.slice(0, 200), code: result.code, ts: Date.now() });
490
+ send("command_result", {
491
+ cmd, stdout: result.stdout.substring(0, 1000),
492
+ stderr: result.stderr.substring(0, 500),
493
+ index: idx, isError: result.code !== 0 && !!result.stderr
494
+ });
495
+
496
+ if (cmd.includes("node --check") && result.code !== 0 && result.stderr) {
497
+ const target = cmd.split(/\s+/).pop().replace(/\s*\(.*\)$/, "");
498
+ send("status", { text: `Syntax error β€” fixing...` });
499
+ await syntaxFixLoop(target, send, modelKey, 3);
500
+ fileContent = fs.existsSync(absPath) ? fs.readFileSync(absPath, "utf8") : fileContent;
501
+ }
502
+ }
503
+
504
+ fileContent = fs.existsSync(absPath) ? fs.readFileSync(absPath, "utf8") : fileContent;
505
+ }
506
+
507
+ appendCmdHistory(historyEntries);
508
+
509
+ return {
510
+ allCommandResults,
511
+ fileContent,
512
+ plan,
513
+ hadSedEdits: allCommandResults.some(r => r.cmd.startsWith("sed -i") && r.code === 0)
514
+ };
515
+ }
516
+
517
  // ── FILE TREE ──────────────────────────────────────────────────────────────
518
  app.get("/api/tree", (req, res) => {
519
  const buildTree = (dir, base = HOME_DIR) => {
 
568
  res.download(fp);
569
  });
570
 
571
+ app.get("/api/tasks", (req, res) => res.json({ tasks: readTasks() }));
 
 
572
 
573
  app.get("/api/history", (req, res) => {
574
  if (!fs.existsSync(CMD_HISTORY_FILE)) return res.json({ history: [] });
 
618
  try { res.write(`data: ${JSON.stringify({ type, ...data })}\n\n`); } catch {}
619
  };
620
 
621
+ const { message, currentFile, model: modelKey = "cryo2" } = req.body;
622
 
623
  try {
624
+ // ── Web search (no file context) ───────────────────────────────────────
625
  if (needsWebSearch(message) && !currentFile) {
626
  send("status", { text: "Searching the web..." });
627
  send("web_search", { query: message });
 
641
  const absPath = path.join(HOME_DIR, filePath);
642
  let fileContent = fs.existsSync(absPath) ? fs.readFileSync(absPath, "utf8") : "";
643
  const hasFile = fileContent.length > 0;
 
 
644
  const existingTasks = readTasks();
645
  const pendingTasks = existingTasks.filter(t => !t.done);
646
 
647
+ // ── NO FILE: Create from scratch ───────────────────────────────────────
648
  if (!hasFile) {
649
  send("status", { text: "Planning what to build..." });
650
 
651
+ const createPrompt = `You are Cryo - a precise developer AI. Create the code the user requested.
 
 
 
 
 
 
652
 
653
+ USER REQUEST: "${message}"
654
+
655
+ Respond with ONLY a raw JSON object (no markdown, no prose around it):
656
+ {
657
+ "filename": "appropriate filename like bot.js or app.py",
658
+ "description": "one line description of what this is",
659
+ "tasks": [{"task": "what you built", "done": true}],
660
+ "content": "COMPLETE working file content here",
661
+ "status": "Created - brief description"
662
+ }
663
+
664
+ RULES:
665
+ - content must be a complete, working file
666
+ - Escape all special characters in content properly for JSON
667
+ - filename must have correct extension for the language`;
668
+
669
+ const aiRaw = await callAI(createPrompt, "", modelKey);
670
  let parsed = parseJSON(aiRaw);
671
+
672
+ // Fallback: extract code from markdown response
673
  if (!parsed?.content) {
674
+ const extracted = extractContentFromText(aiRaw || "");
675
+ // Try to guess filename from response
676
+ const fnMatch = aiRaw.match(/(?:file|save|name)[^a-z]*?([a-z0-9_-]+\.[a-z]{2,4})/i);
677
  parsed = {
678
+ filename: fnMatch?.[1] || "index.js",
679
+ content: extracted || aiRaw,
680
+ description: "Generated code",
681
+ tasks: [{ task: "Generate file", done: true }],
682
+ status: "Created"
683
  };
684
  }
685
 
686
+ send("status", { text: `Plan ready β€” ${parsed.tasks?.length || 1} task(s)` });
687
  send("task_update", { tasks: parsed.tasks || [] });
688
  send("status", { text: "Checking generated code..." });
689
 
690
  const tmpName = `__cryo_new_${Date.now()}.js`;
691
  const tmpPath = path.join(HOME_DIR, tmpName);
692
  fs.writeFileSync(tmpPath, parsed.content, "utf8");
693
+
694
+ // Only syntax-check JS files
695
+ if (parsed.filename.endsWith(".js") || parsed.filename.endsWith(".ts")) {
696
+ await syntaxFixLoop(tmpName, send, modelKey, 3);
697
+ parsed.content = fs.readFileSync(tmpPath, "utf8");
698
+ }
699
  fs.unlinkSync(tmpPath);
700
 
701
  send("status", { text: `Saving ${parsed.filename}...` });
 
703
  fs.mkdirSync(path.dirname(savePath), { recursive: true });
704
  fs.writeFileSync(savePath, parsed.content, "utf8");
705
 
706
+ if (parsed.filename.endsWith(".js")) {
707
+ const finalCheck = await runCmd(`node --check ${parsed.filename}`);
708
+ send("command", { cmd: `node --check ${parsed.filename}`, index: 0 });
709
+ send("command_result", {
710
+ cmd: `node --check ${parsed.filename}`,
711
+ stdout: finalCheck.code === 0 ? "βœ“ Syntax OK β€” file saved!" : "",
712
+ stderr: finalCheck.stderr, index: 0, isError: finalCheck.code !== 0
713
+ });
714
+ }
715
 
716
  updateTasks(parsed.tasks || []);
717
  updateMeta({ last_ai_edit: new Date().toISOString(), languages: [parsed.filename.split(".").pop()] });
718
  send("file_created", { filename: parsed.filename, content: parsed.content, description: parsed.description });
719
+ send("message", { text: parsed.status || `Created \`${parsed.filename}\`` });
720
  send("tree_update", {});
721
  res.write("data: [DONE]\n\n");
722
  res.end();
723
  return;
724
  }
725
 
726
+ // ── FILE EXISTS: Iterative agent loop ──────────���──────────────────────
727
+ send("status", { text: "Analyzing..." });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
728
 
729
+ const loopResult = await runAgentLoop({
730
+ filePath, fileContent, message, modelKey, send, maxRounds: 6
731
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
732
 
733
+ const { allCommandResults, hadSedEdits, plan } = loopResult;
734
+ fileContent = loopResult.fileContent;
735
 
736
+ const resultsText = allCommandResults
737
+ .map(r => `$ ${r.cmd}\n${r.stdout.substring(0, 400)}${r.stderr ? `\nSTDERR: ${r.stderr.substring(0, 150)}` : ""}`)
738
  .join("\n\n");
739
 
740
  send("status", { text: "Synthesizing..." });
741
 
742
+ // ── QUERY: just answer ─────────────────────────────────────────────
743
  if (plan.task_type === "query") {
 
744
  if (needsWebSearch(message)) {
745
  send("web_search", { query: message });
746
  try {
 
749
  send("message", { text: wsResult.answer });
750
  } catch {
751
  const ansRaw = await callAI(
752
+ `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"}`,
753
+ `Question: "${message}"\nCommand results:\n${resultsText}\nFile context:\n${fileContent.substring(0, 1500)}`,
754
  modelKey
755
  );
756
  const ans = parseJSON(ansRaw);
757
+ send("message", { text: ans?.answer || ansRaw || "Could not generate answer." });
758
  }
759
  } else {
760
  const ansRaw = await callAI(
761
+ `You are Cryo. Answer the question based on file content and command output. Return ONLY JSON: {"answer":"your answer here"}`,
762
+ `Question: "${message}"\nCommand results:\n${resultsText}\nFile:\n${fileContent.substring(0, 1500)}`,
763
  modelKey
764
  );
765
  const ans = parseJSON(ansRaw);
766
+ send("message", { text: ans?.answer || ansRaw || "Could not generate answer." });
767
  }
768
 
769
  } else {
770
+ // ── EDIT/CREATE: sed was used β†’ verify & summarize ─────────────────
 
771
  if (hadSedEdits) {
772
+ send("status", { text: "Verifying edits..." });
773
+ if (filePath.endsWith(".js")) {
774
+ await syntaxFixLoop(filePath, send, modelKey, 3);
775
+ }
776
+ fileContent = fs.existsSync(absPath) ? fs.readFileSync(absPath, "utf8") : fileContent;
777
 
778
  const sumRaw = await callAI(
779
+ `Summarize what changed in the file. Return ONLY JSON: {"summary":"what changed","tasks_completed":["t1"]}`,
780
+ `User request: ${message}\nCommands run:\n${resultsText}`,
781
  modelKey
782
  );
783
  const sum = parseJSON(sumRaw);
 
787
  versionFile(filePath);
788
  updateMeta({ last_ai_edit: new Date().toISOString() });
789
  send("file_updated", { filename: filePath, content: fileContent, description: sum?.summary || "Updated" });
790
+ send("message", { text: sum?.summary || "Changes applied successfully." });
791
 
792
  } else {
793
+ // ── No sed edits were made β†’ ask AI for complete file content ───
794
  const editRaw = await callAI(
795
+ `You are Cryo. Produce the COMPLETE updated file based on the user's request.
796
+ RULES: Preserve everything not changed. Output must be the full working file.
797
+ Return ONLY valid JSON (no markdown):
798
+ {"content":"COMPLETE file content here","description":"what changed","status":"Done - brief summary"}`,
799
+ `User request: "${message}"\nFile: ${filePath}\nCurrent content:\n${fileContent.substring(0, 4000)}\nCommand results:\n${resultsText}\n\nProduce complete updated file.`,
800
  modelKey
801
  );
802
 
803
  let editParsed = parseJSON(editRaw);
804
+
805
+ // Fallback: extract code from markdown
806
  if (!editParsed?.content) {
807
+ const extracted = extractContentFromText(editRaw || "");
808
+ editParsed = extracted
809
+ ? { content: extracted, description: "Updated", status: "Done" }
810
+ : { content: fileContent, description: "No changes made", status: "Done" };
811
  }
812
 
 
 
 
813
  versionFile(filePath);
814
 
815
+ // Pre-save syntax check
816
+ send("status", { text: "Checking before saving..." });
817
  const tmpName = `__cryo_presave_${Date.now()}.js`;
818
  const tmpPath = path.join(HOME_DIR, tmpName);
819
  fs.writeFileSync(tmpPath, editParsed.content, "utf8");
820
+
821
+ if (filePath.endsWith(".js")) {
822
+ await syntaxFixLoop(tmpName, send, modelKey, 3);
823
+ editParsed.content = fs.readFileSync(tmpPath, "utf8");
824
+ }
825
  fs.unlinkSync(tmpPath);
826
 
827
  fs.writeFileSync(absPath, editParsed.content, "utf8");
828
  send("status", { text: `Saved ${filePath}` });
829
 
830
+ if (filePath.endsWith(".js")) {
831
+ await syntaxFixLoop(filePath, send, modelKey, 3);
832
+ }
833
  fileContent = fs.readFileSync(absPath, "utf8");
834
 
835
  const allTasks = [
836
  ...existingTasks.map(t => ({ ...t, done: true })),
837
+ ...(plan.tasks || []).map(t => ({ ...t, done: true }))
838
  ];
839
  saveTasks(allTasks);
840
  updateMeta({ last_ai_edit: new Date().toISOString() });
 
845
  }
846
  }
847
 
848
+ // ── Pending tasks check ────────────────────────────────────────────
849
  const latestTasks = readTasks();
850
  const stillPending = latestTasks.filter(t => !t.done);
851
  if (stillPending.length > 0) {
 
852
  send("pending_tasks", { tasks: stillPending, count: stillPending.length });
853
  }
854