aether-code 0.27.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.
@@ -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.27.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.27.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
@@ -173,6 +183,24 @@ export async function runAgent({
173
183
 
174
184
  const toolCalls = res.message.tool_calls ?? [];
175
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
+ }
176
204
  if (filesTouched.size) {
177
205
  console.log("\n" + c.bold("Files") + c.gray(` in ${cwd}`));
178
206
  for (const [p, action] of filesTouched) {
@@ -230,9 +258,13 @@ export async function runAgent({
230
258
  if (call.function.name === "read_file" || call.function.name === "edit_file" || call.function.name === "write_file") {
231
259
  if (typeof args.path === "string") referencedPaths.push(args.path);
232
260
  }
233
- if (result.ok && typeof args.path === "string" && (call.function.name === "write_file" || call.function.name === "edit_file")) {
261
+ if (result.ok && typeof args.path === "string") {
234
262
  const rel = path.relative(cwd, path.resolve(cwd, args.path)) || args.path;
235
- filesTouched.set(rel, call.function.name === "write_file" ? "created" : "edited");
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
+ }
236
268
  }
237
269
  const summary = toolSummary(call.function.name, result);
238
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.27.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
- if (!abs.startsWith(cwd + path.sep) && abs !== cwd) {
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 replaces every occurrence without regex-escaping pitfalls; for
595
- // the default single-edit path occurrences === 1 so it's equivalent.
596
- const newContent = args.replace_all
597
- ? oldContent.split(args.find).join(args.replace)
598
- : oldContent.replace(args.find, args.replace);
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));