aether-code 0.38.0 → 0.40.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 +182 -1
- package/src/project-context.js +428 -0
- package/src/repl.js +28 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aether-code",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.40.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
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import path from "node:path";
|
|
7
|
+
import { spawn } from "node:child_process";
|
|
7
8
|
import { agentTurnStream, AetherError } from "./api.js";
|
|
8
9
|
import { TOOL_DEFINITIONS, executeTool } from "./tools.js";
|
|
9
10
|
import { unnamespaceToolName } from "./mcp.js";
|
|
10
11
|
import { loadAllSkills, selectSkills, renderSkillsBlock } from "./skills.js";
|
|
11
|
-
import { scanProject } from "./project-context.js";
|
|
12
|
+
import { scanProject, detectVerifyCommand, detectBuildCommand } from "./project-context.js";
|
|
12
13
|
import { c, divider, turn, toolLabel, toolSummary, makeTokenStripper, errorLine, startSpinner } from "./render.js";
|
|
13
14
|
|
|
14
15
|
const DEFAULT_MAX_TURNS = 25;
|
|
@@ -109,6 +110,8 @@ export async function runAgent({
|
|
|
109
110
|
// finishes having read files it never edited, we nudge it once to finish.
|
|
110
111
|
const looksLikeModification = MOD_RE.test(initialPrompt);
|
|
111
112
|
let appliedNothingNudges = 0;
|
|
113
|
+
let verifyRounds = 0;
|
|
114
|
+
let buildRounds = 0;
|
|
112
115
|
|
|
113
116
|
for (let i = 0; i < maxTurns; i++) {
|
|
114
117
|
// No turn header and no leading blank here — each step (assistant text and
|
|
@@ -218,6 +221,88 @@ export async function runAgent({
|
|
|
218
221
|
messages.push({ role: "user", content: msg });
|
|
219
222
|
continue; // give the model another turn to finish
|
|
220
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
|
+
}
|
|
282
|
+
// Auto-verify: if we touched files, run the project's typecheck/lint
|
|
283
|
+
// and feed errors back so the model can self-correct without the user
|
|
284
|
+
// having to say "fix that." At most 2 verify rounds to avoid infinite loops.
|
|
285
|
+
if (filesTouched.size > 0 && verifyRounds < 2) {
|
|
286
|
+
const verifyResult = await runVerify(cwd);
|
|
287
|
+
if (verifyResult && !verifyResult.ok) {
|
|
288
|
+
verifyRounds++;
|
|
289
|
+
console.log("\n" + c.yellow("●") + " " + c.bold("auto-verify") + c.gray(` (${verifyResult.label})`));
|
|
290
|
+
console.log(c.red(" └─") + " " + c.gray("errors found — feeding back to fix"));
|
|
291
|
+
const errPreview = verifyResult.output.length > 4000
|
|
292
|
+
? verifyResult.output.slice(0, 4000) + "\n…(truncated)"
|
|
293
|
+
: verifyResult.output;
|
|
294
|
+
messages.push({
|
|
295
|
+
role: "user",
|
|
296
|
+
content:
|
|
297
|
+
`AUTO-VERIFY FAILED (${verifyResult.label}):\n\`\`\`\n${errPreview}\n\`\`\`\n` +
|
|
298
|
+
`Fix these errors NOW using edit_file. Do not explain — just fix and move on.`,
|
|
299
|
+
});
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
if (verifyResult && verifyResult.ok) {
|
|
303
|
+
console.log("\n" + c.green("●") + " " + c.bold("auto-verify") + c.gray(` (${verifyResult.label}) — passed`));
|
|
304
|
+
}
|
|
305
|
+
}
|
|
221
306
|
if (filesTouched.size) {
|
|
222
307
|
console.log("\n" + c.bold("Files") + c.gray(` in ${cwd}`));
|
|
223
308
|
for (const [p, action] of filesTouched) {
|
|
@@ -298,6 +383,102 @@ export async function runAgent({
|
|
|
298
383
|
return { ok: false, error: new Error("Max turns reached"), totalCredits, totalIn, totalOut, balance: lastBalance, messages };
|
|
299
384
|
}
|
|
300
385
|
|
|
386
|
+
// Auto-verify: run the project's typecheck/lint command silently and return
|
|
387
|
+
// { ok, output, label }. Returns null if no verify command is detected.
|
|
388
|
+
let cachedVerify = undefined;
|
|
389
|
+
async function runVerify(cwd) {
|
|
390
|
+
if (cachedVerify === undefined) cachedVerify = detectVerifyCommand(cwd);
|
|
391
|
+
if (!cachedVerify) return null;
|
|
392
|
+
const { cmd, label } = cachedVerify;
|
|
393
|
+
return new Promise((resolve) => {
|
|
394
|
+
const child = spawn(cmd, [], { cwd, shell: true, stdio: ["ignore", "pipe", "pipe"] });
|
|
395
|
+
let stdout = "";
|
|
396
|
+
let stderr = "";
|
|
397
|
+
const timer = setTimeout(() => { child.kill("SIGTERM"); }, 60_000);
|
|
398
|
+
child.stdout.on("data", (d) => { stdout += d.toString(); });
|
|
399
|
+
child.stderr.on("data", (d) => { stderr += d.toString(); });
|
|
400
|
+
child.on("close", (code) => {
|
|
401
|
+
clearTimeout(timer);
|
|
402
|
+
const output = (stdout + "\n" + stderr).trim();
|
|
403
|
+
resolve({ ok: code === 0, output, label });
|
|
404
|
+
});
|
|
405
|
+
child.on("error", () => {
|
|
406
|
+
clearTimeout(timer);
|
|
407
|
+
resolve(null);
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
}
|
|
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
|
+
|
|
301
482
|
/**
|
|
302
483
|
* Per-turn skill injection. Selects skills against the latest user message
|
|
303
484
|
* + paths the model has touched, then prepends matching bodies onto the
|
package/src/project-context.js
CHANGED
|
@@ -263,6 +263,434 @@ export function scanProject(cwd) {
|
|
|
263
263
|
return lines.join("\n") + "\n";
|
|
264
264
|
}
|
|
265
265
|
|
|
266
|
+
// ─────────── /init — deep project indexing ───────────
|
|
267
|
+
|
|
268
|
+
const KEY_FILES = [
|
|
269
|
+
"README.md", "CLAUDE.md", ".env.example", "Dockerfile",
|
|
270
|
+
"docker-compose.yml", "docker-compose.yaml",
|
|
271
|
+
];
|
|
272
|
+
|
|
273
|
+
const ENTRY_PATTERNS = [
|
|
274
|
+
/^(src\/)?index\.[jt]sx?$/,
|
|
275
|
+
/^(src\/)?main\.[jt]sx?$/,
|
|
276
|
+
/^(src\/)?app\.[jt]sx?$/,
|
|
277
|
+
/^(src\/)?server\.[jt]sx?$/,
|
|
278
|
+
/^(bin|cli|cmd)\/[^/]+\.[jt]sx?$/,
|
|
279
|
+
/^(pages|app)\/_app\.[jt]sx?$/,
|
|
280
|
+
/^(pages|app)\/layout\.[jt]sx?$/,
|
|
281
|
+
];
|
|
282
|
+
|
|
283
|
+
const CONFIG_FILES = [
|
|
284
|
+
"next.config.js", "next.config.mjs", "next.config.ts",
|
|
285
|
+
"vite.config.ts", "vite.config.js", "webpack.config.js",
|
|
286
|
+
"tsconfig.json", "tailwind.config.js", "tailwind.config.ts",
|
|
287
|
+
"eslint.config.js", ".eslintrc.js", ".eslintrc.json",
|
|
288
|
+
"jest.config.js", "jest.config.ts", "vitest.config.ts",
|
|
289
|
+
"prisma/schema.prisma", "drizzle.config.ts",
|
|
290
|
+
];
|
|
291
|
+
|
|
292
|
+
function collectSourceFiles(dir, base, maxFiles = 500) {
|
|
293
|
+
const results = [];
|
|
294
|
+
function walk(current, rel) {
|
|
295
|
+
if (results.length >= maxFiles) return;
|
|
296
|
+
let entries;
|
|
297
|
+
try { entries = fs.readdirSync(current, { withFileTypes: true }); } catch { return; }
|
|
298
|
+
for (const e of entries) {
|
|
299
|
+
if (results.length >= maxFiles) return;
|
|
300
|
+
if (e.name.startsWith(".") && e.name !== ".env.example") continue;
|
|
301
|
+
if (SKIP_DIRS.has(e.name)) continue;
|
|
302
|
+
const fullPath = path.join(current, e.name);
|
|
303
|
+
const relPath = rel ? `${rel}/${e.name}` : e.name;
|
|
304
|
+
if (e.isDirectory()) {
|
|
305
|
+
walk(fullPath, relPath);
|
|
306
|
+
} else if (e.isFile()) {
|
|
307
|
+
let size = 0;
|
|
308
|
+
try { size = fs.statSync(fullPath).size; } catch { /* skip */ }
|
|
309
|
+
results.push({ path: relPath, size });
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
walk(dir, "");
|
|
314
|
+
return results;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function identifyKeyFiles(allFiles, cwd) {
|
|
318
|
+
const key = [];
|
|
319
|
+
for (const f of allFiles) {
|
|
320
|
+
const name = f.path.split("/").pop();
|
|
321
|
+
if (KEY_FILES.includes(name)) { key.push(f.path); continue; }
|
|
322
|
+
if (CONFIG_FILES.includes(f.path)) { key.push(f.path); continue; }
|
|
323
|
+
for (const pat of ENTRY_PATTERNS) {
|
|
324
|
+
if (pat.test(f.path)) { key.push(f.path); break; }
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return key;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function summarizeFile(filePath, maxLines = 30) {
|
|
331
|
+
try {
|
|
332
|
+
const stat = fs.statSync(filePath);
|
|
333
|
+
if (!stat.isFile() || stat.size > 200_000) return null;
|
|
334
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
335
|
+
const lines = content.split("\n");
|
|
336
|
+
const exports = [];
|
|
337
|
+
const imports = [];
|
|
338
|
+
for (const line of lines.slice(0, 100)) {
|
|
339
|
+
if (/^export\s+(default\s+)?(function|class|const|let|async)\s+(\w+)/m.test(line)) {
|
|
340
|
+
const match = line.match(/^export\s+(?:default\s+)?(?:function|class|const|let|async\s+function)\s+(\w+)/);
|
|
341
|
+
if (match) exports.push(match[1]);
|
|
342
|
+
}
|
|
343
|
+
if (/^import\s/.test(line)) {
|
|
344
|
+
const match = line.match(/from\s+["']([^"']+)["']/);
|
|
345
|
+
if (match && !match[1].startsWith(".")) imports.push(match[1]);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
const summary = { lines: lines.length };
|
|
349
|
+
if (exports.length > 0) summary.exports = exports.slice(0, 15);
|
|
350
|
+
if (imports.length > 0) summary.externalImports = [...new Set(imports)].slice(0, 10);
|
|
351
|
+
const firstComment = content.match(/^\/\/\s*(.+)/m);
|
|
352
|
+
if (firstComment) summary.description = firstComment[1].trim().slice(0, 120);
|
|
353
|
+
return summary;
|
|
354
|
+
} catch {
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function detectConventions(allFiles, cwd) {
|
|
360
|
+
const conventions = [];
|
|
361
|
+
const extensions = new Set();
|
|
362
|
+
for (const f of allFiles) {
|
|
363
|
+
const ext = path.extname(f.path);
|
|
364
|
+
if (ext) extensions.add(ext);
|
|
365
|
+
}
|
|
366
|
+
if (extensions.has(".ts") || extensions.has(".tsx")) conventions.push("TypeScript");
|
|
367
|
+
if (extensions.has(".jsx") || extensions.has(".tsx")) conventions.push("JSX/TSX (React-style)");
|
|
368
|
+
const hasTest = allFiles.some((f) =>
|
|
369
|
+
/\.(test|spec)\.[jt]sx?$/.test(f.path) || f.path.startsWith("test/") || f.path.startsWith("__tests__/"),
|
|
370
|
+
);
|
|
371
|
+
if (hasTest) {
|
|
372
|
+
const testDirs = [];
|
|
373
|
+
if (allFiles.some((f) => f.path.startsWith("test/"))) testDirs.push("test/");
|
|
374
|
+
if (allFiles.some((f) => f.path.startsWith("__tests__/"))) testDirs.push("__tests__/");
|
|
375
|
+
const colocated = allFiles.some((f) => /src\/.*\.(test|spec)\.[jt]sx?$/.test(f.path));
|
|
376
|
+
if (colocated) testDirs.push("colocated in src/");
|
|
377
|
+
conventions.push(`Tests: ${testDirs.join(", ") || "detected"}`);
|
|
378
|
+
}
|
|
379
|
+
if (fs.existsSync(path.join(cwd, ".prettierrc")) || fs.existsSync(path.join(cwd, ".prettierrc.json"))) {
|
|
380
|
+
conventions.push("Prettier formatting");
|
|
381
|
+
}
|
|
382
|
+
if (fs.existsSync(path.join(cwd, ".eslintrc.js")) || fs.existsSync(path.join(cwd, "eslint.config.js"))) {
|
|
383
|
+
conventions.push("ESLint");
|
|
384
|
+
}
|
|
385
|
+
return conventions;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function buildDirectoryTree(allFiles, maxDepth = 3) {
|
|
389
|
+
const tree = {};
|
|
390
|
+
for (const f of allFiles) {
|
|
391
|
+
const parts = f.path.split("/");
|
|
392
|
+
let node = tree;
|
|
393
|
+
for (let i = 0; i < Math.min(parts.length - 1, maxDepth); i++) {
|
|
394
|
+
if (!node[parts[i]]) node[parts[i]] = {};
|
|
395
|
+
node = node[parts[i]];
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
function render(node, indent = "") {
|
|
399
|
+
const lines = [];
|
|
400
|
+
const entries = Object.keys(node).sort();
|
|
401
|
+
for (const key of entries) {
|
|
402
|
+
const children = Object.keys(node[key]);
|
|
403
|
+
lines.push(`${indent}${key}/`);
|
|
404
|
+
if (children.length > 0 && indent.length < maxDepth * 2) {
|
|
405
|
+
lines.push(...render(node[key], indent + " "));
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return lines;
|
|
409
|
+
}
|
|
410
|
+
return render(tree);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export function generateProjectIndex(cwd) {
|
|
414
|
+
const lines = [];
|
|
415
|
+
lines.push("# Project Context");
|
|
416
|
+
lines.push(`# Auto-generated by \`aether /init\` — edit freely, this file is yours.`);
|
|
417
|
+
lines.push(`# Loaded on every agent turn to give the model deep project understanding.`);
|
|
418
|
+
lines.push("");
|
|
419
|
+
|
|
420
|
+
const { dirs, files: topFiles } = listTopLevel(cwd);
|
|
421
|
+
const frameworks = detectFrameworks(cwd, topFiles);
|
|
422
|
+
const pkg = extractPackageInfo(cwd);
|
|
423
|
+
const cargo = extractCargoInfo(cwd);
|
|
424
|
+
const goInfo = extractGoInfo(cwd);
|
|
425
|
+
const pyInfo = extractPythonInfo(cwd);
|
|
426
|
+
|
|
427
|
+
lines.push("## Overview");
|
|
428
|
+
if (pkg?.name) lines.push(`- **Package:** ${pkg.name}${pkg.version ? ` v${pkg.version}` : ""}`);
|
|
429
|
+
if (cargo?.name) lines.push(`- **Crate:** ${cargo.name}${cargo.version ? ` v${cargo.version}` : ""}`);
|
|
430
|
+
if (goInfo?.module) lines.push(`- **Module:** ${goInfo.module}${goInfo.goVersion ? ` (go ${goInfo.goVersion})` : ""}`);
|
|
431
|
+
if (pyInfo?.name) lines.push(`- **Python:** ${pyInfo.name || pyInfo.type}`);
|
|
432
|
+
if (frameworks.length > 0) lines.push(`- **Stack:** ${frameworks.join(", ")}`);
|
|
433
|
+
if (pkg?.frameworks?.length) lines.push(`- **Libraries:** ${pkg.frameworks.join(", ")}`);
|
|
434
|
+
if (pkg?.depCounts) lines.push(`- **Dependencies:** ${pkg.depCounts}`);
|
|
435
|
+
lines.push("");
|
|
436
|
+
|
|
437
|
+
const allFiles = collectSourceFiles(cwd);
|
|
438
|
+
const conventions = detectConventions(allFiles, cwd);
|
|
439
|
+
if (conventions.length > 0) {
|
|
440
|
+
lines.push("## Conventions");
|
|
441
|
+
for (const c of conventions) lines.push(`- ${c}`);
|
|
442
|
+
lines.push("");
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (pkg?.scripts?.length) {
|
|
446
|
+
lines.push("## Scripts");
|
|
447
|
+
const pkgFull = readJsonSafe(path.join(cwd, "package.json"));
|
|
448
|
+
if (pkgFull?.scripts) {
|
|
449
|
+
for (const [name, cmd] of Object.entries(pkgFull.scripts)) {
|
|
450
|
+
lines.push(`- \`npm run ${name}\` → \`${cmd}\``);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
lines.push("");
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
lines.push("## Structure");
|
|
457
|
+
const tree = buildDirectoryTree(allFiles);
|
|
458
|
+
for (const t of tree.slice(0, 60)) lines.push(t);
|
|
459
|
+
if (tree.length > 60) lines.push(`... (${tree.length - 60} more directories)`);
|
|
460
|
+
lines.push("");
|
|
461
|
+
|
|
462
|
+
const keyFiles = identifyKeyFiles(allFiles, cwd);
|
|
463
|
+
if (keyFiles.length > 0) {
|
|
464
|
+
lines.push("## Key Files");
|
|
465
|
+
for (const kf of keyFiles.slice(0, 25)) {
|
|
466
|
+
const abs = path.join(cwd, kf.replace(/\//g, path.sep));
|
|
467
|
+
const summary = summarizeFile(abs);
|
|
468
|
+
if (summary) {
|
|
469
|
+
const desc = summary.description ? ` — ${summary.description}` : "";
|
|
470
|
+
const exports = summary.exports?.length ? ` (exports: ${summary.exports.join(", ")})` : "";
|
|
471
|
+
lines.push(`- \`${kf}\` [${summary.lines} lines]${desc}${exports}`);
|
|
472
|
+
} else {
|
|
473
|
+
lines.push(`- \`${kf}\``);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
lines.push("");
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const sourceByDir = {};
|
|
480
|
+
for (const f of allFiles) {
|
|
481
|
+
const dir = f.path.includes("/") ? f.path.split("/").slice(0, -1).join("/") : ".";
|
|
482
|
+
if (!sourceByDir[dir]) sourceByDir[dir] = [];
|
|
483
|
+
sourceByDir[dir].push(f);
|
|
484
|
+
}
|
|
485
|
+
const topDirs = Object.entries(sourceByDir)
|
|
486
|
+
.sort((a, b) => b[1].length - a[1].length)
|
|
487
|
+
.slice(0, 10);
|
|
488
|
+
if (topDirs.length > 0) {
|
|
489
|
+
lines.push("## Module Map");
|
|
490
|
+
for (const [dir, dirFiles] of topDirs) {
|
|
491
|
+
const exts = {};
|
|
492
|
+
for (const f of dirFiles) {
|
|
493
|
+
const ext = path.extname(f.path) || "(no ext)";
|
|
494
|
+
exts[ext] = (exts[ext] || 0) + 1;
|
|
495
|
+
}
|
|
496
|
+
const extStr = Object.entries(exts).map(([e, c]) => `${c}${e}`).join(", ");
|
|
497
|
+
lines.push(`- \`${dir}/\` — ${dirFiles.length} files (${extStr})`);
|
|
498
|
+
}
|
|
499
|
+
lines.push("");
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
lines.push("## Notes");
|
|
503
|
+
lines.push("<!-- Add project-specific instructions, patterns, or gotchas below. -->");
|
|
504
|
+
lines.push("<!-- The agent reads this file on every turn. -->");
|
|
505
|
+
lines.push("");
|
|
506
|
+
|
|
507
|
+
return lines.join("\n");
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// ─────────── Auto-verify: detect project check commands ───────────
|
|
511
|
+
|
|
512
|
+
export function detectVerifyCommand(cwd) {
|
|
513
|
+
const pkg = readJsonSafe(path.join(cwd, "package.json"));
|
|
514
|
+
if (pkg?.scripts) {
|
|
515
|
+
if (pkg.scripts.typecheck) return { cmd: "npm run typecheck", label: "typecheck" };
|
|
516
|
+
if (pkg.scripts["type-check"]) return { cmd: "npm run type-check", label: "typecheck" };
|
|
517
|
+
}
|
|
518
|
+
if (fs.existsSync(path.join(cwd, "tsconfig.json"))) {
|
|
519
|
+
const hasTsc = pkg?.devDependencies?.typescript || pkg?.dependencies?.typescript;
|
|
520
|
+
if (hasTsc) return { cmd: "npx tsc --noEmit", label: "typecheck" };
|
|
521
|
+
}
|
|
522
|
+
if (pkg?.scripts) {
|
|
523
|
+
if (pkg.scripts.lint) return { cmd: "npm run lint", label: "lint" };
|
|
524
|
+
if (pkg.scripts.check) return { cmd: "npm run check", label: "check" };
|
|
525
|
+
}
|
|
526
|
+
const cargo = readTextSafe(path.join(cwd, "Cargo.toml"), 500);
|
|
527
|
+
if (cargo) return { cmd: "cargo check", label: "cargo check" };
|
|
528
|
+
const goMod = readTextSafe(path.join(cwd, "go.mod"), 500);
|
|
529
|
+
if (goMod) return { cmd: "go vet ./...", label: "go vet" };
|
|
530
|
+
if (fs.existsSync(path.join(cwd, "pyproject.toml"))) {
|
|
531
|
+
if (pkg?.scripts?.mypy || fs.existsSync(path.join(cwd, "mypy.ini"))) {
|
|
532
|
+
return { cmd: "mypy .", label: "mypy" };
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return null;
|
|
536
|
+
}
|
|
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
|
+
|
|
266
694
|
export function scanProjectQuick(cwd) {
|
|
267
695
|
const { dirs, files } = listTopLevel(cwd);
|
|
268
696
|
const total = dirs.length + files.length;
|
package/src/repl.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// - Bottom status line is rendered after each turn
|
|
9
9
|
// - Ctrl+C: first press cancels in-progress turn, second press exits
|
|
10
10
|
|
|
11
|
+
import fs from "node:fs";
|
|
11
12
|
import path from "node:path";
|
|
12
13
|
import { runAgent } from "./agent.js";
|
|
13
14
|
import { fetchBalance, AetherError } from "./api.js";
|
|
@@ -20,7 +21,7 @@ import {
|
|
|
20
21
|
generateSessionId, saveSession, loadSession, listSessions,
|
|
21
22
|
findRecentSession, deleteSession, extractSummary, formatAge,
|
|
22
23
|
} from "./sessions.js";
|
|
23
|
-
import { scanProjectQuick } from "./project-context.js";
|
|
24
|
+
import { scanProjectQuick, generateProjectIndex } from "./project-context.js";
|
|
24
25
|
|
|
25
26
|
const VERSION = getVersion();
|
|
26
27
|
const MODEL_NAME = "Aether Core";
|
|
@@ -44,6 +45,7 @@ const SHORTCUTS = `
|
|
|
44
45
|
${c.cyan("/yes")} Toggle auto-approve mode (skip y/N prompts)
|
|
45
46
|
${c.cyan("/turns")} ${c.gray("<n>")} Set max turns per prompt (default 25)
|
|
46
47
|
${c.cyan("/model")} ${c.gray("[core|ultra]")} Show or switch model (ultra=Opus — premium)
|
|
48
|
+
${c.cyan("/init")} Deep-scan project → generate .aether/context.md
|
|
47
49
|
${c.cyan("/sessions")} List saved sessions for this directory
|
|
48
50
|
${c.cyan("/resume")} ${c.gray("[id]")} Resume a saved session (default: most recent)
|
|
49
51
|
${c.cyan("/new")} Start a fresh session (save current first)
|
|
@@ -286,6 +288,31 @@ async function handleSlash(line, state) {
|
|
|
286
288
|
}
|
|
287
289
|
break;
|
|
288
290
|
}
|
|
291
|
+
case "init": {
|
|
292
|
+
console.log(c.gray("Scanning project..."));
|
|
293
|
+
try {
|
|
294
|
+
const content = generateProjectIndex(state.cwd);
|
|
295
|
+
const dir = path.join(state.cwd, ".aether");
|
|
296
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
297
|
+
const filePath = path.join(dir, "context.md");
|
|
298
|
+
fs.writeFileSync(filePath, content, "utf8");
|
|
299
|
+
const lineCount = content.split("\n").length;
|
|
300
|
+
console.log(c.green("●") + " " + c.bold(`.aether/context.md`) + c.gray(` generated (${lineCount} lines)`));
|
|
301
|
+
console.log(c.gray(" The agent now reads this on every turn. Edit it freely to add project-specific notes."));
|
|
302
|
+
// Add to .gitignore if not already there
|
|
303
|
+
const gitignorePath = path.join(state.cwd, ".gitignore");
|
|
304
|
+
try {
|
|
305
|
+
const existing = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, "utf8") : "";
|
|
306
|
+
if (!existing.includes(".aether/")) {
|
|
307
|
+
fs.appendFileSync(gitignorePath, `\n# Aether agent context\n.aether/\n`, "utf8");
|
|
308
|
+
console.log(c.gray(" Added .aether/ to .gitignore"));
|
|
309
|
+
}
|
|
310
|
+
} catch { /* non-fatal */ }
|
|
311
|
+
} catch (err) {
|
|
312
|
+
console.log(errorLine(`/init failed: ${err.message}`));
|
|
313
|
+
}
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
289
316
|
case "sessions": {
|
|
290
317
|
const sessions = listSessions(state.cwd);
|
|
291
318
|
if (sessions.length === 0) {
|