aether-code 0.24.0 → 0.26.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 +30 -2
- package/package.json +1 -1
- package/src/agent.js +13 -0
- package/src/render.js +24 -6
- package/src/repl.js +33 -9
package/bin/aether-code.js
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
|
|
10
10
|
import process from "node:process";
|
|
11
11
|
import path from "node:path";
|
|
12
|
+
import os from "node:os";
|
|
13
|
+
import fs from "node:fs";
|
|
12
14
|
import { runAgent } from "../src/agent.js";
|
|
13
15
|
import { runRepl } from "../src/repl.js";
|
|
14
16
|
import { runSetup } from "../src/setup.js";
|
|
@@ -26,7 +28,7 @@ import {
|
|
|
26
28
|
import readline from "node:readline";
|
|
27
29
|
import { c, errorLine, divider, setTerminalTitle } from "../src/render.js";
|
|
28
30
|
|
|
29
|
-
const VERSION = "0.
|
|
31
|
+
const VERSION = "0.26.0";
|
|
30
32
|
|
|
31
33
|
/**
|
|
32
34
|
* Try to start MCP servers from ~/.aether/mcp.json. Returns a started
|
|
@@ -132,6 +134,18 @@ function die(msg, code = 1) {
|
|
|
132
134
|
process.exit(code);
|
|
133
135
|
}
|
|
134
136
|
|
|
137
|
+
// True for Windows system folders we should never write a user's project into.
|
|
138
|
+
function isSystemDir(p) {
|
|
139
|
+
const low = p.toLowerCase().replace(/[\\/]+$/, "").replace(/\//g, "\\");
|
|
140
|
+
return (
|
|
141
|
+
low.endsWith("\\windows\\system32") ||
|
|
142
|
+
low.endsWith("\\windows\\syswow64") ||
|
|
143
|
+
low.endsWith("\\windows") ||
|
|
144
|
+
low.endsWith("\\system32") ||
|
|
145
|
+
/\\program files( \(x86\))?$/.test(low)
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
135
149
|
async function main() {
|
|
136
150
|
const args = parseArgs(process.argv.slice(2));
|
|
137
151
|
setTerminalTitle("Aether");
|
|
@@ -145,7 +159,16 @@ async function main() {
|
|
|
145
159
|
return;
|
|
146
160
|
}
|
|
147
161
|
|
|
148
|
-
|
|
162
|
+
// Where files land. If the user didn't pass --cwd and we were launched from a
|
|
163
|
+
// Windows system folder (admin cmd opens in C:\WINDOWS\system32), don't dump
|
|
164
|
+
// their project there — they'd never find it. Work in the Desktop instead.
|
|
165
|
+
let cwd = args.flags.cwd ? path.resolve(args.flags.cwd) : process.cwd();
|
|
166
|
+
let redirectedFrom = null;
|
|
167
|
+
if (!args.flags.cwd && isSystemDir(cwd)) {
|
|
168
|
+
const desktop = path.join(os.homedir(), "Desktop");
|
|
169
|
+
const target = fs.existsSync(desktop) ? desktop : os.homedir();
|
|
170
|
+
if (target.toLowerCase() !== cwd.toLowerCase()) { redirectedFrom = cwd; cwd = target; }
|
|
171
|
+
}
|
|
149
172
|
// Skip-permissions by DEFAULT (like `claude --dangerously-skip-permissions`):
|
|
150
173
|
// auto-approve writes/shell and don't sandbox file paths, so the agent can
|
|
151
174
|
// just build. Opt back in with --review (y/N before each action) and
|
|
@@ -184,6 +207,10 @@ async function main() {
|
|
|
184
207
|
if (!prompt) {
|
|
185
208
|
if (cwd !== process.cwd()) process.chdir(cwd);
|
|
186
209
|
const mcpManager = await bootMcp();
|
|
210
|
+
if (redirectedFrom) {
|
|
211
|
+
console.log(c.gray(`Note: launched from a system folder (${redirectedFrom}).`));
|
|
212
|
+
console.log(c.gray(`Your files will be saved to ${c.cyan(cwd)} so you can find them. Use ${c.cyan("--cwd <path>")} or ${c.cyan("/cwd")} to change.`));
|
|
213
|
+
}
|
|
187
214
|
await runRepl({ cwd, autoYes, unsafePaths, maxTurns, mcpManager });
|
|
188
215
|
return;
|
|
189
216
|
}
|
|
@@ -198,6 +225,7 @@ async function main() {
|
|
|
198
225
|
console.log(divider());
|
|
199
226
|
const modeLabel = autoYes && unsafePaths ? " · skip-permissions" : `${autoYes ? " · auto-yes" : " · review"}${unsafePaths ? "" : " · sandboxed"}`;
|
|
200
227
|
console.log(c.magenta(c.bold("aether-code")) + c.gray(` · cwd ${cwd}${modeLabel}`));
|
|
228
|
+
if (redirectedFrom) console.log(c.gray(`(launched from ${redirectedFrom} — saving to ${cwd} instead)`));
|
|
201
229
|
console.log(c.gray(`task: `) + prompt);
|
|
202
230
|
console.log(divider());
|
|
203
231
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aether-code",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.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
|
@@ -80,6 +80,9 @@ export async function runAgent({
|
|
|
80
80
|
// Loop guard: count identical (name+args) tool calls across the whole run so a
|
|
81
81
|
// confused model can't burn turns re-running the same call (e.g. glob *.md x9).
|
|
82
82
|
const callCounts = new Map();
|
|
83
|
+
// Files the run created/edited, shown in a summary at the end so the user
|
|
84
|
+
// knows exactly what was produced and where to find it.
|
|
85
|
+
const filesTouched = new Map();
|
|
83
86
|
|
|
84
87
|
for (let i = 0; i < maxTurns; i++) {
|
|
85
88
|
// No turn header and no leading blank here — each step (assistant text and
|
|
@@ -167,6 +170,12 @@ export async function runAgent({
|
|
|
167
170
|
|
|
168
171
|
const toolCalls = res.message.tool_calls ?? [];
|
|
169
172
|
if (toolCalls.length === 0) {
|
|
173
|
+
if (filesTouched.size) {
|
|
174
|
+
console.log("\n" + c.bold("Files") + c.gray(` in ${cwd}`));
|
|
175
|
+
for (const [p, action] of filesTouched) {
|
|
176
|
+
console.log(" " + c.green("●") + " " + p + c.gray(` — ${action}`));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
170
179
|
return { ok: true, totalCredits, totalIn, totalOut, turns: i + 1, balance: lastBalance, messages };
|
|
171
180
|
}
|
|
172
181
|
|
|
@@ -218,6 +227,10 @@ export async function runAgent({
|
|
|
218
227
|
if (call.function.name === "read_file" || call.function.name === "edit_file" || call.function.name === "write_file") {
|
|
219
228
|
if (typeof args.path === "string") referencedPaths.push(args.path);
|
|
220
229
|
}
|
|
230
|
+
if (result.ok && typeof args.path === "string" && (call.function.name === "write_file" || call.function.name === "edit_file")) {
|
|
231
|
+
const rel = path.relative(cwd, path.resolve(cwd, args.path)) || args.path;
|
|
232
|
+
filesTouched.set(rel, call.function.name === "write_file" ? "created" : "edited");
|
|
233
|
+
}
|
|
221
234
|
const summary = toolSummary(call.function.name, result);
|
|
222
235
|
if (summary) console.log(summary);
|
|
223
236
|
|
package/src/render.js
CHANGED
|
@@ -26,17 +26,35 @@ export function divider() {
|
|
|
26
26
|
|
|
27
27
|
const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
28
28
|
|
|
29
|
-
// Draw a bordered box around content lines
|
|
30
|
-
//
|
|
31
|
-
export function
|
|
32
|
-
const inner = Math.max(0, ...lines.map((l) => stripAnsi(l).length));
|
|
29
|
+
// Draw a bordered box around content lines, returning the rows as an array (so
|
|
30
|
+
// boxes can be composed side by side). Rounded box-drawing chars.
|
|
31
|
+
export function boxLines(lines, { color = c.gray, padX = 2, minInner = 0 } = {}) {
|
|
32
|
+
const inner = Math.max(minInner, 0, ...lines.map((l) => stripAnsi(l).length));
|
|
33
33
|
const w = inner + padX * 2;
|
|
34
34
|
const top = color("╭" + "─".repeat(w) + "╮");
|
|
35
35
|
const bot = color("╰" + "─".repeat(w) + "╯");
|
|
36
36
|
const mid = lines.map(
|
|
37
37
|
(l) => color("│") + " ".repeat(padX) + l + " ".repeat(inner - stripAnsi(l).length + padX) + color("│"),
|
|
38
38
|
);
|
|
39
|
-
return [top, ...mid, bot]
|
|
39
|
+
return [top, ...mid, bot];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function box(lines, opts) {
|
|
43
|
+
return boxLines(lines, opts).join("\n");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Join two blocks of lines horizontally with a gap, padding the left block to a
|
|
47
|
+
// uniform width so the right block aligns. Shorter block is padded with blanks.
|
|
48
|
+
export function sideBySide(a, b, gap = 3) {
|
|
49
|
+
const wa = Math.max(0, ...a.map((l) => stripAnsi(l).length));
|
|
50
|
+
const h = Math.max(a.length, b.length);
|
|
51
|
+
const out = [];
|
|
52
|
+
for (let i = 0; i < h; i++) {
|
|
53
|
+
const la = a[i] ?? "";
|
|
54
|
+
const lb = b[i] ?? "";
|
|
55
|
+
out.push(la + " ".repeat(wa - stripAnsi(la).length + gap) + lb);
|
|
56
|
+
}
|
|
57
|
+
return out.join("\n");
|
|
40
58
|
}
|
|
41
59
|
|
|
42
60
|
export function turn(n) {
|
|
@@ -63,7 +81,7 @@ export function toolLabel(name, args) {
|
|
|
63
81
|
case "write_file": return `${verb("write")} ${arg(a.path)}`;
|
|
64
82
|
case "edit_file": return `${verb("edit")} ${arg(a.path)}`;
|
|
65
83
|
case "list_dir": return `${verb("list")} ${arg(a.path)}`;
|
|
66
|
-
case "glob_files": return `${verb("
|
|
84
|
+
case "glob_files": return `${verb("find")} ${arg(a.pattern)}`;
|
|
67
85
|
case "search_files": return `${verb("search")} ${arg(`/${a.pattern}/ in ${a.path ?? "."}`)}`;
|
|
68
86
|
case "run_shell": return `${verb("run")} ${arg(a.command)}`;
|
|
69
87
|
case "web_search": return `${verb("search web")} ${arg(JSON.stringify(a.query ?? ""))}`;
|
package/src/repl.js
CHANGED
|
@@ -12,11 +12,11 @@ import path from "node:path";
|
|
|
12
12
|
import { runAgent } from "./agent.js";
|
|
13
13
|
import { fetchBalance, AetherError } from "./api.js";
|
|
14
14
|
import { runSetup } from "./setup.js";
|
|
15
|
-
import { c, errorLine,
|
|
15
|
+
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.26.0";
|
|
20
20
|
const MODEL_NAME = "Aether Core";
|
|
21
21
|
|
|
22
22
|
const SHORTCUTS = `
|
|
@@ -213,16 +213,40 @@ async function handleSlash(line, state) {
|
|
|
213
213
|
const visLen = (s) => s.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
214
214
|
const shortenPath = (p, max) => (p.length <= max ? p : "..." + p.slice(p.length - max + 3));
|
|
215
215
|
|
|
216
|
+
// Short, hand-curated highlights shown in the "What's new" box. Update with
|
|
217
|
+
// notable releases — keep it to ~3 lines so it balances the banner height.
|
|
218
|
+
const WHATS_NEW = [
|
|
219
|
+
"Files now save to your Desktop",
|
|
220
|
+
"Live thinking spinner + loop guard",
|
|
221
|
+
"Asks you when a task is unclear",
|
|
222
|
+
];
|
|
223
|
+
|
|
216
224
|
function printBanner(state) {
|
|
217
225
|
const mode = state.autoYes ? (state.unsafePaths ? "skip-permissions" : "auto-yes") : "review mode";
|
|
218
|
-
const credits = state.balance != null ? `${state.balance.toLocaleString()} credits
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
226
|
+
const credits = state.balance != null ? `${state.balance.toLocaleString()} credits` : "";
|
|
227
|
+
const left = boxLines(
|
|
228
|
+
[
|
|
229
|
+
c.bold(c.magenta("aether")) + c.gray(" · ") + c.bold(MODEL_NAME) + c.gray(" v" + VERSION),
|
|
230
|
+
c.gray("uncensored AI coding agent"),
|
|
231
|
+
c.gray((credits ? credits + " · " : "") + mode),
|
|
232
|
+
c.gray(shortenPath(state.cwd, 52)),
|
|
233
|
+
],
|
|
234
|
+
{ color: c.magenta, padX: 2 },
|
|
235
|
+
);
|
|
236
|
+
const right = boxLines(
|
|
237
|
+
[c.bold(c.cyan("What's new")), ...WHATS_NEW.map((s) => c.gray("• " + s))],
|
|
238
|
+
{ color: c.cyan, padX: 2 },
|
|
239
|
+
);
|
|
240
|
+
|
|
224
241
|
console.log("");
|
|
225
|
-
|
|
242
|
+
const cols = process.stdout.columns || 80;
|
|
243
|
+
// Only place the two boxes side by side when there's room; otherwise the
|
|
244
|
+
// banner alone (narrow terminals would wrap and tear the second box).
|
|
245
|
+
if (cols >= visLen(left[0]) + visLen(right[0]) + 4) {
|
|
246
|
+
console.log(sideBySide(left, right, 3));
|
|
247
|
+
} else {
|
|
248
|
+
console.log(left.join("\n"));
|
|
249
|
+
}
|
|
226
250
|
console.log("");
|
|
227
251
|
}
|
|
228
252
|
|