aether-code 0.15.0 → 0.16.1

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.15.0";
29
+ const VERSION = "0.16.1";
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.15.0",
3
+ "version": "0.16.1",
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": {
@@ -23,7 +23,7 @@
23
23
  "node": ">=18"
24
24
  },
25
25
  "scripts": {
26
- "lint": "node --check bin/aether-code.js src/agent.js src/api.js src/config.js src/render.js src/tools.js src/diff.js src/repl.js src/mcp.js src/mcp-cli.js src/mcp-registry.js src/skills.js src/update-check.js",
26
+ "lint": "node --check bin/aether-code.js src/agent.js src/api.js src/config.js src/render.js src/tools.js src/diff.js src/repl.js src/mcp.js src/mcp-cli.js src/mcp-registry.js src/skills.js src/update-check.js src/ink-input.js",
27
27
  "test": "node --test \"test/**/*.test.js\"",
28
28
  "prepublishOnly": "npm run lint && npm test"
29
29
  },
@@ -64,6 +64,12 @@
64
64
  "trynoguard"
65
65
  ],
66
66
  "dependencies": {
67
- "@modelcontextprotocol/sdk": "^1.29.0"
67
+ "@modelcontextprotocol/sdk": "^1.29.0",
68
+ "ink": "^7.1.0",
69
+ "ink-text-input": "^6.0.0",
70
+ "react": "^19.2.7"
71
+ },
72
+ "devDependencies": {
73
+ "ink-testing-library": "^4.0.0"
68
74
  }
69
75
  }
@@ -0,0 +1,91 @@
1
+ // Ink-based boxed input for the REPL — the bordered live input box (Claude
2
+ // Code-style). Rendered fresh for each prompt, then unmounted so the agent's
3
+ // plain console.log streaming during a turn never fights Ink for the terminal.
4
+ //
5
+ // Uses a `single`-line border (broadly supported in cmd.exe Consolas) rather
6
+ // than rounded corners, which render as tofu on some Windows fonts.
7
+ //
8
+ // No JSX / no build step: components are built with React.createElement so the
9
+ // package ships as-is. `InputApp` is exported (pure UI + callbacks) so it can be
10
+ // unit-tested with ink-testing-library without a real TTY.
11
+
12
+ import React, { useState, useRef } from "react";
13
+ import { render, Box, Text, useApp, useInput } from "ink";
14
+ import TextInput from "ink-text-input";
15
+
16
+ const h = React.createElement;
17
+
18
+ // Sentinel promptBoxed() resolves with on a double Ctrl+C, so the caller can
19
+ // distinguish "user wants to quit" from an empty submitted line. Plain ASCII so
20
+ // it can never be confused with a real keystroke or hide invisible chars.
21
+ export const EXIT_SIGNAL = "<<aether-exit>>";
22
+
23
+ /**
24
+ * Pure input UI. Calls onSubmit(value) on Enter and onExit() on a double Ctrl+C.
25
+ * Up/Down arrows walk `history` (newest last). No process/terminal side effects
26
+ * of its own, so it's testable in isolation with ink-testing-library.
27
+ */
28
+ export function InputApp({ onSubmit, onExit, statusLeft = "", statusRight = "", history = [] }) {
29
+ const [value, setValue] = useState("");
30
+ const lastCtrlC = useRef(0);
31
+ const histIdx = useRef(history.length);
32
+
33
+ useInput((input, key) => {
34
+ if (key.ctrl && input === "c") {
35
+ const now = Date.now();
36
+ if (now - lastCtrlC.current < 1500) onExit();
37
+ else { lastCtrlC.current = now; setValue(""); }
38
+ return;
39
+ }
40
+ if (key.upArrow) {
41
+ if (histIdx.current > 0) { histIdx.current -= 1; setValue(history[histIdx.current] ?? ""); }
42
+ } else if (key.downArrow) {
43
+ if (histIdx.current < history.length - 1) { histIdx.current += 1; setValue(history[histIdx.current] ?? ""); }
44
+ else { histIdx.current = history.length; setValue(""); }
45
+ }
46
+ });
47
+
48
+ return h(
49
+ Box,
50
+ { flexDirection: "column", width: "100%" },
51
+ h(
52
+ Box,
53
+ { borderStyle: "single", borderColor: "magenta", paddingX: 1 },
54
+ h(Text, { color: "magenta", bold: true }, "> "),
55
+ h(TextInput, { value, onChange: setValue, onSubmit, placeholder: "" }),
56
+ ),
57
+ h(
58
+ Box,
59
+ { paddingX: 1, width: "100%", justifyContent: "space-between" },
60
+ h(Text, { dimColor: true }, statusLeft),
61
+ h(Text, { dimColor: true }, statusRight),
62
+ ),
63
+ );
64
+ }
65
+
66
+ /**
67
+ * Render a one-shot boxed input and resolve with the submitted line. Resolves
68
+ * EXIT_SIGNAL on a double Ctrl+C; resolves "" if the app exits without a submit.
69
+ */
70
+ export function promptBoxed(opts = {}) {
71
+ return new Promise((resolve) => {
72
+ let done = false;
73
+ let appApi = null;
74
+ const fin = (val) => {
75
+ if (done) return;
76
+ done = true;
77
+ try { if (appApi) appApi.exit(); } catch { /* already exiting */ }
78
+ resolve(val);
79
+ };
80
+ const Wrapper = () => {
81
+ appApi = useApp();
82
+ return h(InputApp, {
83
+ ...opts,
84
+ onSubmit: (v) => fin(v),
85
+ onExit: () => fin(EXIT_SIGNAL),
86
+ });
87
+ };
88
+ const instance = render(h(Wrapper), { exitOnCtrlC: false });
89
+ instance.waitUntilExit().then(() => { if (!done) { done = true; resolve(""); } });
90
+ });
91
+ }
package/src/repl.js CHANGED
@@ -15,8 +15,9 @@ import { fetchBalance, AetherError } from "./api.js";
15
15
  import { runSetup } from "./setup.js";
