aether-code 0.3.0 → 0.4.0

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.
@@ -16,7 +16,7 @@ import { fetchBalance, AetherError } from "../src/api.js";
16
16
  import { writeConfigFile, getConfig, CONFIG_PATH } from "../src/config.js";
17
17
  import { c, errorLine, divider } from "../src/render.js";
18
18
 
19
- const VERSION = "0.3.0";
19
+ const VERSION = "0.4.0";
20
20
 
21
21
  const HELP = `${c.bold("aether")} — uncensored AI coding agent
22
22
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aether-code",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Uncensored AI coding agent for your terminal. Type `aether` to launch the interactive REPL — like Claude Code, with no refusal layer.",
5
5
  "homepage": "https://trynoguard.com",
6
6
  "repository": {
package/src/agent.js CHANGED
@@ -4,7 +4,9 @@
4
4
 
5
5
  import { agentTurnStream, AetherError } from "./api.js";
6
6
  import { TOOL_DEFINITIONS, executeTool } from "./tools.js";
7
- import { c, divider, turn, toolHeader, toolResult, errorLine } from "./render.js";
7
+ import {
8
+ c, turnLine, costLine, toolAnnounce, toolResultLine, errorLine, assistantPrefix,
9
+ } from "./render.js";
8
10
 
9
11
  const DEFAULT_MAX_TURNS = 25;
10
12
 
@@ -17,8 +19,6 @@ export async function runAgent({
17
19
  maxTurns = DEFAULT_MAX_TURNS,
18
20
  onTokens = () => {},
19
21
  }) {
20
- // Two callers: one-shot (initialPrompt only, fresh conversation) and REPL
21
- // (priorMessages + initialPrompt to continue an ongoing chat).
22
22
  const messages = priorMessages
23
23
  ? [...priorMessages, { role: "user", content: initialPrompt }]
24
24
  : [{ role: "user", content: initialPrompt }];
@@ -28,13 +28,10 @@ export async function runAgent({
28
28
  let lastBalance = null;
29
29
 
30
30
  for (let i = 0; i < maxTurns; i++) {
31
- process.stdout.write("\n" + turn(i + 1) + "\n");
31
+ process.stdout.write("\n" + turnLine(i + 1) + "\n");
32
32
 
33
- // Stream the assistant's response. Print text deltas as they arrive,
34
- // along with tool-call announcements as soon as the model commits to
35
- // calling a particular tool (i.e. the `name` arrives in the stream).
36
33
  const announced = new Set();
37
- let lastWasText = false;
34
+ let textOpened = false;
38
35
 
39
36
  let res;
40
37
  try {
@@ -42,19 +39,22 @@ export async function runAgent({
42
39
  messages,
43
40
  tools: TOOL_DEFINITIONS,
44
41
  onDelta: (text) => {
45
- if (!lastWasText) {
46
- process.stdout.write(" ");
47
- lastWasText = true;
42
+ if (!textOpened) {
43
+ process.stdout.write(assistantPrefix() + " ");
44
+ textOpened = true;
48
45
  }
49
46
  process.stdout.write(text);
50
47
  },
51
48
  onToolCallDelta: (delta) => {
52
- // Print the tool header once we know the name (first chunk for that index)
49
+ // Print friendly tool-call header once name + (eventually) args land.
50
+ // We delay the announce until args are mostly assembled — see below.
53
51
  if (delta.name && !announced.has(delta.index)) {
54
52
  announced.add(delta.index);
55
- if (lastWasText) process.stdout.write("\n");
56
- lastWasText = false;
57
- process.stdout.write(c.cyan(c.bold(delta.name)) + c.gray("(...)") + c.gray(" preparing args\n"));
53
+ if (textOpened) {
54
+ process.stdout.write("\n");
55
+ textOpened = false;
56
+ }
57
+ process.stdout.write(c.cyan(" ⚡ ") + c.bold(c.cyan(delta.name)) + c.dim(" preparing…\n"));
58
58
  }
59
59
  },
60
60
  });
@@ -65,21 +65,24 @@ export async function runAgent({
65
65
  throw err;
66
66
  }
67
67
 
68
- // End-of-turn newline + cost meter
69
- if (lastWasText) process.stdout.write("\n");
68
+ // End-of-turn: newline + cost meter
69
+ if (textOpened) process.stdout.write("\n");
70
70
  totalCredits += res.creditsCharged ?? 0;
71
71
  totalIn += res.usage?.prompt_tokens ?? 0;
72
72
  totalOut += res.usage?.completion_tokens ?? 0;
73
73
  if (typeof res.balanceAfter === "number") lastBalance = res.balanceAfter;
74
74
  onTokens({ totalCredits, totalIn, totalOut, balance: lastBalance });
75
- process.stdout.write(
76
- c.dim(` ${res.creditsCharged ?? 0} cr · ${res.usage?.prompt_tokens ?? 0}→${res.usage?.completion_tokens ?? 0} tokens · finish: ${res.finish_reason}\n`),
77
- );
75
+ process.stdout.write(costLine({
76
+ creditsCharged: res.creditsCharged ?? 0,
77
+ inputTokens: res.usage?.prompt_tokens ?? 0,
78
+ outputTokens: res.usage?.completion_tokens ?? 0,
79
+ finishReason: res.finish_reason,
80
+ }) + "\n");
78
81
 
79
- // Push assistant message into history
82
+ // Push assistant message into history. Coerce null → "" (Venice hates null).
80
83
  messages.push({
81
84
  role: "assistant",
82
- content: res.message.content,
85
+ content: res.message.content ?? "",
83
86
  tool_calls: res.message.tool_calls,
84
87
  });
85
88
 
@@ -88,19 +91,16 @@ export async function runAgent({
88
91
  return { ok: true, totalCredits, totalIn, totalOut, turns: i + 1, balance: lastBalance, messages };
89
92
  }
90
93
 
91
- // Execute each tool call. Show the actual args (now that we have them
92
- // fully assembled) and run.
94
+ // Execute each tool call. Now we know the full args, render the friendly
95
+ // label + execute + show summarized result.
93
96
  for (const call of toolCalls) {
94
97
  let args = {};
95
98
  try { args = JSON.parse(call.function.arguments || "{}"); } catch { /* leave empty */ }
96
99
  console.log("");
97
- console.log(toolHeader(call.function.name, args));
100
+ console.log(toolAnnounce(call.function.name, args));
98
101
 
99
102
  const result = await executeTool(call, { cwd, autoYes, unsafePaths });
100
- if (result.output) {
101
- const preview = result.output.length > 800 ? result.output.slice(0, 800) + "\n…(truncated)" : result.output;
102
- console.log(toolResult(preview, result.ok));
103
- }
103
+ console.log(toolResultLine(call.function.name, result));
104
104
 
105
105
  messages.push({
106
106
  role: "tool",
@@ -110,6 +110,6 @@ export async function runAgent({
110
110
  }
111
111
  }
112
112
 
113
- console.log(c.yellow(`\nReached max turns (${maxTurns}). Stopping.`));
113
+ console.log(c.yellow(`\n⚠ Reached max turns (${maxTurns}). Stopping.`));
114
114
  return { ok: false, error: new Error("Max turns reached"), totalCredits, totalIn, totalOut, balance: lastBalance, messages };
115
115
  }
package/src/render.js CHANGED
@@ -1,4 +1,5 @@
1
- // ANSI helpers — no chalk dependency.
1
+ // ANSI helpers — no chalk dependency. Designed to feel like Claude Code:
2
+ // bordered banner, indented tool calls, structured tool-result rendering.
2
3
 
3
4
  const isTty = process.stdout.isTTY;
4
5
  const noColor = !!process.env.NO_COLOR || !isTty;
@@ -10,6 +11,7 @@ function wrap(open, close) {
10
11
  export const c = {
11
12
  bold: wrap(1, 22),
12
13
  dim: wrap(2, 22),
14
+ italic: wrap(3, 23),
13
15
  red: wrap(31, 39),
14
16
  green: wrap(32, 39),
15
17
  yellow: wrap(33, 39),
@@ -17,42 +19,180 @@ export const c = {
17
19
  magenta: wrap(35, 39),
18
20
  cyan: wrap(36, 39),
19
21
  gray: wrap(90, 39),
22
+ white: wrap(37, 39),
20
23
  };
21
24
 
25
+ const W = () => Math.min(process.stdout.columns || 80, 90);
26
+
27
+ export function hr() {
28
+ return c.gray("─".repeat(W()));
29
+ }
30
+
22
31
  export function divider() {
23
- const w = process.stdout.columns || 60;
24
- return c.gray("─".repeat(Math.min(60, w)));
32
+ return hr();
33
+ }
34
+
35
+ /* ─── Banner — Claude-Code-style box ─── */
36
+
37
+ export function banner({ version, model, mode, balance, cwd }) {
38
+ const w = Math.min(W(), 78);
39
+ const top = c.gray("╭" + "─".repeat(w - 2) + "╮");
40
+ const bot = c.gray("╰" + "─".repeat(w - 2) + "╯");
41
+ const pad = (line) => {
42
+ // Strip ANSI for length calc
43
+ const visible = line.replace(/\x1b\[[0-9;]*m/g, "");
44
+ const space = Math.max(0, w - 2 - visible.length - 2);
45
+ return c.gray("│ ") + line + " ".repeat(space) + c.gray(" │");
46
+ };
47
+ const blank = pad("");
48
+ const title = pad(`${c.magenta(c.bold("✻ aether-code"))} ${c.gray("v" + version)}`);
49
+ const sub = pad(`${c.gray(model + " · uncensored · " + mode + (balance != null ? " · " + balance.toLocaleString() + " credits" : ""))}`);
50
+ const cwdLine = pad(c.gray("📁 " + truncatePath(cwd, w - 6)));
51
+ const tip = pad(c.gray("Type ") + c.cyan("/help") + c.gray(" for shortcuts. ") + c.cyan("/exit") + c.gray(" or Ctrl+C twice to quit."));
52
+ return [top, blank, title, sub, cwdLine, blank, tip, bot].join("\n");
53
+ }
54
+
55
+ function truncatePath(p, max) {
56
+ if (!p) return "";
57
+ if (p.length <= max) return p;
58
+ return "…" + p.slice(-(max - 1));
59
+ }
60
+
61
+ /* ─── Turn / cost line ─── */
62
+
63
+ export function turnLine(n) {
64
+ return c.dim("─── turn " + n + " " + "─".repeat(Math.max(0, W() - 12 - String(n).length)));
65
+ }
66
+
67
+ export function costLine({ creditsCharged, inputTokens, outputTokens, finishReason }) {
68
+ const parts = [
69
+ `${creditsCharged} cr`,
70
+ `${inputTokens}→${outputTokens} tok`,
71
+ finishReason ? `finish: ${finishReason}` : null,
72
+ ].filter(Boolean);
73
+ return " " + c.dim(parts.join(" · "));
74
+ }
75
+
76
+ /* ─── Assistant text ─── */
77
+
78
+ export function assistantPrefix() {
79
+ return c.magenta("● ") + c.bold(c.magenta("Aether")) + c.gray(":");
25
80
  }
26
81
 
27
- export function turn(n) {
28
- return c.gray(`turn ${n}`);
82
+ /* ─── Tool calls — friendly labels instead of raw JSON ─── */
83
+
84
+ export function toolAnnounce(name, args) {
85
+ const label = TOOL_LABELS[name] ? TOOL_LABELS[name](args) : `${name}(${jsonPreview(args)})`;
86
+ return c.cyan(" ⚡ ") + c.bold(c.cyan(name)) + c.gray(" ") + c.gray(label);
87
+ }
88
+
89
+ const TOOL_LABELS = {
90
+ read_file: (a) => `→ ${a.path || "?"}`,
91
+ list_dir: (a) => `→ ${a.path || "."}`,
92
+ search_files: (a) => `→ ${a.pattern || "?"} in ${a.path || "."}`,
93
+ write_file: (a) => `→ ${a.path || "?"} ${c.gray("(" + (a.content?.length || 0) + " bytes)")}`,
94
+ edit_file: (a) => `→ ${a.path || "?"}`,
95
+ run_shell: (a) => `${c.yellow("$")} ${a.command || "?"}` + (a.cwd && a.cwd !== "." ? c.gray(` (cwd: ${a.cwd})`) : ""),
96
+ };
97
+
98
+ function jsonPreview(obj) {
99
+ const s = JSON.stringify(obj);
100
+ return s.length > 80 ? s.slice(0, 77) + "..." : s;
29
101
  }
30
102
 
31
- export function toolHeader(name, args) {
32
- // Format args compactly. If any value is huge, truncate it.
33
- const compact = JSON.stringify(args);
34
- const trimmed = compact.length > 120 ? compact.slice(0, 117) + "..." : compact;
35
- return `${c.cyan(c.bold(name))}${c.gray("(")}${c.gray(trimmed)}${c.gray(")")}`;
103
+ /* ─── Tool results — structured render per tool ─── */
104
+
105
+ export function toolResultLine(name, result) {
106
+ const icon = result.ok ? c.green(" ✓") : c.red("");
107
+ const summary = summarizeResult(name, result);
108
+ return `${icon} ${c.dim(summary)}`;
36
109
  }
37
110
 
38
- export function toolResult(text, ok = true) {
39
- const prefix = ok ? c.green(" ✓ ") : c.red(" ✗ ");
40
- // First line bold-ish, then dim continuation
41
- const lines = text.split("\n");
42
- const head = lines[0].slice(0, 200);
43
- const rest = lines.slice(1, 6).join("\n").slice(0, 600);
44
- return `${prefix}${head}${rest ? "\n" + c.dim(rest) : ""}`;
111
+ function summarizeResult(name, result) {
112
+ if (!result.ok) {
113
+ // Try to extract a useful one-liner from error JSON
114
+ let txt = result.output || "";
115
+ try {
116
+ const j = JSON.parse(txt);
117
+ if (j.exit_code !== undefined) {
118
+ // Shell error
119
+ const stderr = (j.stderr || "").trim().split("\n")[0] || "(no stderr)";
120
+ return `exit ${j.exit_code} — ${stderr}`;
121
+ }
122
+ } catch { /* not JSON */ }
123
+ const firstLine = txt.split("\n")[0];
124
+ return firstLine.length > 100 ? firstLine.slice(0, 97) + "..." : firstLine;
125
+ }
126
+ // Success — short summary by tool type
127
+ const out = result.output || "";
128
+ if (name === "read_file") {
129
+ const lines = out.split("\n").length;
130
+ return `read ${out.length} bytes (${lines} line${lines === 1 ? "" : "s"})`;
131
+ }
132
+ if (name === "list_dir") {
133
+ try {
134
+ const items = JSON.parse(out);
135
+ return `${items.length} entr${items.length === 1 ? "y" : "ies"}`;
136
+ } catch {
137
+ return "ok";
138
+ }
139
+ }
140
+ if (name === "search_files") {
141
+ try {
142
+ const matches = JSON.parse(out);
143
+ return `${matches.length} match${matches.length === 1 ? "" : "es"}`;
144
+ } catch {
145
+ return "ok";
146
+ }
147
+ }
148
+ if (name === "write_file" || name === "edit_file") {
149
+ return out.split("\n")[0];
150
+ }
151
+ if (name === "run_shell") {
152
+ try {
153
+ const j = JSON.parse(out);
154
+ const stdoutLines = (j.stdout || "").split("\n").filter(Boolean).length;
155
+ return `exit ${j.exit_code} · ${stdoutLines} stdout line${stdoutLines === 1 ? "" : "s"}`;
156
+ } catch {
157
+ return "ok";
158
+ }
159
+ }
160
+ return out.split("\n")[0].slice(0, 80);
45
161
  }
46
162
 
47
- export function assistant(text) {
48
- // Indent each line for visual separation from tool calls
49
- return text.split("\n").map((l) => ` ${l}`).join("\n");
163
+ /* ─── Status line at end of turn ─── */
164
+
165
+ export function statusLine({ sessionCredits, sessionIn, sessionOut, balance, history, mode }) {
166
+ const parts = [];
167
+ parts.push(`session: ${sessionCredits} cr · ${sessionIn}→${sessionOut} tok`);
168
+ if (balance != null) parts.push(`balance: ${balance.toLocaleString()}`);
169
+ if (history > 0) parts.push(`history: ${history} msg${history === 1 ? "" : "s"}`);
170
+ parts.push(c.cyan(mode));
171
+ return c.dim(parts.join(" · "));
50
172
  }
51
173
 
174
+ /* ─── Errors ─── */
175
+
52
176
  export function errorLine(msg) {
53
- return `${c.red(c.bold("Error:"))} ${msg}`;
177
+ return `${c.red(c.bold("Error:"))} ${msg}`;
178
+ }
179
+
180
+ export function warnLine(msg) {
181
+ return `${c.yellow("⚠ ")} ${msg}`;
54
182
  }
55
183
 
56
184
  export function note(msg) {
57
185
  return c.dim(msg);
58
186
  }
187
+
188
+ /* ─── Confirmation prompt rendering ─── */
189
+
190
+ export function confirmPrompt(label) {
191
+ return c.yellow(" ? ") + c.bold(label) + c.gray(" [y/N] ");
192
+ }
193
+
194
+ /* ─── Spinner / pending indicator (no animation, just text) ─── */
195
+
196
+ export function pending(msg) {
197
+ return c.gray(" ⋯ " + msg);
198
+ }
package/src/repl.js CHANGED
@@ -13,9 +13,9 @@ import path from "node:path";
13
13
  import { runAgent } from "./agent.js";
14
14
  import { fetchBalance, AetherError } from "./api.js";
15
15
  import { runSetup } from "./setup.js";
16
- import { c, errorLine } from "./render.js";
16
+ import { c, errorLine, banner, statusLine } from "./render.js";
17
17
 
18
- const VERSION = "0.2.0";
18
+ const VERSION = "0.4.0";
19
19
  const MODEL_NAME = "Aether Core";
20
20
 
21
21
  const SHORTCUTS = `
