aether-code 0.43.2 → 0.43.4
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 +19 -12
- package/package.json +1 -1
- package/src/agent.js +31 -18
- package/src/render.js +24 -23
- package/src/repl.js +4 -7
package/bin/aether-code.js
CHANGED
|
@@ -31,6 +31,14 @@ import { getVersion } from "../src/version.js";
|
|
|
31
31
|
|
|
32
32
|
const VERSION = getVersion();
|
|
33
33
|
|
|
34
|
+
function formatElapsed(ms) {
|
|
35
|
+
const s = Math.round(ms / 1000);
|
|
36
|
+
if (s < 60) return `${s}s`;
|
|
37
|
+
const m = Math.floor(s / 60);
|
|
38
|
+
const rem = s % 60;
|
|
39
|
+
return rem > 0 ? `${m}m ${rem}s` : `${m}m`;
|
|
40
|
+
}
|
|
41
|
+
|
|
34
42
|
/**
|
|
35
43
|
* Try to start MCP servers from ~/.aether/mcp.json. Returns a started
|
|
36
44
|
* MCPManager (possibly with zero servers) or null if no config exists.
|
|
@@ -245,12 +253,12 @@ async function main() {
|
|
|
245
253
|
if (!ok) process.exit(1);
|
|
246
254
|
}
|
|
247
255
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
console.log(c.magenta(c.bold("aether
|
|
256
|
+
const modeLabel = autoYes && unsafePaths ? "skip-permissions" : `${autoYes ? "auto-yes" : "review"}${unsafePaths ? "" : " · sandboxed"}`;
|
|
257
|
+
console.log("");
|
|
258
|
+
console.log(c.magenta(c.bold("aether")) + c.gray(` · ${modeLabel} · cwd ${cwd}`));
|
|
251
259
|
if (redirectedFrom) console.log(c.gray(`(launched from ${redirectedFrom} — saving to ${cwd} instead)`));
|
|
252
|
-
console.log(c.
|
|
253
|
-
console.log(
|
|
260
|
+
console.log(c.dim(prompt));
|
|
261
|
+
console.log("");
|
|
254
262
|
|
|
255
263
|
const mcpManager = await bootMcp();
|
|
256
264
|
const result = await runAgent({
|
|
@@ -264,17 +272,16 @@ async function main() {
|
|
|
264
272
|
});
|
|
265
273
|
if (mcpManager) await mcpManager.shutdown().catch(() => {});
|
|
266
274
|
|
|
267
|
-
|
|
275
|
+
const elapsed = result.elapsed ? formatElapsed(result.elapsed) : "";
|
|
276
|
+
console.log("");
|
|
268
277
|
if (result.ok) {
|
|
269
|
-
|
|
270
|
-
if (typeof result.balance === "number") {
|
|
271
|
-
|
|
272
|
-
}
|
|
278
|
+
const parts = [elapsed, `${result.totalCredits} credits`];
|
|
279
|
+
if (typeof result.balance === "number") parts.push(`balance: ${result.balance.toLocaleString()}`);
|
|
280
|
+
console.log(c.dim(`Done in ${parts.filter(Boolean).join(" · ")}`));
|
|
273
281
|
} else {
|
|
274
|
-
console.log(c.red(
|
|
282
|
+
console.log(c.red("Stopped") + c.gray(` ${result.totalCredits} credits used` + (elapsed ? ` · ${elapsed}` : "")));
|
|
275
283
|
if (result.error) console.log(errorLine(result.error.message));
|
|
276
284
|
}
|
|
277
|
-
console.log(divider());
|
|
278
285
|
}
|
|
279
286
|
|
|
280
287
|
async function handleConfig(rest) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aether-code",
|
|
3
|
-
"version": "0.43.
|
|
3
|
+
"version": "0.43.4",
|
|
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
|
@@ -129,6 +129,7 @@ export async function runAgent({
|
|
|
129
129
|
let totalIn = 0;
|
|
130
130
|
let totalOut = 0;
|
|
131
131
|
let lastBalance = null;
|
|
132
|
+
const t0 = Date.now();
|
|
132
133
|
// Loop guard: count identical (name+args) tool calls across the whole run so a
|
|
133
134
|
// confused model can't burn turns re-running the same call (e.g. glob *.md x9).
|
|
134
135
|
const callCounts = new Map();
|
|
@@ -159,6 +160,8 @@ export async function runAgent({
|
|
|
159
160
|
const announced = new Set();
|
|
160
161
|
let lastWasText = false;
|
|
161
162
|
const stripper = makeTokenStripper();
|
|
163
|
+
let textEmitted = false; // track if real content has been emitted this turn
|
|
164
|
+
const CHANNEL_WORD_SET = new Set(["thought","analysis","final","commentary","thinking","return","assistant","user","system","developer","tool"]);
|
|
162
165
|
|
|
163
166
|
// Select skills for this turn against the current user prompt + any
|
|
164
167
|
// paths the model has read so far. Prepend the matching skills' bodies
|
|
@@ -179,16 +182,23 @@ export async function runAgent({
|
|
|
179
182
|
tools,
|
|
180
183
|
model,
|
|
181
184
|
onDelta: (text) => {
|
|
182
|
-
// Buffered strip of leaked model channel/control tokens (which can
|
|
183
|
-
// be split across stream chunks) before display.
|
|
184
185
|
const clean = stripper.push(text);
|
|
185
186
|
if (!clean) return;
|
|
186
|
-
//
|
|
187
|
-
//
|
|
187
|
+
// Before any real content, strip stray channel name words that leak
|
|
188
|
+
// as the first tokens (e.g. "thought\nthought\n" from harmony).
|
|
189
|
+
if (!textEmitted) {
|
|
190
|
+
const stripped = clean.replace(/^[\s]*(thought|analysis|final|commentary|thinking|return|assistant|user|system|developer|tool)[\s]*/gi, "");
|
|
191
|
+
if (!stripped.trim()) return;
|
|
192
|
+
textEmitted = true;
|
|
193
|
+
stopSpin();
|
|
194
|
+
process.stdout.write("\n" + stripped);
|
|
195
|
+
lastWasText = true;
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
188
198
|
if (!lastWasText && !clean.trim()) return;
|
|
189
199
|
stopSpin();
|
|
190
200
|
if (!lastWasText) {
|
|
191
|
-
process.stdout.write("\n"
|
|
201
|
+
process.stdout.write("\n");
|
|
192
202
|
lastWasText = true;
|
|
193
203
|
}
|
|
194
204
|
process.stdout.write(clean);
|
|
@@ -208,16 +218,19 @@ export async function runAgent({
|
|
|
208
218
|
} catch (err) {
|
|
209
219
|
stopSpin();
|
|
210
220
|
if (err instanceof AetherError) {
|
|
211
|
-
return { ok: false, error: err, totalCredits, totalIn, totalOut, balance: lastBalance, messages };
|
|
221
|
+
return { ok: false, error: err, totalCredits, totalIn, totalOut, balance: lastBalance, messages, elapsed: Date.now() - t0 };
|
|
212
222
|
}
|
|
213
223
|
throw err;
|
|
214
224
|
}
|
|
215
225
|
stopSpin(); // ensure it's cleared even if the turn produced no output
|
|
216
226
|
|
|
217
227
|
// Flush any held-back partial token, then close the line.
|
|
218
|
-
|
|
228
|
+
let tail = stripper.flush();
|
|
229
|
+
if (tail && !textEmitted) {
|
|
230
|
+
tail = tail.replace(/^[\s]*(thought|analysis|final|commentary|thinking|return|assistant|user|system|developer|tool)[\s]*/gi, "");
|
|
231
|
+
}
|
|
219
232
|
if (tail && (lastWasText || tail.trim())) {
|
|
220
|
-
if (!lastWasText) { process.stdout.write("\n"
|
|
233
|
+
if (!lastWasText) { process.stdout.write("\n"); lastWasText = true; }
|
|
221
234
|
process.stdout.write(tail);
|
|
222
235
|
}
|
|
223
236
|
if (lastWasText) process.stdout.write("\n");
|
|
@@ -228,10 +241,10 @@ export async function runAgent({
|
|
|
228
241
|
if (typeof res.balanceAfter === "number") lastBalance = res.balanceAfter;
|
|
229
242
|
onTokens({ totalCredits, totalIn, totalOut, balance: lastBalance });
|
|
230
243
|
|
|
231
|
-
// Per-turn cost —
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
244
|
+
// Per-turn cost — only shown when credits are high enough to warrant attention
|
|
245
|
+
if (turnCredits > 20) {
|
|
246
|
+
console.log(c.gray(` ${turnCredits} credits this turn (${totalCredits} total)`));
|
|
247
|
+
}
|
|
235
248
|
|
|
236
249
|
// Push assistant message into history
|
|
237
250
|
messages.push({
|
|
@@ -252,19 +265,19 @@ export async function runAgent({
|
|
|
252
265
|
// Credit cap — hard stop to prevent runaway cost
|
|
253
266
|
if (maxCredits > 0 && totalCredits >= maxCredits) {
|
|
254
267
|
console.log(c.red(`\n[Credit budget exhausted: ${totalCredits} credits used, limit was ${maxCredits}. Stopping.]`));
|
|
255
|
-
return { ok: true, totalCredits, totalIn, totalOut, balance: lastBalance, messages };
|
|
268
|
+
return { ok: true, totalCredits, totalIn, totalOut, balance: lastBalance, messages, elapsed: Date.now() - t0 };
|
|
256
269
|
}
|
|
257
270
|
|
|
258
271
|
// Tool call count cap
|
|
259
272
|
if (toolCalls.length > 0 && totalToolCalls + toolCalls.length > MAX_TOTAL_TOOL_CALLS) {
|
|
260
273
|
console.log(c.red(`\n[Tool call limit reached (${MAX_TOTAL_TOOL_CALLS}). Stopping.]`));
|
|
261
|
-
return { ok: true, totalCredits, totalIn, totalOut, balance: lastBalance, messages };
|
|
274
|
+
return { ok: true, totalCredits, totalIn, totalOut, balance: lastBalance, messages, elapsed: Date.now() - t0 };
|
|
262
275
|
}
|
|
263
276
|
|
|
264
277
|
// Consecutive tool-only turns cap (model stuck in a tool loop with no text)
|
|
265
278
|
if (consecutiveToolOnlyTurns >= MAX_CONSECUTIVE_TOOL_ONLY) {
|
|
266
279
|
console.log(c.red(`\n[Too many consecutive tool-only turns (${MAX_CONSECUTIVE_TOOL_ONLY}). Stopping.]`));
|
|
267
|
-
return { ok: true, totalCredits, totalIn, totalOut, balance: lastBalance, messages };
|
|
280
|
+
return { ok: true, totalCredits, totalIn, totalOut, balance: lastBalance, messages, elapsed: Date.now() - t0 };
|
|
268
281
|
}
|
|
269
282
|
if (toolCalls.length === 0) {
|
|
270
283
|
// Completeness guard: on a modification task, if the model read files it
|
|
@@ -373,7 +386,7 @@ export async function runAgent({
|
|
|
373
386
|
console.log(" " + c.green("●") + " " + p + c.gray(` — ${action}`));
|
|
374
387
|
}
|
|
375
388
|
}
|
|
376
|
-
return { ok: true, totalCredits, totalIn, totalOut, turns: i + 1, balance: lastBalance, messages };
|
|
389
|
+
return { ok: true, totalCredits, totalIn, totalOut, turns: i + 1, balance: lastBalance, messages, elapsed: Date.now() - t0 };
|
|
377
390
|
}
|
|
378
391
|
|
|
379
392
|
// Execute each tool call. Show the actual args (now that we have them
|
|
@@ -384,7 +397,7 @@ export async function runAgent({
|
|
|
384
397
|
try { args = JSON.parse(call.function.arguments || "{}"); } catch { /* leave empty */ }
|
|
385
398
|
// todo_write renders its own Plan box, so it returns an empty label.
|
|
386
399
|
const label = toolLabel(call.function.name, args);
|
|
387
|
-
if (label) console.log("\n" + c.
|
|
400
|
+
if (label) console.log("\n" + c.green("●") + " " + label);
|
|
388
401
|
|
|
389
402
|
// Loop guard — short-circuit a tool call we've already run 3+ times with
|
|
390
403
|
// the same args, and tell the model to change approach instead of looping.
|
|
@@ -445,7 +458,7 @@ export async function runAgent({
|
|
|
445
458
|
}
|
|
446
459
|
|
|
447
460
|
console.log(c.yellow(`\nReached max turns (${maxTurns}). Stopping.`));
|
|
448
|
-
return { ok: false, error: new Error("Max turns reached"), totalCredits, totalIn, totalOut, balance: lastBalance, messages };
|
|
461
|
+
return { ok: false, error: new Error("Max turns reached"), totalCredits, totalIn, totalOut, balance: lastBalance, messages, elapsed: Date.now() - t0 };
|
|
449
462
|
}
|
|
450
463
|
|
|
451
464
|
// A build/verify command failing because the TOOLCHAIN ITSELF is missing (e.g.
|
package/src/render.js
CHANGED
|
@@ -74,27 +74,27 @@ const ellip = (s, n) => (s && s.length > n ? s.slice(0, n - 1) + "…" : s || ""
|
|
|
74
74
|
// dumping raw JSON (which buried file contents / queries in noise).
|
|
75
75
|
export function toolLabel(name, args) {
|
|
76
76
|
const a = args || {};
|
|
77
|
-
const verb = (v) => c.
|
|
78
|
-
const arg = (s) =>
|
|
77
|
+
const verb = (v) => c.bold(v);
|
|
78
|
+
const arg = (s) => ellip(String(s), 72);
|
|
79
79
|
switch (name) {
|
|
80
|
-
case "read_file": return `${verb("
|
|
81
|
-
case "write_file": return `${verb("
|
|
82
|
-
case "edit_file": return `${verb("
|
|
83
|
-
case "list_dir": return `${verb("
|
|
84
|
-
case "glob_files": return `${verb("
|
|
85
|
-
case "search_files": return `${verb("
|
|
86
|
-
case "run_shell": return `${verb("
|
|
87
|
-
case "web_search": return `${verb("
|
|
88
|
-
case "web_fetch": return `${verb("
|
|
89
|
-
case "todo_write": return "";
|
|
90
|
-
case "ask_user": return "";
|
|
91
|
-
case "git_status": return `${verb("
|
|
92
|
-
case "git_diff": return `${verb("
|
|
93
|
-
case "git_log": return `${verb("
|
|
94
|
-
case "git_commit": return `${verb("
|
|
95
|
-
case "git_branch": return `${verb("
|
|
96
|
-
case "git_push": return `${verb("
|
|
97
|
-
case "git_create_pr": return `${verb("
|
|
80
|
+
case "read_file": return `${verb("Read")} ${arg(a.path)}`;
|
|
81
|
+
case "write_file": return `${verb("Write")} ${arg(a.path)}`;
|
|
82
|
+
case "edit_file": return `${verb("Edit")} ${arg(a.path)}`;
|
|
83
|
+
case "list_dir": return `${verb("List")} ${arg(a.path)}`;
|
|
84
|
+
case "glob_files": return `${verb("Glob")} ${arg(a.pattern)}`;
|
|
85
|
+
case "search_files": return `${verb("Grep")} ${arg(`/${a.pattern}/${a.path ? ` in ${a.path}` : ""}`)}`;
|
|
86
|
+
case "run_shell": return `${verb("Bash")} ${c.dim(ellip(String(a.command), 72))}`;
|
|
87
|
+
case "web_search": return `${verb("Search")} ${arg(a.query ?? "")}`;
|
|
88
|
+
case "web_fetch": return `${verb("Fetch")} ${arg(a.url)}`;
|
|
89
|
+
case "todo_write": return "";
|
|
90
|
+
case "ask_user": return "";
|
|
91
|
+
case "git_status": return `${verb("Git")} ${arg("status")}`;
|
|
92
|
+
case "git_diff": return `${verb("Git")} ${arg("diff" + (a.staged ? " --staged" : "") + (a.base ? ` ${a.base}...${a.head || "HEAD"}` : ""))}`;
|
|
93
|
+
case "git_log": return `${verb("Git")} ${arg("log" + (a.file ? ` -- ${a.file}` : "") + (a.branch ? ` ${a.branch}` : ""))}`;
|
|
94
|
+
case "git_commit": return `${verb("Git")} ${arg(`commit "${ellip(a.message || "", 50)}"`)}`
|
|
95
|
+
case "git_branch": return `${verb("Git")} ${arg(`branch ${a.name}`)}`;
|
|
96
|
+
case "git_push": return `${verb("Git")} ${arg("push" + (a.force ? " --force" : ""))}`;
|
|
97
|
+
case "git_create_pr": return `${verb("Git")} ${arg(`pr "${ellip(a.title || "", 50)}"`)}`
|
|
98
98
|
default: return `${verb(name)} ${arg(JSON.stringify(a))}`;
|
|
99
99
|
}
|
|
100
100
|
}
|
|
@@ -199,9 +199,10 @@ export function toolSummary(name, result) {
|
|
|
199
199
|
// (<|channel|>, <|tool_response|>, <channel|>) always have one. Real code like
|
|
200
200
|
// <div>, Vec<T>, a < b has no pipe and is left untouched.
|
|
201
201
|
const MODEL_TOKEN_RE = /<\|[a-z_]*\|?>|<[a-z_]+\|>/gi;
|
|
202
|
+
const CHANNEL_NAMES_RE = /^[ \t]*(thought|analysis|final|commentary|thinking|return|assistant|user|system|developer|tool)[ \t]*$/gim;
|
|
202
203
|
|
|
203
204
|
export function stripModelTokens(text) {
|
|
204
|
-
return text.replace(MODEL_TOKEN_RE, "");
|
|
205
|
+
return text.replace(MODEL_TOKEN_RE, "").replace(CHANNEL_NAMES_RE, "");
|
|
205
206
|
}
|
|
206
207
|
|
|
207
208
|
// Streaming-safe stripper: the leaked tokens (<|channel|>, <|tool_response|>, …)
|
|
@@ -212,7 +213,7 @@ export function makeTokenStripper() {
|
|
|
212
213
|
let buf = "";
|
|
213
214
|
return {
|
|
214
215
|
push(text) {
|
|
215
|
-
buf = (buf + text).replace(MODEL_TOKEN_RE, "");
|
|
216
|
+
buf = (buf + text).replace(MODEL_TOKEN_RE, "").replace(CHANNEL_NAMES_RE, "");
|
|
216
217
|
const partial = buf.match(/<[|a-z_/]*$/i); // possible token start at the tail
|
|
217
218
|
if (partial) {
|
|
218
219
|
const emit = buf.slice(0, partial.index);
|
|
@@ -224,7 +225,7 @@ export function makeTokenStripper() {
|
|
|
224
225
|
return emit;
|
|
225
226
|
},
|
|
226
227
|
flush() {
|
|
227
|
-
const out = buf.replace(MODEL_TOKEN_RE, "");
|
|
228
|
+
const out = buf.replace(MODEL_TOKEN_RE, "").replace(CHANNEL_NAMES_RE, "");
|
|
228
229
|
buf = "";
|
|
229
230
|
return out;
|
|
230
231
|
},
|
package/src/repl.js
CHANGED
|
@@ -434,11 +434,8 @@ function printBanner(state, projectLine = "", resumed = false) {
|
|
|
434
434
|
|
|
435
435
|
function printStatusLine(state) {
|
|
436
436
|
const parts = [];
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
if (state.messages.length > 0) {
|
|
440
|
-
|
|
441
|
-
}
|
|
442
|
-
parts.push(c.cyan(state.autoYes ? "auto-yes" : "review"));
|
|
443
|
-
console.log(c.dim(parts.join(" · ")));
|
|
437
|
+
if (state.balance != null) parts.push(`${state.balance.toLocaleString()} credits`);
|
|
438
|
+
parts.push(state.autoYes ? "skip-permissions" : "review mode");
|
|
439
|
+
if (state.messages.length > 0) parts.push(`${state.messages.length} msgs`);
|
|
440
|
+
console.log(c.dim(parts.join(" · ")));
|
|
444
441
|
}
|