aether-code 0.14.0 → 0.16.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.
- package/bin/aether-code.js +1 -1
- package/package.json +9 -3
- package/src/ink-input.js +91 -0
- package/src/repl.js +59 -32
- package/src/update-check.js +60 -0
package/bin/aether-code.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aether-code",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
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",
|
|
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
|
}
|
package/src/ink-input.js
ADDED
|
@@ -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
|
@@ -14,8 +14,10 @@ import { runAgent } from "./agent.js";
|
|
|
14
14
|
import { fetchBalance, AetherError } from "./api.js";
|
|
15
15
|
import { runSetup } from "./setup.js";
|
|
16
16
|
import { c, errorLine } from "./render.js";
|
|
17
|
+
import { checkForUpdate } from "./update-check.js";
|
|
18
|
+
import { promptBoxed, EXIT_SIGNAL } from "./ink-input.js";
|
|
17
19
|
|
|
18
|
-
const VERSION = "0.
|
|
20
|
+
const VERSION = "0.16.0";
|
|
19
21
|
const MODEL_NAME = "Aether Core";
|
|
20
22
|
|
|
21
23
|
const SHORTCUTS = `
|
|
@@ -44,6 +46,10 @@ export async function runRepl({ cwd: initialCwd, autoYes: initialAutoYes, maxTur
|
|
|
44
46
|
sessionOut: 0,
|
|
45
47
|
};
|
|
46
48
|
|
|
49
|
+
// Kick off the npm update check concurrently with the balance fetch so it
|
|
50
|
+
// adds no startup latency; the nudge (if any) prints just under the banner.
|
|
51
|
+
const updatePromise = checkForUpdate().catch(() => null);
|
|
52
|
+
|
|
47
53
|
// Free balance check up front. If no key configured, walk through first-time
|
|
48
54
|
// setup flow (open browser → paste key → verify → save).
|
|
49
55
|
let needsSetup = false;
|
|
@@ -75,46 +81,68 @@ export async function runRepl({ cwd: initialCwd, autoYes: initialAutoYes, maxTur
|
|
|
75
81
|
}
|
|
76
82
|
|
|
77
83
|
printBanner(state);
|
|
84
|
+
const updateNudge = await updatePromise;
|
|
85
|
+
if (updateNudge) console.log(updateNudge + "\n");
|
|
78
86
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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;
|
|
85
94
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
rl.close();
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
});
|
|
95
|
+
function ensureReadline() {
|
|
96
|
+
if (rl) return rl;
|
|
97
|
+
rl = readline.createInterface({ input: process.stdin, output: process.stdout, historySize: 200 });
|
|
98
|
+
let lastSigint = 0;
|
|
99
|
+
rl.on("SIGINT", () => {
|
|
100
|
+
const now = Date.now();
|
|
101
|
+
if (now - lastSigint < 1500) { console.log(c.gray("\nbye.")); rl.close(); process.exit(0); }
|
|
102
|
+
lastSigint = now;
|
|
103
|
+
console.log(c.gray(`\n(Press Ctrl+C again within 1.5s to exit, or type ${c.cyan("/exit")})`));
|
|
104
|
+
});
|
|
105
|
+
return rl;
|
|
106
|
+
}
|
|
99
107
|
|
|
100
|
-
|
|
108
|
+
function readlineQuestion() {
|
|
109
|
+
const r = ensureReadline();
|
|
110
|
+
return new Promise((resolve) => r.question(c.magenta("> "), (ans) => resolve(ans)));
|
|
111
|
+
}
|
|
101
112
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
if (!
|
|
105
|
-
|
|
106
|
-
|
|
113
|
+
// Returns the next raw input line, or EXIT_SIGNAL to quit.
|
|
114
|
+
async function nextLine() {
|
|
115
|
+
if (useInk && !inkBroken) {
|
|
116
|
+
try {
|
|
117
|
+
return await promptBoxed({
|
|
118
|
+
statusLeft: ` ${c.cyan("/help")}${c.dim(" shortcuts")} ${c.cyan("/exit")}${c.dim(" quit")}`,
|
|
119
|
+
statusRight: `${state.autoYes ? "auto-yes" : "review"} · ${MODEL_NAME}`,
|
|
120
|
+
history: inputHistory,
|
|
121
|
+
});
|
|
122
|
+
} catch {
|
|
123
|
+
inkBroken = true;
|
|
124
|
+
console.log(c.gray("(rich input unavailable here — using the basic prompt)"));
|
|
125
|
+
}
|
|
107
126
|
}
|
|
127
|
+
return readlineQuestion();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
while (true) {
|
|
131
|
+
const raw = await nextLine();
|
|
132
|
+
if (raw === EXIT_SIGNAL) { console.log(c.gray("bye.")); if (rl) rl.close(); return; }
|
|
133
|
+
const line = (raw ?? "").trim();
|
|
134
|
+
if (!line) continue;
|
|
135
|
+
|
|
136
|
+
// Echo the submitted line so it persists in scrollback (Ink clears its box
|
|
137
|
+
// region on unmount). Skip in readline mode — the terminal already echoed it.
|
|
138
|
+
if (useInk && !inkBroken) console.log(c.magenta("> ") + line);
|
|
139
|
+
inputHistory.push(line);
|
|
108
140
|
|
|
109
141
|
// Slash command?
|
|
110
142
|
if (line.startsWith("/") || line === "?") {
|
|
111
143
|
const handled = await handleSlash(line, state);
|
|
112
|
-
if (handled === "exit") {
|
|
113
|
-
rl.close();
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
144
|
+
if (handled === "exit") { if (rl) rl.close(); return; }
|
|
116
145
|
printStatusLine(state);
|
|
117
|
-
rl.prompt();
|
|
118
146
|
continue;
|
|
119
147
|
}
|
|
120
148
|
|
|
@@ -140,7 +168,6 @@ export async function runRepl({ cwd: initialCwd, autoYes: initialAutoYes, maxTur
|
|
|
140
168
|
}
|
|
141
169
|
|
|
142
170
|
printStatusLine(state);
|
|
143
|
-
rl.prompt();
|
|
144
171
|
}
|
|
145
172
|
}
|
|
146
173
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Startup update check — like Claude Code / npm itself nudging you when a newer
|
|
2
|
+
// version is published. Never throws, bounded by a short timeout so it can't
|
|
3
|
+
// slow or block startup; silent when offline or already current.
|
|
4
|
+
|
|
5
|
+
import { readFileSync } from "node:fs";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { dirname, join } from "node:path";
|
|
8
|
+
import { c } from "./render.js";
|
|
9
|
+
|
|
10
|
+
export function currentVersion() {
|
|
11
|
+
try {
|
|
12
|
+
const p = join(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
|
|
13
|
+
return JSON.parse(readFileSync(p, "utf8")).version || null;
|
|
14
|
+
} catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Compare two "x.y.z" strings. Returns >0 if a is newer than b.
|
|
20
|
+
export function cmpVersion(a, b) {
|
|
21
|
+
const pa = String(a).split(".").map(Number);
|
|
22
|
+
const pb = String(b).split(".").map(Number);
|
|
23
|
+
for (let i = 0; i < 3; i++) {
|
|
24
|
+
const d = (pa[i] || 0) - (pb[i] || 0);
|
|
25
|
+
if (d !== 0) return d;
|
|
26
|
+
}
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Resolve a one-line "update available" nudge if the npm registry has a newer
|
|
32
|
+
* version than the installed one — otherwise null. Bounded to ~2s; any failure
|
|
33
|
+
* (offline, timeout, registry hiccup) resolves to null silently.
|
|
34
|
+
*/
|
|
35
|
+
export async function checkForUpdate({ fetchImpl = fetch, timeoutMs = 2000 } = {}) {
|
|
36
|
+
const current = currentVersion();
|
|
37
|
+
if (!current) return null;
|
|
38
|
+
try {
|
|
39
|
+
const ac = new AbortController();
|
|
40
|
+
const t = setTimeout(() => ac.abort(), timeoutMs);
|
|
41
|
+
let res;
|
|
42
|
+
try {
|
|
43
|
+
res = await fetchImpl("https://registry.npmjs.org/aether-code/latest", { signal: ac.signal });
|
|
44
|
+
} finally {
|
|
45
|
+
clearTimeout(t);
|
|
46
|
+
}
|
|
47
|
+
if (!res || !res.ok) return null;
|
|
48
|
+
const latest = (await res.json())?.version;
|
|
49
|
+
if (latest && cmpVersion(latest, current) > 0) {
|
|
50
|
+
return (
|
|
51
|
+
c.yellow("update available: ") +
|
|
52
|
+
c.gray(current) + " -> " + c.bold(c.green(latest)) +
|
|
53
|
+
c.gray(" · run ") + c.cyan("npm i -g aether-code@latest")
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
/* offline / aborted / parse error — stay silent */
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|