aether-code 0.39.0 → 0.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aether-code",
3
- "version": "0.39.0",
3
+ "version": "0.41.0",
4
4
  "description": "Uncensored AI coding agent for your terminal — Claude Code alternative with MCP support. Reads code, writes files, runs commands. Drives IDA Pro, Roblox Studio, Wireshark, Blender, and any MCP server. No refusal layer.",
5
5
  "homepage": "https://trynoguard.com",
6
6
  "repository": {
package/src/agent.js CHANGED
@@ -9,7 +9,7 @@ import { agentTurnStream, AetherError } from "./api.js";
9
9
  import { TOOL_DEFINITIONS, executeTool } from "./tools.js";
10
10
  import { unnamespaceToolName } from "./mcp.js";
11
11
  import { loadAllSkills, selectSkills, renderSkillsBlock } from "./skills.js";
12
- import { scanProject, detectVerifyCommand } from "./project-context.js";
12
+ import { scanProject, detectVerifyCommand, detectBuildCommand } from "./project-context.js";
13
13
  import { c, divider, turn, toolLabel, toolSummary, makeTokenStripper, errorLine, startSpinner } from "./render.js";
14
14
 
15
15
  const DEFAULT_MAX_TURNS = 25;
@@ -111,6 +111,7 @@ export async function runAgent({
111
111
  const looksLikeModification = MOD_RE.test(initialPrompt);
112
112
  let appliedNothingNudges = 0;
113
113
  let verifyRounds = 0;
114
+ let buildRounds = 0;
114
115
 
115
116
  for (let i = 0; i < maxTurns; i++) {
116
117
  // No turn header and no leading blank here — each step (assistant text and
@@ -220,6 +221,64 @@ export async function runAgent({
220
221
  messages.push({ role: "user", content: msg });
221
222
  continue; // give the model another turn to finish
222
223
  }
224
+ // Auto-build: if we wrote source files, detect the build toolchain and
225
+ // compile automatically. If build fails, feed errors back to the model
226
+ // so it can fix and recompile. Max 5 rounds to avoid infinite loops.
227
+ if (filesTouched.size > 0 && buildRounds < 5) {
228
+ const buildResult = await runBuild(cwd, [...filesTouched.keys()]);
229
+ if (buildResult && !buildResult.ok) {
230
+ buildRounds++;
231
+ console.log("\n" + c.yellow("●") + " " + c.bold("auto-build") + c.gray(` (${buildResult.label}) — round ${buildRounds}/5`));
232
+ console.log(c.red(" └─") + " " + c.gray("build failed — feeding errors back to fix"));
233
+ const errPreview = buildResult.output.length > 6000
234
+ ? buildResult.output.slice(0, 6000) + "\n…(truncated)"
235
+ : buildResult.output;
236
+ messages.push({
237
+ role: "user",
238
+ content:
239
+ `AUTO-BUILD FAILED (${buildResult.label}):\n\`\`\`\n${errPreview}\n\`\`\`\n` +
240
+ `Fix the build errors NOW using edit_file. Do not explain — just fix the code and move on.`,
241
+ });
242
+ continue;
243
+ }
244
+ if (buildResult && buildResult.ok) {
245
+ console.log("\n" + c.green("●") + " " + c.bold("auto-build") + c.gray(` (${buildResult.label}) — success`));
246
+ // If there's a run command and we haven't already run it, execute the result
247
+ if (buildResult.runCmd && buildRounds === 0) {
248
+ const runResult = await runBuiltProgram(cwd, buildResult.runCmd, buildResult.label);
249
+ if (runResult && !runResult.ok && buildRounds < 5) {
250
+ buildRounds++;
251
+ console.log(c.yellow(" ├─") + " " + c.bold("auto-run") + c.gray(` — crashed (exit ${runResult.code})`));
252
+ const errPreview = runResult.output.length > 6000
253
+ ? runResult.output.slice(0, 6000) + "\n…(truncated)"
254
+ : runResult.output;
255
+ messages.push({
256
+ role: "user",
257
+ content:
258
+ `BUILD SUCCEEDED but the program CRASHED when run:\n\`\`\`\n${errPreview}\n\`\`\`\n` +
259
+ `Fix the runtime error NOW using edit_file. Do not explain — just fix and move on.`,
260
+ });
261
+ continue;
262
+ }
263
+ if (runResult && runResult.ok) {
264
+ const preview = runResult.output.length > 2000
265
+ ? runResult.output.slice(0, 2000) + "\n…(truncated)"
266
+ : runResult.output;
267
+ if (runResult.timedOut) {
268
+ console.log(c.green(" ├─") + " " + c.bold("auto-run") + c.gray(" — running (still alive after 10s)"));
269
+ } else {
270
+ console.log(c.green(" ├─") + " " + c.bold("auto-run") + c.gray(` — exit 0`));
271
+ }
272
+ if (preview.trim()) {
273
+ const lines = preview.trim().split("\n").slice(0, 15);
274
+ for (const line of lines) {
275
+ console.log(c.gray(" │ " + line));
276
+ }
277
+ }
278
+ }
279
+ }
280
+ }
281
+ }
223
282
  // Auto-verify: if we touched files, run the project's typecheck/lint
224
283
  // and feed errors back so the model can self-correct without the user
225
284
  // having to say "fix that." At most 2 verify rounds to avoid infinite loops.
@@ -350,6 +409,76 @@ async function runVerify(cwd) {
350
409
  });
351
410
  }
352
411
 
412
+ // Auto-build: detect the build toolchain from written files and compile.
413
+ // Returns { ok, output, label, runCmd } or null if no buildable project detected.
414
+ let cachedBuild = undefined;
415
+ async function runBuild(cwd, touchedFiles) {
416
+ // Re-detect every time because the model may create new files (e.g. Makefile)
417
+ // or change the project structure between rounds.
418
+ const detected = detectBuildCommand(cwd, touchedFiles);
419
+ if (!detected) {
420
+ // Fallback: if we previously detected a build command, reuse it (the model
421
+ // may have only edited files this round, not created new ones).
422
+ if (cachedBuild) return runBuildCmd(cwd, cachedBuild);
423
+ return null;
424
+ }
425
+ cachedBuild = detected;
426
+ return runBuildCmd(cwd, detected);
427
+ }
428
+
429
+ async function runBuildCmd(cwd, { cmd, label, type, runCmd }) {
430
+ return new Promise((resolve) => {
431
+ const child = spawn(cmd, [], { cwd, shell: true, stdio: ["ignore", "pipe", "pipe"] });
432
+ let stdout = "";
433
+ let stderr = "";
434
+ const timer = setTimeout(() => { child.kill("SIGTERM"); }, 120_000);
435
+ child.stdout.on("data", (d) => { stdout += d.toString(); });
436
+ child.stderr.on("data", (d) => { stderr += d.toString(); });
437
+ child.on("close", (code) => {
438
+ clearTimeout(timer);
439
+ const output = (stdout + "\n" + stderr).trim();
440
+ resolve({ ok: code === 0, output, label, type, runCmd });
441
+ });
442
+ child.on("error", (err) => {
443
+ clearTimeout(timer);
444
+ resolve({ ok: false, output: err.message, label, type, runCmd });
445
+ });
446
+ });
447
+ }
448
+
449
+ // Run the compiled/interpreted program after a successful build.
450
+ // Returns { ok, output, code, timedOut } or null on spawn error.
451
+ async function runBuiltProgram(cwd, runCmd, label) {
452
+ return new Promise((resolve) => {
453
+ const child = spawn(runCmd, [], { cwd, shell: true, stdio: ["ignore", "pipe", "pipe"] });
454
+ let stdout = "";
455
+ let stderr = "";
456
+ let timedOut = false;
457
+ const timer = setTimeout(() => { timedOut = true; child.kill("SIGTERM"); }, 10_000);
458
+ child.stdout.on("data", (d) => {
459
+ stdout += d.toString();
460
+ if (stdout.length > 50_000) { child.kill("SIGTERM"); }
461
+ });
462
+ child.stderr.on("data", (d) => {
463
+ stderr += d.toString();
464
+ if (stderr.length > 50_000) { child.kill("SIGTERM"); }
465
+ });
466
+ child.on("close", (code) => {
467
+ clearTimeout(timer);
468
+ const output = (stdout + "\n" + stderr).trim();
469
+ if (timedOut) {
470
+ resolve({ ok: true, output, code: null, timedOut: true });
471
+ } else {
472
+ resolve({ ok: code === 0, output, code, timedOut: false });
473
+ }
474
+ });
475
+ child.on("error", (err) => {
476
+ clearTimeout(timer);
477
+ resolve({ ok: false, output: err.message, code: null, timedOut: false });
478
+ });
479
+ });
480
+ }
481
+
353
482
  /**
354
483
  * Per-turn skill injection. Selects skills against the latest user message
355
484
  * + paths the model has touched, then prepends matching bodies onto the
package/src/api.js CHANGED
@@ -14,7 +14,7 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
14
14
  * string, so resending is safe. For the streaming endpoint only the initial
15
15
  * connection is retried.
16
16
  */
17
- export async function fetchWithRetry(url, options, { retries = 2, baseDelay = 500, onRetry } = {}) {
17
+ export async function fetchWithRetry(url, options, { retries = 4, baseDelay = 1000, onRetry } = {}) {
18
18
  let attempt = 0;
19
19
  while (true) {
20
20
  try {
@@ -202,7 +202,7 @@ export async function agentTurnStream({
202
202
 
203
203
  // Watchdog: if the open stream stalls (no bytes) for too long, abort instead
204
204
  // of hanging the CLI forever.
205
- const READ_TIMEOUT_MS = 120_000;
205
+ const READ_TIMEOUT_MS = 180_000;
206
206
  const readOnce = () => {
207
207
  let timer;
208
208
  const timeout = new Promise((_, reject) => {
@@ -535,6 +535,162 @@ export function detectVerifyCommand(cwd) {
535
535
  return null;
536
536
  }
537
537
 
538
+ // ─────────── Auto-build: detect compilation / build commands ───────────
539
+
540
+ export function detectBuildCommand(cwd, writtenFiles = []) {
541
+ const exts = new Set(writtenFiles.map((f) => path.extname(f).toLowerCase()));
542
+ const basenames = new Set(writtenFiles.map((f) => path.basename(f).toLowerCase()));
543
+ const win = process.platform === "win32";
544
+
545
+ // Assembly (NASM/FASM/MASM)
546
+ if (exts.has(".asm") || exts.has(".s")) {
547
+ const asmFiles = writtenFiles.filter((f) => /\.(asm|s)$/i.test(f));
548
+ if (asmFiles.length > 0) {
549
+ const outName = win ? "output.exe" : "output";
550
+ const objName = asmFiles[0].replace(/\.(asm|s)$/i, ".o");
551
+ return {
552
+ cmd: `nasm -f ${win ? "win64" : "elf64"} ${asmFiles[0]} -o ${objName} && ${win ? "link" : "ld"} ${objName} -o ${outName}`,
553
+ label: "nasm",
554
+ type: "compiled",
555
+ runCmd: win ? `.\\${outName}` : `./${outName}`,
556
+ };
557
+ }
558
+ }
559
+
560
+ // C/C++
561
+ if (exts.has(".c") || exts.has(".cpp") || exts.has(".cc") || exts.has(".cxx") || exts.has(".h") || exts.has(".hpp")) {
562
+ if (fs.existsSync(path.join(cwd, "CMakeLists.txt"))) {
563
+ return { cmd: "cmake -B build && cmake --build build", label: "cmake", type: "compiled" };
564
+ }
565
+ if (fs.existsSync(path.join(cwd, "Makefile")) || fs.existsSync(path.join(cwd, "makefile"))) {
566
+ return { cmd: "make", label: "make", type: "compiled" };
567
+ }
568
+ const cFiles = writtenFiles.filter((f) => /\.(c|cpp|cc|cxx)$/i.test(f));
569
+ if (cFiles.length > 0) {
570
+ const hasC = cFiles.some((f) => /\.c$/i.test(f) && !/\.(cpp|cc|cxx)$/i.test(f));
571
+ const hasCpp = cFiles.some((f) => /\.(cpp|cc|cxx)$/i.test(f));
572
+ const compiler = hasCpp ? "g++" : "gcc";
573
+ const outName = win ? "output.exe" : "output";
574
+ const flags = [];
575
+ if (hasCpp) flags.push("-std=c++17");
576
+ else if (hasC) flags.push("-std=c11");
577
+ flags.push("-Wall", "-O2");
578
+ // Check if they need threading, math, etc. from the source
579
+ const allSrc = cFiles.map((f) => {
580
+ try { return fs.readFileSync(path.join(cwd, f), "utf8"); } catch { return ""; }
581
+ }).join("\n");
582
+ if (allSrc.includes("pthread") || allSrc.includes("<thread>")) flags.push("-pthread");
583
+ if (allSrc.includes("<math.h>") || allSrc.includes("<cmath>")) flags.push("-lm");
584
+ if (win && (allSrc.includes("winsock") || allSrc.includes("WinSock") || allSrc.includes("ws2_32"))) flags.push("-lws2_32");
585
+ if (win && allSrc.includes("windows.h")) flags.push("-lkernel32", "-luser32");
586
+ return {
587
+ cmd: `${compiler} ${flags.join(" ")} ${cFiles.join(" ")} -o ${outName}`,
588
+ label: compiler,
589
+ type: "compiled",
590
+ runCmd: win ? `.\\${outName}` : `./${outName}`,
591
+ };
592
+ }
593
+ }
594
+
595
+ // Rust
596
+ if (exts.has(".rs") || fs.existsSync(path.join(cwd, "Cargo.toml"))) {
597
+ if (fs.existsSync(path.join(cwd, "Cargo.toml"))) {
598
+ return { cmd: "cargo build --release", label: "cargo", type: "compiled" };
599
+ }
600
+ const rsFiles = writtenFiles.filter((f) => /\.rs$/i.test(f));
601
+ if (rsFiles.length === 1) {
602
+ const outName = win ? "output.exe" : "output";
603
+ return { cmd: `rustc -o ${outName} ${rsFiles[0]}`, label: "rustc", type: "compiled", runCmd: win ? `.\\${outName}` : `./${outName}` };
604
+ }
605
+ }
606
+
607
+ // Go
608
+ if (exts.has(".go") || fs.existsSync(path.join(cwd, "go.mod"))) {
609
+ const outName = win ? "output.exe" : "output";
610
+ return { cmd: `go build -o ${outName} ./...`, label: "go build", type: "compiled", runCmd: win ? `.\\${outName}` : `./${outName}` };
611
+ }
612
+
613
+ // C# / .NET
614
+ if (exts.has(".cs")) {
615
+ const csproj = writtenFiles.find((f) => f.endsWith(".csproj"));
616
+ if (csproj || fs.existsSync(path.join(cwd, "*.csproj"))) {
617
+ return { cmd: "dotnet build", label: "dotnet", type: "compiled" };
618
+ }
619
+ const csFiles = writtenFiles.filter((f) => /\.cs$/i.test(f));
620
+ if (csFiles.length > 0) {
621
+ const outName = win ? "output.exe" : "output";
622
+ return { cmd: `dotnet-script ${csFiles[0]}`, label: "dotnet-script", type: "interpreted", runCmd: `dotnet-script ${csFiles[0]}` };
623
+ }
624
+ }
625
+
626
+ // Java
627
+ if (exts.has(".java")) {
628
+ const javaFiles = writtenFiles.filter((f) => /\.java$/i.test(f));
629
+ if (javaFiles.length > 0) {
630
+ // Find the file with main()
631
+ let mainClass = null;
632
+ for (const jf of javaFiles) {
633
+ try {
634
+ const src = fs.readFileSync(path.join(cwd, jf), "utf8");
635
+ if (src.includes("public static void main")) {
636
+ mainClass = path.basename(jf, ".java");
637
+ break;
638
+ }
639
+ } catch { /* skip */ }
640
+ }
641
+ return {
642
+ cmd: `javac ${javaFiles.join(" ")}`,
643
+ label: "javac",
644
+ type: "compiled",
645
+ runCmd: mainClass ? `java ${mainClass}` : null,
646
+ };
647
+ }
648
+ }
649
+
650
+ // Node/TypeScript — npm build script
651
+ if (fs.existsSync(path.join(cwd, "package.json"))) {
652
+ const pkg = readJsonSafe(path.join(cwd, "package.json"));
653
+ if (pkg?.scripts?.build) {
654
+ const startCmd = pkg.scripts.start ? "npm start" : null;
655
+ return { cmd: "npm install && npm run build", label: "npm build", type: "compiled", runCmd: startCmd };
656
+ }
657
+ if (pkg?.scripts?.start) {
658
+ return { cmd: "npm install", label: "npm install", type: "interpreted", runCmd: "npm start" };
659
+ }
660
+ }
661
+
662
+ // Python
663
+ if (exts.has(".py")) {
664
+ // Check for requirements.txt first
665
+ const hasReqs = fs.existsSync(path.join(cwd, "requirements.txt"));
666
+ const installCmd = hasReqs ? "pip install -r requirements.txt && " : "";
667
+ // Priority: main.py > __main__.py > app.py > single file
668
+ const pyFiles = writtenFiles.filter((f) => /\.py$/i.test(f));
669
+ const mainCandidates = ["main.py", "__main__.py", "app.py", "run.py"];
670
+ let entrypoint = null;
671
+ for (const mc of mainCandidates) {
672
+ const match = pyFiles.find((f) => path.basename(f).toLowerCase() === mc);
673
+ if (match) { entrypoint = match; break; }
674
+ }
675
+ if (!entrypoint && pyFiles.length === 1) entrypoint = pyFiles[0];
676
+ if (entrypoint) {
677
+ return {
678
+ cmd: installCmd ? installCmd.slice(0, -4) : `python ${entrypoint}`,
679
+ label: hasReqs ? "pip install" : "python",
680
+ type: "interpreted",
681
+ runCmd: `python ${entrypoint}`,
682
+ };
683
+ }
684
+ }
685
+
686
+ // Makefile fallback (no source files matched above but a Makefile exists)
687
+ if (fs.existsSync(path.join(cwd, "Makefile")) || fs.existsSync(path.join(cwd, "makefile"))) {
688
+ return { cmd: "make", label: "make", type: "compiled" };
689
+ }
690
+
691
+ return null;
692
+ }
693
+
538
694
  export function scanProjectQuick(cwd) {
539
695
  const { dirs, files } = listTopLevel(cwd);
540
696
  const total = dirs.length + files.length;