aether-code 0.33.4 → 0.33.5

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.
@@ -28,7 +28,7 @@ import {
28
28
  import readline from "node:readline";
29
29
  import { c, errorLine, divider, setTerminalTitle } from "../src/render.js";
30
30
 
31
- const VERSION = "0.33.4";
31
+ const VERSION = "0.33.5";
32
32
 
33
33
  /**
34
34
  * 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.33.4",
3
+ "version": "0.33.5",
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/box-input.js CHANGED
@@ -140,11 +140,15 @@ export function promptBox({ history = [], placeholder = "Message Aether...", mod
140
140
 
141
141
  if (finalResult && finalResult !== EXIT_SIGNAL) {
142
142
  const echoText = (result || "").trim();
143
- const suffix = clipImages.length > 0
144
- ? c.cyan(` [+${clipImages.length} image${clipImages.length > 1 ? "s" : ""}]`)
143
+ const imgTag = clipImages.length > 0
144
+ ? ` [+${clipImages.length} image${clipImages.length > 1 ? "s" : ""}]`
145
145
  : "";
146
146
  if (echoText || clipImages.length > 0) {
147
- process.stdout.write(c.cyan("> ") + (echoText || c.dim("(image)")) + suffix + "\n");
147
+ const displayText = echoText || "(image)";
148
+ const cols = process.stdout.columns || 80;
149
+ const textLen = displayText.length + imgTag.length + 2;
150
+ const pad = Math.max(0, cols - textLen);
151
+ process.stdout.write(`\x1b[48;5;236m ${displayText}${imgTag}${" ".repeat(pad)}\x1b[0m\n`);
148
152
  }
149
153
  }
150
154
  resolve(finalResult);
package/src/render.js CHANGED
@@ -1,257 +1,268 @@
1
- // ANSI helpers — no chalk dependency.
2
-
3
- const isTty = process.stdout.isTTY;
4
- const noColor = !!process.env.NO_COLOR || !isTty;
5
-
6
- function wrap(open, close) {
7
- return (s) => (noColor ? String(s) : `\x1b[${open}m${s}\x1b[${close}m`);
8
- }
9
-
10
- export const c = {
11
- bold: wrap(1, 22),
12
- dim: wrap(2, 22),
13
- red: wrap(31, 39),
14
- green: wrap(32, 39),
15
- yellow: wrap(33, 39),
16
- blue: wrap(34, 39),
17
- magenta: wrap(35, 39),
18
- cyan: wrap(36, 39),
19
- gray: wrap(90, 39),
20
- };
21
-
22
- export function divider() {
23
- const w = process.stdout.columns || 60;
24
- return c.gray("─".repeat(Math.min(60, w)));
25
- }
26
-
27
- const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*m/g, "");
28
-
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
- const w = inner + padX * 2;
34
- const top = color("╭" + "─".repeat(w) + "╮");
35
- const bot = color("╰" + "─".repeat(w) + "╯");
36
- const mid = lines.map(
37
- (l) => color("│") + " ".repeat(padX) + l + " ".repeat(inner - stripAnsi(l).length + padX) + color("│"),
38
- );
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");
58
- }
59
-
60
- export function turn(n) {
61
- return c.gray(`turn ${n}`);
62
- }
63
-
64
- export function toolHeader(name, args) {
65
- // Format args compactly. If any value is huge, truncate it.
66
- const compact = JSON.stringify(args);
67
- const trimmed = compact.length > 120 ? compact.slice(0, 117) + "..." : compact;
68
- return `${c.cyan(c.bold(name))}${c.gray("(")}${c.gray(trimmed)}${c.gray(")")}`;
69
- }
70
-
71
- const ellip = (s, n) => (s && s.length > n ? s.slice(0, n - 1) + "…" : s || "");
72
-
73
- // Clean one-line label for a tool call — a verb + its key argument, instead of
74
- // dumping raw JSON (which buried file contents / queries in noise).
75
- export function toolLabel(name, args) {
76
- const a = args || {};
77
- const verb = (v) => c.cyan(c.bold(v));
78
- const arg = (s) => c.gray(ellip(s == null ? "" : String(s), 72));
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
- default: return `${verb(name)} ${arg(JSON.stringify(a))}`;
92
- }
93
- }
94
-
95
- // Set the terminal window/tab title (so cmd.exe shows "Aether", not the node
96
- // path). OSC 0 — supported by cmd.exe, Windows Terminal, and POSIX terminals.
97
- export function setTerminalTitle(title) {
98
- if (process.stdout.isTTY) process.stdout.write(`\x1b]0;${title}\x07`);
99
- }
100
-
101
- // Live "working…" spinner with optional live metrics (token count, status).
102
- // Rewrites a single line in place (\r) with a rotating half-circle + elapsed
103
- // time (mm:ss when > 60s) + any metrics fed via update(). stop() clears the
104
- // line and restores the cursor. No-op in non-TTY.
105
- export function startSpinner(label) {
106
- if (!process.stdout.isTTY) return { stop: () => {}, update: () => {} };
107
- const frames = ["◐", "◓", "◑", "◒"];
108
- const t0 = Date.now();
109
- let i = 0;
110
- let tokenCount = 0;
111
- let statusText = "";
112
- process.stdout.write("\x1b[?25l"); // hide cursor
113
- const render = () => {
114
- const ms = Date.now() - t0;
115
- const secs = Math.floor(ms / 1000);
116
- const timeFmt = secs >= 60
117
- ? `${Math.floor(secs / 60)}m ${String(secs % 60).padStart(2, "0")}s`
118
- : `${secs}s`;
119
- const parts = [];
120
- if (secs > 0) parts.push(timeFmt);
121
- if (tokenCount > 0) parts.push(`↓ ${fmtTokens(tokenCount)} tokens`);
122
- if (statusText) parts.push(statusText);
123
- const metrics = parts.length > 0 ? c.gray(` (${parts.join(" · ")})`) : "";
124
- process.stdout.write(`\r\x1b[K${c.cyan(frames[i % frames.length])} ${c.gray(label + "…")}${metrics}`);
125
- i++;
126
- };
127
- render();
128
- const timer = setInterval(render, 120);
129
- if (typeof timer.unref === "function") timer.unref(); // don't keep the process alive
130
- return {
131
- update(data) {
132
- if (data.tokens != null) tokenCount = data.tokens;
133
- if (data.status != null) statusText = data.status;
134
- },
135
- stop() {
136
- clearInterval(timer);
137
- process.stdout.write("\r\x1b[K\x1b[?25h"); // clear line + show cursor
138
- },
139
- };
140
- }
141
-
142
- // Human-friendly token count (14.2k, 1.3M, etc.)
143
- export function fmtTokens(n) {
144
- if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + "M";
145
- if (n >= 1_000) return (n / 1_000).toFixed(1) + "k";
146
- return String(n);
147
- }
148
-
149
- // Compact one-line turn summary printed after each agent turn completes.
150
- export function turnStats({ elapsed, tokensIn, tokensOut, tools, credits }) {
151
- const parts = [];
152
- if (elapsed > 0) {
153
- const s = Math.floor(elapsed / 1000);
154
- parts.push(s >= 60 ? `${Math.floor(s / 60)}m ${String(s % 60).padStart(2, "0")}s` : `${s}s`);
155
- }
156
- if (tokensIn > 0 || tokensOut > 0) {
157
- parts.push(`${fmtTokens(tokensIn)}→${fmtTokens(tokensOut)} tokens`);
158
- }
159
- if (tools > 0) parts.push(`${tools} tool call${tools === 1 ? "" : "s"}`);
160
- if (credits > 0) parts.push(`${credits} cr`);
161
- return parts.length > 0 ? c.dim(` ── ${parts.join(" · ")}`) : "";
162
- }
163
-
164
- // Terse one-line result summary instead of dumping raw JSON / file contents.
165
- // Tools whose handlers already render rich output (diffs, the shell stream, the
166
- // plan) just get a check — the detail was already printed.
167
- export function toolSummary(name, result) {
168
- const ok = result.ok;
169
- // A tree branch links the result to the action above it (modern CLI style).
170
- const branch = ok ? c.green("└─") : c.red("└─");
171
- const out = result.output ?? "";
172
- const firstLine = out.split("\n").find((l) => l.trim()) ?? "";
173
-
174
- if (name === "run_shell") {
175
- let code = null;
176
- try { code = JSON.parse(out).exit_code; } catch { /* ignore */ }
177
- return ` ${branch} ${c.gray(code === null ? (ok ? "done" : "failed") : `exit ${code}`)}`;
178
- }
179
- if (name === "todo_write") return ""; // the Plan is its own feedback
180
- if (name === "ask_user") return ""; // the menu already showed the answer
181
- if (name === "write_file" || name === "edit_file") {
182
- // Handler already printed the diff; echo its short status line.
183
- return ` ${branch} ${c.gray(ellip(firstLine, 100))}`;
184
- }
185
- let summary = "";
186
- try {
187
- const j = JSON.parse(out);
188
- if (Array.isArray(j)) summary = `${j.length} result${j.length === 1 ? "" : "s"}`;
189
- else if (Array.isArray(j.files)) summary = `${j.files.length} file${j.files.length === 1 ? "" : "s"}`;
190
- else if (Array.isArray(j.matches)) summary = `${j.matches.length} match${j.matches.length === 1 ? "" : "es"}`;
191
- } catch { /* not JSON */ }
192
- if (!summary) {
193
- summary = name === "read_file" ? `${out.split("\n").length} lines` : ellip(firstLine, 100);
194
- }
195
- return ` ${branch} ${c.gray(summary)}`;
196
- }
197
-
198
- // Strip model "harmony"/channel control tokens (<|channel|>, <|message|>,
199
- // <|tool_response|>, <channel|>, …) that occasionally leak into the text
200
- // stream. Belt-and-suspenders alongside the server-side scrub.
201
- // Only strips tokens containing a PIPE — the harmony/channel control tokens
202
- // (<|channel|>, <|tool_response|>, <channel|>) always have one. Real code like
203
- // <div>, Vec<T>, a < b has no pipe and is left untouched.
204
- const MODEL_TOKEN_RE = /<\|[a-z_]*\|?>|<[a-z_]+\|>/gi;
205
-
206
- export function stripModelTokens(text) {
207
- return text.replace(MODEL_TOKEN_RE, "");
208
- }
209
-
210
- // Streaming-safe stripper: the leaked tokens (<|channel|>, <|tool_response|>, …)
211
- // can be split across stream chunks ("<chann" then "el|>"), which a per-delta
212
- // regex misses. This buffers any trailing "<…" that might be the start of a
213
- // token and only emits it once it's confirmed not-a-token (or on flush).
214
- export function makeTokenStripper() {
215
- let buf = "";
216
- return {
217
- push(text) {
218
- buf = (buf + text).replace(MODEL_TOKEN_RE, "");
219
- const partial = buf.match(/<[|a-z_/]*$/i); // possible token start at the tail
220
- if (partial) {
221
- const emit = buf.slice(0, partial.index);
222
- buf = buf.slice(partial.index);
223
- return emit;
224
- }
225
- const emit = buf;
226
- buf = "";
227
- return emit;
228
- },
229
- flush() {
230
- const out = buf.replace(MODEL_TOKEN_RE, "");
231
- buf = "";
232
- return out;
233
- },
234
- };
235
- }
236
-
237
- export function toolResult(text, ok = true) {
238
- const prefix = ok ? c.green(" ● ") : c.red(" × ");
239
- // First line bold-ish, then dim continuation
240
- const lines = text.split("\n");
241
- const head = lines[0].slice(0, 200);
242
- const rest = lines.slice(1, 6).join("\n").slice(0, 600);
243
- return `${prefix}${head}${rest ? "\n" + c.dim(rest) : ""}`;
244
- }
245
-
246
- export function assistant(text) {
247
- // Indent each line for visual separation from tool calls
248
- return text.split("\n").map((l) => ` ${l}`).join("\n");
249
- }
250
-
251
- export function errorLine(msg) {
252
- return `${c.red(c.bold("Error:"))} ${msg}`;
253
- }
254
-
255
- export function note(msg) {
256
- return c.dim(msg);
257
- }
1
+ // ANSI helpers — no chalk dependency.
2
+
3
+ const isTty = process.stdout.isTTY;
4
+ const noColor = !!process.env.NO_COLOR || !isTty;
5
+
6
+ function wrap(open, close) {
7
+ return (s) => (noColor ? String(s) : `\x1b[${open}m${s}\x1b[${close}m`);
8
+ }
9
+
10
+ export const c = {
11
+ bold: wrap(1, 22),
12
+ dim: wrap(2, 22),
13
+ red: wrap(31, 39),
14
+ green: wrap(32, 39),
15
+ yellow: wrap(33, 39),
16
+ blue: wrap(34, 39),
17
+ magenta: wrap(35, 39),
18
+ cyan: wrap(36, 39),
19
+ gray: wrap(90, 39),
20
+ };
21
+
22
+ export function divider() {
23
+ const w = process.stdout.columns || 60;
24
+ return c.gray("─".repeat(Math.min(60, w)));
25
+ }
26
+
27
+ const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*m/g, "");
28
+
29
+ export function boxLines(lines, { color = c.gray, padX = 2, minInner = 0 } = {}) {
30
+ const inner = Math.max(minInner, 0, ...lines.map((l) => stripAnsi(l).length));
31
+ const w = inner + padX * 2;
32
+ const top = color("╭" + "─".repeat(w) + "╮");
33
+ const bot = color("╰" + "─".repeat(w) + "╯");
34
+ const mid = lines.map(
35
+ (l) => color("│") + " ".repeat(padX) + l + " ".repeat(inner - stripAnsi(l).length + padX) + color("│"),
36
+ );
37
+ return [top, ...mid, bot];
38
+ }
39
+
40
+ export function box(lines, opts) {
41
+ return boxLines(lines, opts).join("\n");
42
+ }
43
+
44
+ export function sideBySide(a, b, gap = 3) {
45
+ const wa = Math.max(0, ...a.map((l) => stripAnsi(l).length));
46
+ const h = Math.max(a.length, b.length);
47
+ const out = [];
48
+ for (let i = 0; i < h; i++) {
49
+ const la = a[i] ?? "";
50
+ const lb = b[i] ?? "";
51
+ out.push(la + " ".repeat(wa - stripAnsi(la).length + gap) + lb);
52
+ }
53
+ return out.join("\n");
54
+ }
55
+
56
+ export function turn(n) {
57
+ return c.gray(`turn ${n}`);
58
+ }
59
+
60
+ export function toolHeader(name, args) {
61
+ const compact = JSON.stringify(args);
62
+ const trimmed = compact.length > 120 ? compact.slice(0, 117) + "..." : compact;
63
+ return `${c.cyan(c.bold(name))}${c.gray("(")}${c.gray(trimmed)}${c.gray(")")}`;
64
+ }
65
+
66
+ const ellip = (s, n) => (s && s.length > n ? s.slice(0, n - 1) + "…" : s || "");
67
+
68
+ export function toolLabel(name, args) {
69
+ const a = args || {};
70
+ const verb = (v) => c.cyan(c.bold(v));
71
+ const arg = (s) => c.gray(ellip(s == null ? "" : String(s), 72));
72
+ switch (name) {
73
+ case "read_file": return `${verb("read")} ${arg(a.path)}`;
74
+ case "write_file": return `${verb("write")} ${arg(a.path)}`;
75
+ case "edit_file": return `${verb("edit")} ${arg(a.path)}`;
76
+ case "list_dir": return `${verb("list")} ${arg(a.path)}`;
77
+ case "glob_files": return `${verb("find")} ${arg(a.pattern)}`;
78
+ case "search_files": return `${verb("search")} ${arg(`/${a.pattern}/ in ${a.path ?? "."}`)}`;
79
+ case "run_shell": return `${verb("run")} ${arg(a.command)}`;
80
+ case "web_search": return `${verb("search web")} ${arg(JSON.stringify(a.query ?? ""))}`;
81
+ case "web_fetch": return `${verb("fetch")} ${arg(a.url)}`;
82
+ case "todo_write": return "";
83
+ case "ask_user": return "";
84
+ default: return `${verb(name)} ${arg(JSON.stringify(a))}`;
85
+ }
86
+ }
87
+
88
+ export function setTerminalTitle(title) {
89
+ if (process.stdout.isTTY) process.stdout.write(`\x1b]0;${title}\x07`);
90
+ }
91
+
92
+ // Fun thinking words — like Claude Code's "Hullaballooing...", "Cogitating..."
93
+ const THINKING_WORDS = [
94
+ "Thinking",
95
+ "Pondering",
96
+ "Mulling",
97
+ "Cogitating",
98
+ "Ruminating",
99
+ "Deliberating",
100
+ "Contemplating",
101
+ "Noodling",
102
+ "Musing",
103
+ "Brainstorming",
104
+ "Percolating",
105
+ "Brainwaving",
106
+ "Daydreaming",
107
+ "Hullaballooing",
108
+ "Churning",
109
+ "Brewing",
110
+ "Conjuring",
111
+ "Scheming",
112
+ "Hatching",
113
+ "Concocting",
114
+ "Simmering",
115
+ "Marinating",
116
+ "Fermenting",
117
+ "Stewing",
118
+ "Whittling",
119
+ "Tinkering",
120
+ "Puzzling",
121
+ "Deciphering",
122
+ "Unraveling",
123
+ "Distilling",
124
+ ];
125
+
126
+ // Pick a random thinking word (seeded by current second so it varies per call)
127
+ function pickThinkingWord() {
128
+ const idx = (Date.now() % (THINKING_WORDS.length * 1000)) / 1000 | 0;
129
+ return THINKING_WORDS[idx % THINKING_WORDS.length];
130
+ }
131
+
132
+ export function startSpinner(label) {
133
+ if (!process.stdout.isTTY) return { stop: () => {}, update: () => {} };
134
+ const frames = ["◐", "◓", "◑", "◒"];
135
+ const t0 = Date.now();
136
+ let i = 0;
137
+ let tokenCount = 0;
138
+ let statusText = "";
139
+ const isThinking = label === "thinking";
140
+ const thinkWord = isThinking ? pickThinkingWord() : null;
141
+ process.stdout.write("\x1b[?25l");
142
+ const render = () => {
143
+ const ms = Date.now() - t0;
144
+ const secs = Math.floor(ms / 1000);
145
+ const timeFmt = secs >= 60
146
+ ? `${Math.floor(secs / 60)}m ${String(secs % 60).padStart(2, "0")}s`
147
+ : `${secs}s`;
148
+ const parts = [];
149
+ if (secs > 0) parts.push(timeFmt);
150
+ if (tokenCount > 0) parts.push(`${fmtTokens(tokenCount)} tokens`);
151
+ if (statusText) parts.push(statusText);
152
+ const metrics = parts.length > 0 ? c.gray(` (${parts.join(" \xB7 ")})`) : "";
153
+ const displayLabel = isThinking ? thinkWord : label;
154
+ process.stdout.write(`\r\x1b[K${c.cyan(frames[i % frames.length])} ${c.dim(displayLabel + "...")}${metrics}`);
155
+ i++;
156
+ };
157
+ render();
158
+ const timer = setInterval(render, 120);
159
+ if (typeof timer.unref === "function") timer.unref();
160
+ return {
161
+ update(data) {
162
+ if (data.tokens != null) tokenCount = data.tokens;
163
+ if (data.status != null) statusText = data.status;
164
+ },
165
+ stop() {
166
+ clearInterval(timer);
167
+ process.stdout.write("\r\x1b[K\x1b[?25h");
168
+ },
169
+ };
170
+ }
171
+
172
+ export function fmtTokens(n) {
173
+ if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + "M";
174
+ if (n >= 1_000) return (n / 1_000).toFixed(1) + "k";
175
+ return String(n);
176
+ }
177
+
178
+ export function turnStats({ elapsed, tokensIn, tokensOut, tools, credits }) {
179
+ const parts = [];
180
+ if (elapsed > 0) {
181
+ const s = Math.floor(elapsed / 1000);
182
+ parts.push(s >= 60 ? `${Math.floor(s / 60)}m ${String(s % 60).padStart(2, "0")}s` : `${s}s`);
183
+ }
184
+ if (tokensIn > 0 || tokensOut > 0) {
185
+ parts.push(`${fmtTokens(tokensIn)}->${fmtTokens(tokensOut)} tokens`);
186
+ }
187
+ if (tools > 0) parts.push(`${tools} tool call${tools === 1 ? "" : "s"}`);
188
+ if (credits > 0) parts.push(`${credits} cr`);
189
+ return parts.length > 0 ? c.dim(` Brewed for ${parts.join(" \xB7 ")}`) : "";
190
+ }
191
+
192
+ export function toolResult(text, ok = true) {
193
+ const prefix = ok ? c.green(" ● ") : c.red(" × ");
194
+ const lines = text.split("\n");
195
+ const head = lines[0].slice(0, 200);
196
+ const rest = lines.slice(1, 6).join("\n").slice(0, 600);
197
+ return `${prefix}${head}${rest ? "\n" + c.dim(rest) : ""}`;
198
+ }
199
+
200
+ export function assistant(text) {
201
+ return text.split("\n").map((l) => ` ${l}`).join("\n");
202
+ }
203
+
204
+ export function errorLine(msg) {
205
+ return `${c.red(c.bold("Error:"))} ${msg}`;
206
+ }
207
+
208
+ export function note(msg) {
209
+ return c.dim(msg);
210
+ }
211
+
212
+ export function toolSummary(name, result) {
213
+ const ok = result.ok;
214
+ const branch = ok ? c.green("└─") : c.red("└─");
215
+ const out = result.output ?? "";
216
+ const firstLine = out.split("\n").find((l) => l.trim()) ?? "";
217
+
218
+ if (name === "run_shell") {
219
+ let code = null;
220
+ try { code = JSON.parse(out).exit_code; } catch { /* ignore */ }
221
+ return ` ${branch} ${c.gray(code === null ? (ok ? "done" : "failed") : `exit ${code}`)}`;
222
+ }
223
+ if (name === "todo_write") return "";
224
+ if (name === "ask_user") return "";
225
+ if (name === "write_file" || name === "edit_file") {
226
+ return ` ${branch} ${c.gray(ellip(firstLine, 100))}`;
227
+ }
228
+ let summary = "";
229
+ try {
230
+ const j = JSON.parse(out);
231
+ if (Array.isArray(j)) summary = `${j.length} result${j.length === 1 ? "" : "s"}`;
232
+ else if (Array.isArray(j.files)) summary = `${j.files.length} file${j.files.length === 1 ? "" : "s"}`;
233
+ else if (Array.isArray(j.matches)) summary = `${j.matches.length} match${j.matches.length === 1 ? "" : "es"}`;
234
+ } catch { /* not JSON */ }
235
+ if (!summary) {
236
+ summary = name === "read_file" ? `${out.split("\n").length} lines` : ellip(firstLine, 100);
237
+ }
238
+ return ` ${branch} ${c.gray(summary)}`;
239
+ }
240
+
241
+ const MODEL_TOKEN_RE = /<\|[a-z_]*\|?>|<[a-z_]+\|>/gi;
242
+
243
+ export function stripModelTokens(text) {
244
+ return text.replace(MODEL_TOKEN_RE, "");
245
+ }
246
+
247
+ export function makeTokenStripper() {
248
+ let buf = "";
249
+ return {
250
+ push(text) {
251
+ buf = (buf + text).replace(MODEL_TOKEN_RE, "");
252
+ const partial = buf.match(/<[|a-z_/]*$/i);
253
+ if (partial) {
254
+ const emit = buf.slice(0, partial.index);
255
+ buf = buf.slice(partial.index);
256
+ return emit;
257
+ }
258
+ const emit = buf;
259
+ buf = "";
260
+ return emit;
261
+ },
262
+ flush() {
263
+ const out = buf.replace(MODEL_TOKEN_RE, "");
264
+ buf = "";
265
+ return out;
266
+ },
267
+ };
268
+ }
package/src/repl.js CHANGED
@@ -9,7 +9,7 @@ import { c, errorLine } from "./render.js";
9
9
  import { autoUpdate } from "./update-check.js";
10
10
  import { promptBox, EXIT_SIGNAL } from "./box-input.js";
11
11
 
12
- const VERSION = "0.33.4";
12
+ const VERSION = "0.33.5";
13
13
  const MODEL_NAME = "Aether Core";
14
14
 
15
15
  const MODEL_ALIASES = {
@@ -129,6 +129,12 @@ export async function runRepl({ cwd: initialCwd, autoYes: initialAutoYes, unsafe
129
129
  if (!result.ok && result.error) {
130
130
  console.log("\n" + errorLine(result.error.message || String(result.error)));
131
131
  }
132
+
133
+ // Push prompt back toward the bottom — content scrolls UP like Claude Code.
134
+ // We leave a few lines visible so the tail of the response stays on screen.
135
+ const rowsNow = process.stdout.rows || 24;
136
+ const gap = Math.max(0, Math.floor(rowsNow * 0.4));
137
+ for (let g = 0; g < gap; g++) console.log("");
132
138
  }
133
139
  }
134
140