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 +1 -1
- package/src/agent.js +130 -1
- package/src/api.js +2 -2
- package/src/project-context.js +156 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aether-code",
|
|
3
|
-
"version": "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 =
|
|
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 =
|
|
205
|
+
const READ_TIMEOUT_MS = 180_000;
|
|
206
206
|
const readOnce = () => {
|
|
207
207
|
let timer;
|
|
208
208
|
const timeout = new Promise((_, reject) => {
|
package/src/project-context.js
CHANGED
|
@@ -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;
|