aether-code 0.43.2 → 0.43.3

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.
@@ -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
- console.log(divider());
249
- const modeLabel = autoYes && unsafePaths ? " · skip-permissions" : `${autoYes ? " · auto-yes" : " · review"}${unsafePaths ? "" : " · sandboxed"}`;
250
- console.log(c.magenta(c.bold("aether-code")) + c.gray(` · cwd ${cwd}${modeLabel}`));
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.gray(`task: `) + prompt);
253
- console.log(divider());
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
- console.log("\n" + divider());
275
+ const elapsed = result.elapsed ? formatElapsed(result.elapsed) : "";
276
+ console.log("");
268
277
  if (result.ok) {
269
- console.log(c.green(c.bold("● Done")) + c.gray(` ${result.turns} turn${result.turns === 1 ? "" : "s"} · ${result.totalCredits} credits · ${result.totalIn}→${result.totalOut} tokens`));
270
- if (typeof result.balance === "number") {
271
- console.log(c.gray(` balance: ${result.balance.toLocaleString()} credits`));
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(c.bold("× Stopped")) + c.gray(` ${result.totalCredits} credits used · ${result.totalIn}→${result.totalOut} tokens`));
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.2",
3
+ "version": "0.43.3",
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();
@@ -188,7 +189,7 @@ export async function runAgent({
188
189
  if (!lastWasText && !clean.trim()) return;
189
190
  stopSpin();
190
191
  if (!lastWasText) {
191
- process.stdout.write("\n" + c.cyan("● "));
192
+ process.stdout.write("\n");
192
193
  lastWasText = true;
193
194
  }
194
195
  process.stdout.write(clean);
@@ -208,7 +209,7 @@ export async function runAgent({
208
209
  } catch (err) {
209
210
  stopSpin();
210
211
  if (err instanceof AetherError) {
211
- return { ok: false, error: err, totalCredits, totalIn, totalOut, balance: lastBalance, messages };
212
+ return { ok: false, error: err, totalCredits, totalIn, totalOut, balance: lastBalance, messages, elapsed: Date.now() - t0 };
212
213
  }
213
214
  throw err;
214
215
  }
@@ -217,7 +218,7 @@ export async function runAgent({
217
218
  // Flush any held-back partial token, then close the line.
218
219
  const tail = stripper.flush();
219
220
  if (tail && (lastWasText || tail.trim())) {
220
- if (!lastWasText) { process.stdout.write("\n" + c.cyan("● ")); lastWasText = true; }
221
+ if (!lastWasText) { process.stdout.write("\n"); lastWasText = true; }
221
222
  process.stdout.write(tail);
222
223
  }
223
224
  if (lastWasText) process.stdout.write("\n");
@@ -228,10 +229,10 @@ export async function runAgent({
228
229
  if (typeof res.balanceAfter === "number") lastBalance = res.balanceAfter;
229
230
  onTokens({ totalCredits, totalIn, totalOut, balance: lastBalance });
230
231
 
231
- // Per-turn cost — visible so users catch runaway loops early
232
- const inK = ((res.usage?.prompt_tokens ?? 0) / 1000).toFixed(1);
233
- const outTok = res.usage?.completion_tokens ?? 0;
234
- console.log(c.gray(` ${inK}k→${outTok} tokens · ${turnCredits} credits (total: ${totalCredits})`));
232
+ // Per-turn cost — only shown when credits are high enough to warrant attention
233
+ if (turnCredits > 20) {
234
+ console.log(c.gray(` ${turnCredits} credits this turn (${totalCredits} total)`));
235
+ }
235
236
 
236
237
  // Push assistant message into history
237
238
  messages.push({
@@ -252,19 +253,19 @@ export async function runAgent({
252
253
  // Credit cap — hard stop to prevent runaway cost
253
254
  if (maxCredits > 0 && totalCredits >= maxCredits) {
254
255
  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 };
256
+ return { ok: true, totalCredits, totalIn, totalOut, balance: lastBalance, messages, elapsed: Date.now() - t0 };
256
257
  }
257
258
 
258
259
  // Tool call count cap
259
260
  if (toolCalls.length > 0 && totalToolCalls + toolCalls.length > MAX_TOTAL_TOOL_CALLS) {
260
261
  console.log(c.red(`\n[Tool call limit reached (${MAX_TOTAL_TOOL_CALLS}). Stopping.]`));
261
- return { ok: true, totalCredits, totalIn, totalOut, balance: lastBalance, messages };
262
+ return { ok: true, totalCredits, totalIn, totalOut, balance: lastBalance, messages, elapsed: Date.now() - t0 };
262
263
  }
263
264
 
264
265
  // Consecutive tool-only turns cap (model stuck in a tool loop with no text)
265
266
  if (consecutiveToolOnlyTurns >= MAX_CONSECUTIVE_TOOL_ONLY) {
266
267
  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 };
268
+ return { ok: true, totalCredits, totalIn, totalOut, balance: lastBalance, messages, elapsed: Date.now() - t0 };
268
269
  }
269
270
  if (toolCalls.length === 0) {
270
271
  // Completeness guard: on a modification task, if the model read files it
@@ -373,7 +374,7 @@ export async function runAgent({
373
374
  console.log(" " + c.green("●") + " " + p + c.gray(` — ${action}`));
374
375
  }
375
376
  }
376
- return { ok: true, totalCredits, totalIn, totalOut, turns: i + 1, balance: lastBalance, messages };
377
+ return { ok: true, totalCredits, totalIn, totalOut, turns: i + 1, balance: lastBalance, messages, elapsed: Date.now() - t0 };
377
378
  }
378
379
 
379
380
  // Execute each tool call. Show the actual args (now that we have them
@@ -384,7 +385,7 @@ export async function runAgent({
384
385
  try { args = JSON.parse(call.function.arguments || "{}"); } catch { /* leave empty */ }
385
386
  // todo_write renders its own Plan box, so it returns an empty label.
386
387
  const label = toolLabel(call.function.name, args);
387
- if (label) console.log("\n" + c.cyan("●") + " " + label);
388
+ if (label) console.log("\n" + c.green("●") + " " + label);
388
389
 
389
390
  // Loop guard — short-circuit a tool call we've already run 3+ times with
390
391
  // the same args, and tell the model to change approach instead of looping.
@@ -445,7 +446,7 @@ export async function runAgent({
445
446
  }
446
447
 
447
448
  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 };
449
+ return { ok: false, error: new Error("Max turns reached"), totalCredits, totalIn, totalOut, balance: lastBalance, messages, elapsed: Date.now() - t0 };
449
450
  }
450
451
 
451
452
  // 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.cyan(c.bold(v));
78
- const arg = (s) => c.gray(ellip(String(s), 72));
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("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("find")} ${arg(a.pattern)}`;
85
- case "search_files": return `${verb("search")} ${arg(`/${a.pattern}/ in ${a.path ?? "."}`)}`;
86
- case "run_shell": return `${verb("run")} ${arg(a.command)}`;
87
- case "web_search": return `${verb("search web")} ${arg(JSON.stringify(a.query ?? ""))}`;
88
- case "web_fetch": return `${verb("fetch")} ${arg(a.url)}`;
89
- case "todo_write": return ""; // its Plan box is the label
90
- case "ask_user": return ""; // the interactive menu is its display
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)}"`)}`
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
  }
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
- parts.push(c.gray(`session: ${state.sessionCredits} cr · ${state.sessionIn}→${state.sessionOut} tokens`));
438
- if (state.balance != null) parts.push(c.gray(`balance: ${state.balance.toLocaleString()}`));
439
- if (state.messages.length > 0) {
440
- parts.push(c.gray(`history: ${state.messages.length} msg${state.messages.length === 1 ? "" : "s"}`));
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
  }