aether-code 0.16.2 → 0.16.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.
@@ -26,7 +26,7 @@ import {
26
26
  import readline from "node:readline";
27
27
  import { c, errorLine, divider } from "../src/render.js";
28
28
 
29
- const VERSION = "0.16.2";
29
+ const VERSION = "0.16.4";
30
30
 
31
31
  /**
32
32
  * Try to start MCP servers from ~/.aether/mcp.json. Returns a started
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aether-code",
3
- "version": "0.16.2",
3
+ "version": "0.16.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
@@ -6,7 +6,7 @@ import { agentTurnStream, AetherError } from "./api.js";
6
6
  import { TOOL_DEFINITIONS, executeTool } from "./tools.js";
7
7
  import { unnamespaceToolName } from "./mcp.js";
8
8
  import { loadAllSkills, selectSkills, renderSkillsBlock } from "./skills.js";
9
- import { c, divider, turn, toolHeader, toolResult, errorLine } from "./render.js";
9
+ import { c, divider, turn, toolLabel, toolSummary, makeTokenStripper, errorLine } from "./render.js";
10
10
 
11
11
  const DEFAULT_MAX_TURNS = 25;
12
12
 
@@ -59,6 +59,7 @@ export async function runAgent({
59
59
  // calling a particular tool (i.e. the `name` arrives in the stream).
60
60
  const announced = new Set();
61
61
  let lastWasText = false;
62
+ const stripper = makeTokenStripper();
62
63
 
63
64
  // Select skills for this turn against the current user prompt + any
64
65
  // paths the model has read so far. Prepend the matching skills' bodies
@@ -73,19 +74,24 @@ export async function runAgent({
73
74
  messages: turnMessages,
74
75
  tools,
75
76
  onDelta: (text) => {
77
+ // Buffered strip of leaked model channel/control tokens (which can
78
+ // be split across stream chunks) before display.
79
+ const clean = stripper.push(text);
80
+ if (!clean) return;
76
81
  if (!lastWasText) {
77
82
  process.stdout.write(" ");
78
83
  lastWasText = true;
79
84
  }
80
- process.stdout.write(text);
85
+ process.stdout.write(clean);
81
86
  },
82
87
  onToolCallDelta: (delta) => {
83
- // Print the tool header once we know the name (first chunk for that index)
88
+ // Just close the streamed text line when the model starts a tool
89
+ // call — the clean label is printed at execution time, so no noisy
90
+ // "preparing args" placeholder here.
84
91
  if (delta.name && !announced.has(delta.index)) {
85
92
  announced.add(delta.index);
86
93
  if (lastWasText) process.stdout.write("\n");
87
94
  lastWasText = false;
88
- process.stdout.write(c.cyan(c.bold(delta.name)) + c.gray("(...)") + c.gray(" preparing args\n"));
89
95
  }
90
96
  },
91
97
  });
@@ -96,7 +102,12 @@ export async function runAgent({
96
102
  throw err;
97
103
  }
98
104
 
99
- // End-of-turn newline + cost meter
105
+ // Flush any held-back partial token, then close the line.
106
+ const tail = stripper.flush();
107
+ if (tail) {
108
+ if (!lastWasText) { process.stdout.write(" "); lastWasText = true; }
109
+ process.stdout.write(tail);
110
+ }
100
111
  if (lastWasText) process.stdout.write("\n");
101
112
  totalCredits += res.creditsCharged ?? 0;
102
113
  totalIn += res.usage?.prompt_tokens ?? 0;
@@ -125,7 +136,7 @@ export async function runAgent({
125
136
  let args = {};
126
137
  try { args = JSON.parse(call.function.arguments || "{}"); } catch { /* leave empty */ }
127
138
  console.log("");
128
- console.log(toolHeader(call.function.name, args));
139
+ console.log(toolLabel(call.function.name, args));
129
140
 
130
141
  // Route to MCP if the tool name is namespaced (mcp__server__tool);
131
142
  // otherwise execute the built-in tool. unnamespaceToolName returns