16
16
  import { c, errorLine } from "./render.js";
17
17
  import { checkForUpdate } from "./update-check.js";
18
+ import { promptBoxed, EXIT_SIGNAL } from "./ink-input.js";
18
19
 
19
- const VERSION = "0.15.0";
20
+ const VERSION = "0.16.1";
20
21
  const MODEL_NAME = "Aether Core";
21
22
 
22
23
  const SHORTCUTS = `
@@ -83,45 +84,69 @@ export async function runRepl({ cwd: initialCwd, autoYes: initialAutoYes, maxTur
83
84
  const updateNudge = await updatePromise;
84
85
  if (updateNudge) console.log(updateNudge + "\n");
85
86
 
86
- const rl = readline.createInterface({
87
- input: process.stdin,
88
- output: process.stdout,
89
- prompt: c.magenta("> "),
90
- historySize: 200,
91
- });
87
+ // Input: the Ink boxed input (bordered, Claude-style) when we have a TTY,
88
+ // with a readline fallback for non-TTY/CI or if Ink can't init raw mode.
89
+ // Set AETHER_NO_INK=1 to force the plain prompt.
90
+ const inputHistory = [];
91
+ const useInk = !!process.stdin.isTTY && process.env.AETHER_NO_INK !== "1";
92
+ let inkBroken = false;
93
+ let rl = null;
92
94
 
93
- // Two-stage Ctrl+C: first cancels current line, second exits
94
- let lastSigint = 0;
95
- rl.on("SIGINT", () => {
96
- const now = Date.now();
97
- if (now - lastSigint < 1500) {
98
- console.log(c.gray("\nbye."));
99
- rl.close();
100
- process.exit(0);
101
- }
102
- lastSigint = now;
103
- console.log(c.gray(`\n(Press Ctrl+C again within 1.5s to exit, or type ${c.cyan("/exit")})`));
104
- rl.prompt();
105
- });
95
+ // The Ink box carries its own status bar; the plain prompt doesn't, so show a
96
+ // one-line hint up front when we won't be using Ink.
97
+ if (!useInk) console.log(` ${c.cyan("/help")}${c.dim(" shortcuts")} ${c.cyan("/exit")}${c.dim(" quit")}\n`);
106
98
 
107
- rl.prompt();
99
+ function ensureReadline() {
100
+ if (rl) return rl;
101
+ rl = readline.createInterface({ input: process.stdin, output: process.stdout, historySize: 200 });
102
+ let lastSigint = 0;
103
+ rl.on("SIGINT", () => {
104
+ const now = Date.now();
105
+ if (now - lastSigint < 1500) { console.log(c.gray("\nbye.")); rl.close(); process.exit(0); }
106
+ lastSigint = now;
107
+ console.log(c.gray(`\n(Press Ctrl+C again within 1.5s to exit, or type ${c.cyan("/exit")})`));
108
+ });
109
+ return rl;
110
+ }
108
111
 
109
- for await (const rawLine of rl) {
110
- const line = rawLine.trim();
111
- if (!line) {
112
- rl.prompt();
113
- continue;
112
+ function readlineQuestion() {
113
+ const r = ensureReadline();
114
+ return new Promise((resolve) => r.question(c.magenta("> "), (ans) => resolve(ans)));
115
+ }
116
+
117
+ // Returns the next raw input line, or EXIT_SIGNAL to quit.
118
+ async function nextLine() {
119
+ if (useInk && !inkBroken) {
120
+ try {
121
+ return await promptBoxed({
122
+ statusLeft: ` ${c.cyan("/help")}${c.dim(" shortcuts")} ${c.cyan("/exit")}${c.dim(" quit")}`,
123
+ statusRight: `${state.autoYes ? "auto-yes" : "review"} · ${MODEL_NAME}`,
124
+ history: inputHistory,
125
+ });
126
+ } catch {
127
+ inkBroken = true;
128
+ console.log(c.gray("(rich input unavailable here — using the basic prompt)"));
129
+ }
114
130
  }
131
+ return readlineQuestion();
132
+ }
133
+
134
+ while (true) {
135
+ const raw = await nextLine();
136
+ if (raw === EXIT_SIGNAL) { console.log(c.gray("bye.")); if (rl) rl.close(); return; }
137
+ const line = (raw ?? "").trim();
138
+ if (!line) continue;
139
+
140
+ // Echo the submitted line so it persists in scrollback (Ink clears its box
141
+ // region on unmount). Skip in readline mode — the terminal already echoed it.
142
+ if (useInk && !inkBroken) console.log(c.magenta("> ") + line);
143
+ inputHistory.push(line);
115
144
 
116
145
  // Slash command?
117
146
  if (line.startsWith("/") || line === "?") {
118
147
  const handled = await handleSlash(line, state);
119
- if (handled === "exit") {
120
- rl.close();
121
- return;
122
- }
148
+ if (handled === "exit") { if (rl) rl.close(); return; }
123
149
  printStatusLine(state);
124
- rl.prompt();
125
150
  continue;
126
151
  }
127
152
 
@@ -147,7 +172,6 @@ export async function runRepl({ cwd: initialCwd, autoYes: initialAutoYes, maxTur
147
172
  }
148
173
 
149
174
  printStatusLine(state);
150
- rl.prompt();
151
175
  }
152
176
  }
153
177
 
@@ -246,13 +270,10 @@ function printBanner(state) {
246
270
  console.log(` ${c.gray(mode)}${state.balance != null ? c.gray(` · ${state.balance.toLocaleString()} credits`) : ""}`);
247
271
  console.log(` ${c.gray(shortenPath(state.cwd, W - 2))}`);
248
272
  console.log(rule);
249
-
250
- // Bottom status bar: shortcuts on the left, mode on the right (Claude-style).
251
- const left = ` ${c.cyan("/help")}${c.dim(" shortcuts")} ${c.cyan("/exit")}${c.dim(" quit")}`;
252
- const right = `${c.cyan(mode)}${c.dim(" · ")}${c.gray(MODEL_NAME)} `;
253
- const gap = Math.max(3, cols - visLen(left) - visLen(right) - 1);
254
- console.log(left + " ".repeat(gap) + right);
255
273
  console.log("");
274
+ // No bottom status bar here — the Ink input box renders its own persistent
275
+ // status bar beneath it (one bar, Claude-style). The readline fallback prints
276
+ // a one-line hint instead (see runRepl).
256
277
  }
257
278
 
258
279
  function printStatusLine(state) {