aether-code 0.26.0 → 0.28.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/bin/aether-code.js +14 -1
- package/package.json +1 -1
- package/src/agent.js +38 -3
- package/src/repl.js +1 -1
- package/src/tools.js +13 -6
package/bin/aether-code.js
CHANGED
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
import readline from "node:readline";
|
|
29
29
|
import { c, errorLine, divider, setTerminalTitle } from "../src/render.js";
|
|
30
30
|
|
|
31
|
-
const VERSION = "0.
|
|
31
|
+
const VERSION = "0.28.0";
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
34
|
* Try to start MCP servers from ~/.aether/mcp.json. Returns a started
|
|
@@ -146,6 +146,19 @@ function isSystemDir(p) {
|
|
|
146
146
|
);
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
// Always restore the terminal on exit. The raw-mode input box, the ask_user
|
|
150
|
+
// menu, and the spinner hide the cursor (\x1b[?25l) and set raw mode; if the
|
|
151
|
+
// process dies mid-prompt (an uncaught error, or the MCP SIGINT handler's
|
|
152
|
+
// process.exit(130)) their own cleanup never runs and the user's shell is left
|
|
153
|
+
// with no cursor and no echo. This idempotent backstop fires on every exit.
|
|
154
|
+
function restoreTerminal() {
|
|
155
|
+
try {
|
|
156
|
+
if (process.stdout.isTTY) process.stdout.write("\x1b[?25h"); // show cursor
|
|
157
|
+
if (process.stdin.isTTY && process.stdin.setRawMode) process.stdin.setRawMode(false);
|
|
158
|
+
} catch { /* terminal already gone — nothing to restore */ }
|
|
159
|
+
}
|
|
160
|
+
process.on("exit", restoreTerminal);
|
|
161
|
+
|
|
149
162
|
async function main() {
|
|
150
163
|
const args = parseArgs(process.argv.slice(2));
|
|
151
164
|
setTerminalTitle("Aether");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aether-code",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.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
|
@@ -83,6 +83,16 @@ export async function runAgent({
|
|
|
83
83
|
// Files the run created/edited, shown in a summary at the end so the user
|
|
84
84
|
// knows exactly what was produced and where to find it.
|
|
85
85
|
const filesTouched = new Map();
|
|
86
|
+
// Files the run read — used by the completeness guard to spot files that were
|
|
87
|
+
// read (e.g. callers in a refactor) but never edited.
|
|
88
|
+
const filesRead = new Set();
|
|
89
|
+
// Completeness guard: gemma sometimes reads files then narrates the fix as
|
|
90
|
+
// text, or refactors one file but forgets the others. If a MODIFICATION task
|
|
91
|
+
// finishes having read files it never edited, we nudge it once to finish.
|
|
92
|
+
const MOD_RE =
|
|
93
|
+
/\b(refactor|fix|add|adds?|change|update|implement|creat|writ|remov|delet|rename|extract|move|replace|build|generate|insert|append|convert|migrat|wire|hook|edit|modif|patch|integrat)/i;
|
|
94
|
+
const looksLikeModification = MOD_RE.test(initialPrompt);
|
|
95
|
+
let appliedNothingNudges = 0;
|
|
86
96
|
|
|
87
97
|
for (let i = 0; i < maxTurns; i++) {
|
|
88
98
|
// No turn header and no leading blank here — each step (assistant text and
|
|
@@ -118,6 +128,9 @@ export async function runAgent({
|
|
|
118
128
|
// be split across stream chunks) before display.
|
|
119
129
|
const clean = stripper.push(text);
|
|
120
130
|
if (!clean) return;
|
|
131
|
+
// Don't open a "● " bullet for leading whitespace (e.g. when a whole
|
|
132
|
+
// turn's text was suppressed as a leak, leaving only a stray newline).
|
|
133
|
+
if (!lastWasText && !clean.trim()) return;
|
|
121
134
|
stopSpin();
|
|
122
135
|
if (!lastWasText) {
|
|
123
136
|
process.stdout.write("\n" + c.cyan("● "));
|
|
@@ -148,7 +161,7 @@ export async function runAgent({
|
|
|
148
161
|
|
|
149
162
|
// Flush any held-back partial token, then close the line.
|
|
150
163
|
const tail = stripper.flush();
|
|
151
|
-
if (tail) {
|
|
164
|
+
if (tail && (lastWasText || tail.trim())) {
|
|
152
165
|
if (!lastWasText) { process.stdout.write("\n" + c.cyan("● ")); lastWasText = true; }
|
|
153
166
|
process.stdout.write(tail);
|
|
154
167
|
}
|
|
@@ -170,6 +183,24 @@ export async function runAgent({
|
|
|
170
183
|
|
|
171
184
|
const toolCalls = res.message.tool_calls ?? [];
|
|
172
185
|
if (toolCalls.length === 0) {
|
|
186
|
+
// Completeness guard: on a modification task, if the model read files it
|
|
187
|
+
// never edited, it likely either described the fix instead of applying it
|
|
188
|
+
// (zero edits) or refactored some files but forgot others (partial). Nudge
|
|
189
|
+
// once (at most) to finish. The "if it needs no change, just finish" escape
|
|
190
|
+
// keeps a legitimately read-for-context file from forcing a wrong edit.
|
|
191
|
+
const unedited = [...filesRead].filter((p) => !filesTouched.has(p));
|
|
192
|
+
if (looksLikeModification && appliedNothingNudges < 1 && (filesTouched.size === 0 ? referencedPaths.length > 0 : unedited.length > 0)) {
|
|
193
|
+
appliedNothingNudges++;
|
|
194
|
+
const msg =
|
|
195
|
+
filesTouched.size === 0
|
|
196
|
+
? "You read the file(s) but applied NO changes (no write_file / edit_file). This task asks you to " +
|
|
197
|
+
"modify code — ACTUALLY APPLY the change now with edit_file/write_file, don't just describe it."
|
|
198
|
+
: `You edited ${[...filesTouched.keys()].join(", ")} but read ${unedited.join(", ")} without editing ${unedited.length === 1 ? "it" : "them"}. ` +
|
|
199
|
+
`If the task needs ${unedited.length === 1 ? "that file" : "those files"} changed too (e.g. updating callers/imports after a refactor), do it NOW. ` +
|
|
200
|
+
`If ${unedited.length === 1 ? "it genuinely needs" : "they genuinely need"} no change, then finish.`;
|
|
201
|
+
messages.push({ role: "user", content: msg });
|
|
202
|
+
continue; // give the model another turn to finish
|
|
203
|
+
}
|
|
173
204
|
if (filesTouched.size) {
|
|
174
205
|
console.log("\n" + c.bold("Files") + c.gray(` in ${cwd}`));
|
|
175
206
|
for (const [p, action] of filesTouched) {
|
|
@@ -227,9 +258,13 @@ export async function runAgent({
|
|
|
227
258
|
if (call.function.name === "read_file" || call.function.name === "edit_file" || call.function.name === "write_file") {
|
|
228
259
|
if (typeof args.path === "string") referencedPaths.push(args.path);
|
|
229
260
|
}
|
|
230
|
-
if (result.ok && typeof args.path === "string"
|
|
261
|
+
if (result.ok && typeof args.path === "string") {
|
|
231
262
|
const rel = path.relative(cwd, path.resolve(cwd, args.path)) || args.path;
|
|
232
|
-
|
|
263
|
+
if (call.function.name === "write_file" || call.function.name === "edit_file") {
|
|
264
|
+
filesTouched.set(rel, call.function.name === "write_file" ? "created" : "edited");
|
|
265
|
+
} else if (call.function.name === "read_file") {
|
|
266
|
+
filesRead.add(rel);
|
|
267
|
+
}
|
|
233
268
|
}
|
|
234
269
|
const summary = toolSummary(call.function.name, result);
|
|
235
270
|
if (summary) console.log(summary);
|
package/src/repl.js
CHANGED
|
@@ -16,7 +16,7 @@ import { c, errorLine, boxLines, sideBySide } from "./render.js";
|
|
|
16
16
|
import { checkForUpdate } from "./update-check.js";
|
|
17
17
|
import { promptBox, EXIT_SIGNAL } from "./box-input.js";
|
|
18
18
|
|
|
19
|
-
const VERSION = "0.
|
|
19
|
+
const VERSION = "0.28.0";
|
|
20
20
|
const MODEL_NAME = "Aether Core";
|
|
21
21
|
|
|
22
22
|
const SHORTCUTS = `
|
package/src/tools.js
CHANGED
|
@@ -336,7 +336,14 @@ function resolveSafe(rel, opts) {
|
|
|
336
336
|
const abs = path.isAbsolute(rel) ? path.normalize(rel) : path.resolve(opts.cwd, rel);
|
|
337
337
|
if (!opts.unsafePaths) {
|
|
338
338
|
const cwd = path.resolve(opts.cwd);
|
|
339
|
-
|
|
339
|
+
// Use path.relative (not startsWith) so Windows drive-relative inputs like
|
|
340
|
+
// "C:evil.txt" — which path.isAbsolute() reports as NON-absolute and
|
|
341
|
+
// path.resolve() may anchor to a drive's own cwd outside the project — are
|
|
342
|
+
// caught. A path is inside cwd only if its relative form doesn't climb out
|
|
343
|
+
// (no leading "..") and isn't itself absolute (different drive/root).
|
|
344
|
+
const relToCwd = path.relative(cwd, abs);
|
|
345
|
+
const escapes = relToCwd === ".." || relToCwd.startsWith(".." + path.sep) || path.isAbsolute(relToCwd);
|
|
346
|
+
if (abs !== cwd && escapes) {
|
|
340
347
|
throw new Error(
|
|
341
348
|
`Refusing to touch path outside cwd: ${abs}\n Run with --unsafe-paths if you really mean this.`,
|
|
342
349
|
);
|
|
@@ -591,11 +598,11 @@ async function editFile(args, opts) {
|
|
|
591
598
|
output: `\`find\` text appears ${occurrences} times — must be unique. Add more context to disambiguate, or set replace_all: true to replace all ${occurrences}.`,
|
|
592
599
|
};
|
|
593
600
|
}
|
|
594
|
-
// split/join
|
|
595
|
-
// the
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
601
|
+
// split/join for BOTH paths: String.replace(str, str) would interpret special
|
|
602
|
+
// patterns in the replacement ($&, $`, $', $$) and silently corrupt code that
|
|
603
|
+
// contains them (regex, jQuery $, template strings). split/join is literal.
|
|
604
|
+
// On the single-edit path occurrences === 1, so it's equivalent + safe.
|
|
605
|
+
const newContent = oldContent.split(args.find).join(args.replace);
|
|
599
606
|
const rel = path.relative(opts.cwd, abs);
|
|
600
607
|
console.log(c.dim(`edit ${rel}${args.replace_all ? ` (${occurrences} occurrences)` : ""}`));
|
|
601
608
|
console.log(unifiedDiff(oldContent, newContent, rel));
|