@@ -143,10 +154,7 @@ export async function runAgent({
143
154
  if (call.function.name === "read_file" || call.function.name === "edit_file" || call.function.name === "write_file") {
144
155
  if (typeof args.path === "string") referencedPaths.push(args.path);
145
156
  }
146
- if (result.output) {
147
- const preview = result.output.length > 800 ? result.output.slice(0, 800) + "\n…(truncated)" : result.output;
148
- console.log(toolResult(preview, result.ok));
149
- }
157
+ console.log(toolSummary(call.function.name, result));
150
158
 
151
159
  messages.push({
152
160
  role: "tool",
package/src/render.js CHANGED
@@ -35,6 +35,99 @@ export function toolHeader(name, args) {
35
35
  return `${c.cyan(c.bold(name))}${c.gray("(")}${c.gray(trimmed)}${c.gray(")")}`;
36
36
  }
37
37
 
38
+ const ellip = (s, n) => (s && s.length > n ? s.slice(0, n - 1) + "…" : s || "");
39
+
40
+ // Clean one-line label for a tool call — a verb + its key argument, instead of
41
+ // dumping raw JSON (which buried file contents / queries in noise).
42
+ export function toolLabel(name, args) {
43
+ const a = args || {};
44
+ const verb = (v) => c.cyan(c.bold(v));
45
+ const arg = (s) => c.gray(ellip(String(s), 72));
46
+ switch (name) {
47
+ case "read_file": return `${verb("read")} ${arg(a.path)}`;
48
+ case "write_file": return `${verb("write")} ${arg(a.path)}`;
49
+ case "edit_file": return `${verb("edit")} ${arg(a.path)}`;
50
+ case "list_dir": return `${verb("list")} ${arg(a.path)}`;
51
+ case "glob_files": return `${verb("glob")} ${arg(a.pattern)}`;
52
+ case "search_files": return `${verb("search")} ${arg(`/${a.pattern}/ in ${a.path ?? "."}`)}`;
53
+ case "run_shell": return `${verb("run")} ${arg(a.command)}`;
54
+ case "web_search": return `${verb("search web")} ${arg(JSON.stringify(a.query ?? ""))}`;
55
+ case "web_fetch": return `${verb("fetch")} ${arg(a.url)}`;
56
+ case "todo_write": return verb("plan");
57
+ default: return `${verb(name)} ${arg(JSON.stringify(a))}`;
58
+ }
59
+ }
60
+
61
+ // Terse one-line result summary instead of dumping raw JSON / file contents.
62
+ // Tools whose handlers already render rich output (diffs, the shell stream, the
63
+ // plan) just get a check — the detail was already printed.
64
+ export function toolSummary(name, result) {
65
+ const ok = result.ok;
66
+ const mark = ok ? c.green("✓") : c.red("✗");
67
+ const out = result.output ?? "";
68
+ const firstLine = out.split("\n").find((l) => l.trim()) ?? "";
69
+
70
+ if (name === "run_shell") {
71
+ let code = null;
72
+ try { code = JSON.parse(out).exit_code; } catch { /* ignore */ }
73
+ return ` ${mark} ${c.gray(code === null ? (ok ? "done" : "failed") : `exit ${code}`)}`;
74
+ }
75
+ if (name === "write_file" || name === "edit_file" || name === "todo_write") {
76
+ // Handler already printed the diff / plan; echo its short status line.
77
+ return ` ${mark} ${c.gray(ellip(firstLine, 100))}`;
78
+ }
79
+ let summary = "";
80
+ try {
81
+ const j = JSON.parse(out);
82
+ if (Array.isArray(j)) summary = `${j.length} result${j.length === 1 ? "" : "s"}`;
83
+ else if (Array.isArray(j.files)) summary = `${j.files.length} file${j.files.length === 1 ? "" : "s"}`;
84
+ else if (Array.isArray(j.matches)) summary = `${j.matches.length} match${j.matches.length === 1 ? "" : "es"}`;
85
+ } catch { /* not JSON */ }
86
+ if (!summary) {
87
+ summary = name === "read_file" ? `${out.split("\n").length} lines` : ellip(firstLine, 100);
88
+ }
89
+ return ` ${mark} ${c.gray(summary)}`;
90
+ }
91
+
92
+ // Strip model "harmony"/channel control tokens (<|channel|>, <|message|>,
93
+ // <|tool_response|>, <channel|>, …) that occasionally leak into the text
94
+ // stream. Belt-and-suspenders alongside the server-side scrub.
95
+ // Only strips tokens containing a PIPE — the harmony/channel control tokens
96
+ // (<|channel|>, <|tool_response|>, <channel|>) always have one. Real code like
97
+ // <div>, Vec<T>, a < b has no pipe and is left untouched.
98
+ const MODEL_TOKEN_RE = /<\|[a-z_]*\|?>|<[a-z_]+\|>/gi;
99
+
100
+ export function stripModelTokens(text) {
101
+ return text.replace(MODEL_TOKEN_RE, "");
102
+ }
103
+
104
+ // Streaming-safe stripper: the leaked tokens (<|channel|>, <|tool_response|>, …)
105
+ // can be split across stream chunks ("<chann" then "el|>"), which a per-delta
106
+ // regex misses. This buffers any trailing "<…" that might be the start of a
107
+ // token and only emits it once it's confirmed not-a-token (or on flush).
108
+ export function makeTokenStripper() {
109
+ let buf = "";
110
+ return {
111
+ push(text) {
112
+ buf = (buf + text).replace(MODEL_TOKEN_RE, "");
113
+ const partial = buf.match(/<[|a-z_/]*$/i); // possible token start at the tail
114
+ if (partial) {
115
+ const emit = buf.slice(0, partial.index);
116
+ buf = buf.slice(partial.index);
117
+ return emit;
118
+ }
119
+ const emit = buf;
120
+ buf = "";
121
+ return emit;
122
+ },
123
+ flush() {
124
+ const out = buf.replace(MODEL_TOKEN_RE, "");
125
+ buf = "";
126
+ return out;
127
+ },
128
+ };
129
+ }
130
+
38
131
  export function toolResult(text, ok = true) {
39
132
  const prefix = ok ? c.green(" ✓ ") : c.red(" ✗ ");
40
133
  // First line bold-ish, then dim continuation
package/src/repl.js CHANGED
@@ -17,7 +17,7 @@ import { c, errorLine } from "./render.js";
17
17
  import { checkForUpdate } from "./update-check.js";
18
18
  import { promptBoxed, EXIT_SIGNAL } from "./ink-input.js";
19
19
 
20
- const VERSION = "0.16.2";
20
+ const VERSION = "0.16.4";
21
21
  const MODEL_NAME = "Aether Core";
22
22
 
23
23
  const SHORTCUTS = `