aether-code 0.38.0 → 0.39.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 +53 -1
- package/src/project-context.js +272 -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.39.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 } 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,7 @@ 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;
|
|
112
114
|
|
|
113
115
|
for (let i = 0; i < maxTurns; i++) {
|
|
114
116
|
// No turn header and no leading blank here — each step (assistant text and
|
|
@@ -218,6 +220,30 @@ export async function runAgent({
|
|
|
218
220
|
messages.push({ role: "user", content: msg });
|
|
219
221
|
continue; // give the model another turn to finish
|
|
220
222
|
}
|
|
223
|
+
// Auto-verify: if we touched files, run the project's typecheck/lint
|
|
224
|
+
// and feed errors back so the model can self-correct without the user
|
|
225
|
+
// having to say "fix that." At most 2 verify rounds to avoid infinite loops.
|
|
226
|
+
if (filesTouched.size > 0 && verifyRounds < 2) {
|
|
227
|
+
const verifyResult = await runVerify(cwd);
|
|
228
|
+
if (verifyResult && !verifyResult.ok) {
|
|
229
|
+
verifyRounds++;
|
|
230
|
+
console.log("\n" + c.yellow("●") + " " + c.bold("auto-verify") + c.gray(` (${verifyResult.label})`));
|
|
231
|
+
console.log(c.red(" └─") + " " + c.gray("errors found — feeding back to fix"));
|
|
232
|
+
const errPreview = verifyResult.output.length > 4000
|
|
233
|
+
? verifyResult.output.slice(0, 4000) + "\n…(truncated)"
|
|
234
|
+
: verifyResult.output;
|
|
235
|
+
messages.push({
|
|
236
|
+
role: "user",
|
|
237
|
+
content:
|
|
238
|
+
`AUTO-VERIFY FAILED (${verifyResult.label}):\n\`\`\`\n${errPreview}\n\`\`\`\n` +
|
|
239
|
+
`Fix these errors NOW using edit_file. Do not explain — just fix and move on.`,
|
|
240
|
+
});
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
if (verifyResult && verifyResult.ok) {
|
|
244
|
+
console.log("\n" + c.green("●") + " " + c.bold("auto-verify") + c.gray(` (${verifyResult.label}) — passed`));
|
|
245
|
+
}
|
|
246
|
+
}
|
|
221
247
|
if (filesTouched.size) {
|
|
222
248
|
console.log("\n" + c.bold("Files") + c.gray(` in ${cwd}`));
|
|
223
249
|
for (const [p, action] of filesTouched) {
|
|
@@ -298,6 +324,32 @@ export async function runAgent({
|
|
|
298
324
|
return { ok: false, error: new Error("Max turns reached"), totalCredits, totalIn, totalOut, balance: lastBalance, messages };
|
|
299
325
|
}
|
|
300
326
|
|
|
327
|
+
// Auto-verify: run the project's typecheck/lint command silently and return
|
|
328
|
+
// { ok, output, label }. Returns null if no verify command is detected.
|
|
329
|
+
let cachedVerify = undefined;
|
|
330
|
+
async function runVerify(cwd) {
|
|
331
|
+
if (cachedVerify === undefined) cachedVerify = detectVerifyCommand(cwd);
|
|
332
|
+
if (!cachedVerify) return null;
|
|
333
|
+
const { cmd, label } = cachedVerify;
|
|
334
|
+
return new Promise((resolve) => {
|
|
335
|
+
const child = spawn(cmd, [], { cwd, shell: true, stdio: ["ignore", "pipe", "pipe"] });
|
|
336
|
+
let stdout = "";
|
|
337
|
+
let stderr = "";
|
|
338
|
+
const timer = setTimeout(() => { child.kill("SIGTERM"); }, 60_000);
|
|
339
|
+
child.stdout.on("data", (d) => { stdout += d.toString(); });
|
|
340
|
+
child.stderr.on("data", (d) => { stderr += d.toString(); });
|
|
341
|
+
child.on("close", (code) => {
|
|
342
|
+
clearTimeout(timer);
|
|
343
|
+
const output = (stdout + "\n" + stderr).trim();
|
|
344
|
+
resolve({ ok: code === 0, output, label });
|
|
345
|
+
});
|
|
346
|
+
child.on("error", () => {
|
|
347
|
+
clearTimeout(timer);
|
|
348
|
+
resolve(null);
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
301
353
|
/**
|
|
302
354
|
* Per-turn skill injection. Selects skills against the latest user message
|
|
303
355
|
* + paths the model has touched, then prepends matching bodies onto the
|
package/src/project-context.js
CHANGED
|
@@ -263,6 +263,278 @@ 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
|
+
|
|
266
538
|
export function scanProjectQuick(cwd) {
|
|
267
539
|
const { dirs, files } = listTopLevel(cwd);
|
|
268
540
|
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) {
|