Reaperxxxx commited on
Commit
658e80c
Β·
verified Β·
1 Parent(s): 0865d60

Update server.js

Browse files
Files changed (1) hide show
  1. server.js +445 -67
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 = "ak_2ep6ba2Ww0pn5cH09U2Mq3Eo0ez1M";
 
55
 
56
- // All model keys from the UI now map to DeepSeek β€” one API, no confusion
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 ← READ A SECTION by line range (most important)
82
- grep -n "pattern" file ← FIND with line numbers (always do this first)
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
- === MANDATORY WORKFLOW (follow exactly) ===
96
- STEP 1 β€” LOCATE: wc -l file β†’ grep -n "keyword" file (find the exact line number)
97
- STEP 2 β€” READ: sed -n 'START,ENDp' file (read the section around that line)
98
- STEP 3 β€” BACKUP: cp file file.bak (before any edit)
99
- STEP 4 β€” EDIT: sed -i 'Ns/exact-old/new/' file (use line number from step 1)
100
- STEP 5 β€” VERIFY: sed -n 'START,ENDp' file (confirm the change is there)
101
- STEP 6 β€” CHECK: node --check file.js (after every JS write)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
 
103
  === STRICT RULES ===
104
- 1. ALWAYS grep -n first β€” NEVER edit blind without knowing the line number
105
- 2. ALWAYS sed -n to read a section BEFORE editing it β€” verify the exact text
106
- 3. ALWAYS cp file.bak before rewrites β€” never destroy without a backup
107
- 4. ALWAYS node --check after every JS write β€” catch syntax errors immediately
108
  5. NEVER echo -e for multiline β€” always tee with heredoc
109
- 6. Mark done:true ONLY after sed -n confirms the change is physically in the file
110
- 7. If a sed -i edit produced no output change, the pattern was wrong β€” grep again
 
 
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
- resolve({ stdout: stdout || "", stderr: stderr || "", code: err ? (err.code || 1) : 0 });
 
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-Chat
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) })); // cap each turn
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-Chat",
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
- const { data } = await axios.post(LONGCAT_API_URL, payload, {
324
- timeout: 60000,
325
- headers: {
326
- "Content-Type": "application/json",
327
- "Authorization": `Bearer ${LONGCAT_API_KEY}`,
328
- "anthropic-version": "2023-06-01",
329
- },
330
- });
 
 
 
 
 
 
 
 
 
331
 
332
- // Anthropic format: data.content is an array of blocks
333
- if (data.content && Array.isArray(data.content)) {
334
- const textBlock = data.content.find(b => b.type === "text");
335
- return textBlock ? textBlock.text : "";
 
 
 
 
 
 
 
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
- for (const cmd of commands) {
411
- if (!cmd || !cmd.trim()) continue;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
412
  const idx = cmdIndex++;
413
  send("status", { text: `[${idx + 1}] ${cmd}` });
414
  send("command", { cmd, index: idx });
415
  const result = await runCmd(cmd);
416
- allCommandResults.push({ cmd, ...result });
 
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, stdout: result.stdout.substring(0, 1000),
420
- stderr: result.stderr.substring(0, 500),
421
- index: idx, isError: result.code !== 0 && !!result.stderr
 
 
 
 
 
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 actual outputs β€” this is what the AI uses to
471
- // know WHAT grep found, WHAT sed changed, WHERE errors occurred
472
  const cmdLog = allCommandResults.slice(-14).map((r, i) => {
473
- const out = r.stdout.trim().slice(0, 400);
474
- const err = r.stderr.trim().slice(0, 200);
475
  const exitLabel = r.code === 0 ? "βœ“" : `βœ—(${r.code})`;
476
- return `[${i + 1}] ${exitLabel} $ ${r.cmd}${out ? "\n OUT: " + out : ""}${err ? "\n ERR: " + err : ""}`;
 
 
 
 
 
 
 
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 wc -l and grep -n)"}
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 β€” use these in sed -i 'Ns/...' commands):
530
  ${initCtx}
531
 
532
  USER REQUEST: "${message}"
533
 
534
  ⚠ Complete ONLY what was requested. Do not add unrequested features.
 
 
 
535
 
536
- YOUR FIRST BATCH must follow the mandatory workflow:
537
- 1. wc -l ${filePath} β†’ confirm line count
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
- "commands": ["wc -l ${filePath}", "grep -n \\"keyword\\" ${filePath}", "..."],
550
- "reasoning": "what you are looking for and why",
 
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
- CRITICAL: done:true only when sed -n CONFIRMS the change is physically in the file.`;
 
 
 
 
 
 
 
 
 
 
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
- res.json({ stdout: stdout || "", stderr: stderr || "", code: err ? (err.code || 1) : 0 });
 
 
 
 
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 || (loopResult.hadTeeWrites)) {
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);