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.
@@ -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.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
- // Don't open a "● " bullet for leading whitespace (e.g. when a whole
187
- // turn's text was suppressed as a leak, leaving only a stray newline).
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" + c.cyan("● "));
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
- const tail = stripper.flush();
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" + c.cyan("● ")); lastWasText = true; }
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 — 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})`));
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.cyan("●") + " " + label);
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.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
  }
@@ -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
- 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
  }