@@ -222,25 +222,23 @@ async function handleSlash(line, state) {
222
222
 
223
223
  function printBanner(state) {
224
224
  console.log("");
225
- console.log(c.bold(c.magenta(`aether-code`)) + c.gray(` v${VERSION}`));
226
- console.log(
227
- c.gray(`${MODEL_NAME} (1M context) · uncensored · `) +
228
- c.cyan(state.autoYes ? "auto-yes" : "review mode") +
229
- (state.balance != null ? c.gray(` · ${state.balance.toLocaleString()} credits`) : ""),
230
- );
231
- console.log(c.gray(state.cwd));
232
- console.log("");
233
- console.log(c.gray(`Type ${c.cyan("/help")} for shortcuts. ${c.cyan("/exit")} or Ctrl+C twice to quit.`));
225
+ console.log(banner({
226
+ version: VERSION,
227
+ model: MODEL_NAME + " (1M context)",
228
+ mode: state.autoYes ? "auto-yes" : "review mode",
229
+ balance: state.balance,
230
+ cwd: state.cwd,
231
+ }));
234
232
  console.log("");
235
233
  }
236
234
 
237
235
  function printStatusLine(state) {
238
- const parts = [];
239
- parts.push(c.gray(`session: ${state.sessionCredits} cr · ${state.sessionIn}→${state.sessionOut} tokens`));
240
- if (state.balance != null) parts.push(c.gray(`balance: ${state.balance.toLocaleString()}`));
241
- if (state.messages.length > 0) {
242
- parts.push(c.gray(`history: ${state.messages.length} msg${state.messages.length === 1 ? "" : "s"}`));
243
- }
244
- parts.push(c.cyan(state.autoYes ? "auto-yes" : "review"));
245
- console.log(c.dim(parts.join(" · ")));
236
+ console.log(statusLine({
237
+ sessionCredits: state.sessionCredits,
238
+ sessionIn: state.sessionIn,
239
+ sessionOut: state.sessionOut,
240
+ balance: state.balance,
241
+ history: state.messages.length,
242
+ mode: state.autoYes ? "auto-yes" : "review",
243
+ }));
246
244
  }
package/src/tools.js CHANGED
@@ -147,8 +147,9 @@ function ask(question) {
147
147
 
148
148
  async function confirm(question, autoYes) {
149
149
  if (autoYes) return true;
150
- const ans = await ask(`${question} ${c.dim("[y/N]: ")}`);
151
- return ans === "y" || ans === "yes";
150
+ const ans = await ask(c.yellow(" ? ") + c.bold(question) + c.gray(" [y/N] "));
151
+ // Accept y, yes, or any string starting with y (handles double-tap "yy")
152
+ return /^y/i.test(ans.trim());
152
153
  }
153
154
 
154
155
  /* ─────────────────────── Implementations ─────────────────────── */