aether-code 0.33.0 → 0.33.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.
- package/bin/aether-code.js +1 -1
- package/package.json +2 -2
- package/src/box-input.js +209 -132
- package/src/repl.js +249 -291
- package/src/update-check.js +117 -60
package/bin/aether-code.js
CHANGED
|
@@ -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.
|
|
31
|
+
const VERSION = "0.33.1";
|
|
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.
|
|
3
|
+
"version": "0.33.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": {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
},
|
|
26
26
|
"scripts": {
|
|
27
27
|
"postinstall": "node scripts/postinstall.js",
|
|
28
|
-
"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 scripts/postinstall.js",
|
|
28
|
+
"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/box-input.js src/ink-input.js scripts/postinstall.js",
|
|
29
29
|
"test": "node --test \"test/**/*.test.js\"",
|
|
30
30
|
"prepublishOnly": "npm run lint && npm test"
|
|
31
31
|
},
|
package/src/box-input.js
CHANGED
|
@@ -1,132 +1,209 @@
|
|
|
1
|
-
// Bordered single-line text input — raw-mode, cmd.exe-safe.
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
import readline from "node:readline";
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
else
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
1
|
+
// Bordered single-line text input — raw-mode, cmd.exe-safe.
|
|
2
|
+
//
|
|
3
|
+
// Features:
|
|
4
|
+
// - Ctrl+V / Alt+V: paste image from clipboard (saves to temp, attaches)
|
|
5
|
+
// - @file.png: reference images by path (parsed by agent.js)
|
|
6
|
+
// - Arrow keys, Home/End, Ctrl+A/E, Ctrl+U, history navigation
|
|
7
|
+
// - Horizontal scroll for long input
|
|
8
|
+
//
|
|
9
|
+
// Returns the typed string (with @temppath appended for clipboard images),
|
|
10
|
+
// or EXIT_SIGNAL on Ctrl+C / Ctrl+D.
|
|
11
|
+
|
|
12
|
+
import readline from "node:readline";
|
|
13
|
+
import { execFileSync } from "node:child_process";
|
|
14
|
+
import fs from "node:fs";
|
|
15
|
+
import os from "node:os";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import { c } from "./render.js";
|
|
18
|
+
|
|
19
|
+
export const EXIT_SIGNAL = "<<aether-exit>>";
|
|
20
|
+
|
|
21
|
+
const HIDE = "\x1b[?25l";
|
|
22
|
+
const SHOW = "\x1b[?25h";
|
|
23
|
+
const INV = (s) => `\x1b[7m${s}\x1b[27m`;
|
|
24
|
+
|
|
25
|
+
// Save clipboard image to a temp file. Returns the file path or null.
|
|
26
|
+
// Platform-specific: PowerShell on Windows, pngpaste on macOS, xclip on Linux.
|
|
27
|
+
function grabClipboardImage() {
|
|
28
|
+
const tmpFile = path.join(os.tmpdir(), `aether-paste-${Date.now()}.png`);
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
if (process.platform === "win32") {
|
|
32
|
+
const script =
|
|
33
|
+
"Add-Type -AssemblyName System.Windows.Forms;" +
|
|
34
|
+
"$img=[System.Windows.Forms.Clipboard]::GetImage();" +
|
|
35
|
+
"if($img){$img.Save('" + tmpFile.replace(/'/g, "''") + "'," +
|
|
36
|
+
"[System.Drawing.Imaging.ImageFormat]::Png);'OK'}else{'NO_IMAGE'}";
|
|
37
|
+
const out = execFileSync("powershell.exe", [
|
|
38
|
+
"-NoProfile", "-NonInteractive", "-Command", script,
|
|
39
|
+
], { timeout: 5000, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
40
|
+
if (out.endsWith("OK") && fs.existsSync(tmpFile)) return tmpFile;
|
|
41
|
+
} else if (process.platform === "darwin") {
|
|
42
|
+
try {
|
|
43
|
+
execFileSync("pngpaste", [tmpFile], { timeout: 3000, stdio: "pipe" });
|
|
44
|
+
if (fs.existsSync(tmpFile) && fs.statSync(tmpFile).size > 0) return tmpFile;
|
|
45
|
+
} catch { /* pngpaste not installed — hint below */ }
|
|
46
|
+
} else {
|
|
47
|
+
// Linux: xclip
|
|
48
|
+
try {
|
|
49
|
+
const fd = fs.openSync(tmpFile, "w");
|
|
50
|
+
try {
|
|
51
|
+
execFileSync("xclip", ["-selection", "clipboard", "-t", "image/png", "-o"], {
|
|
52
|
+
timeout: 3000, stdio: ["pipe", fd, "pipe"],
|
|
53
|
+
});
|
|
54
|
+
} finally {
|
|
55
|
+
fs.closeSync(fd);
|
|
56
|
+
}
|
|
57
|
+
if (fs.existsSync(tmpFile) && fs.statSync(tmpFile).size > 0) return tmpFile;
|
|
58
|
+
} catch { /* xclip not installed */ }
|
|
59
|
+
}
|
|
60
|
+
} catch { /* any failure — fall through */ }
|
|
61
|
+
|
|
62
|
+
// Clean up empty/failed temp file
|
|
63
|
+
try { if (fs.existsSync(tmpFile)) fs.unlinkSync(tmpFile); } catch { /* ignore */ }
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function promptBox({ history = [], placeholder = "Message Aether…" } = {}) {
|
|
68
|
+
return new Promise((resolve) => {
|
|
69
|
+
// Non-interactive (piped / tests): plain readline, no box.
|
|
70
|
+
if (!process.stdin.isTTY) {
|
|
71
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
72
|
+
rl.question(c.cyan("> "), (a) => { rl.close(); resolve(a); });
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let buf = "";
|
|
77
|
+
let pos = 0;
|
|
78
|
+
const hist = history.slice();
|
|
79
|
+
let histIdx = hist.length;
|
|
80
|
+
let draft = "";
|
|
81
|
+
let drawn = 0;
|
|
82
|
+
let lastSigint = 0;
|
|
83
|
+
const clipImages = []; // temp file paths from clipboard pastes
|
|
84
|
+
|
|
85
|
+
const innerW = () => Math.max(24, Math.min(process.stdout.columns || 80, 100) - 6);
|
|
86
|
+
|
|
87
|
+
const draw = (first) => {
|
|
88
|
+
const W = innerW();
|
|
89
|
+
if (!first) process.stdout.write(`\x1b[${drawn}A`);
|
|
90
|
+
process.stdout.write("\x1b[J");
|
|
91
|
+
|
|
92
|
+
// Horizontal scroll: keep the caret within the visible window.
|
|
93
|
+
const maxVisible = W - 1;
|
|
94
|
+
const start = pos > maxVisible ? pos - maxVisible : 0;
|
|
95
|
+
const view = buf.slice(start, start + W);
|
|
96
|
+
const rel = pos - start;
|
|
97
|
+
|
|
98
|
+
let content;
|
|
99
|
+
if (buf.length === 0) {
|
|
100
|
+
const ph = placeholder.slice(0, W - 1);
|
|
101
|
+
content = INV(" ") + c.dim(ph) + " ".repeat(Math.max(0, W - 1 - ph.length));
|
|
102
|
+
} else {
|
|
103
|
+
let s = "";
|
|
104
|
+
for (let i = 0; i < W; i++) {
|
|
105
|
+
const ch = view[i] ?? " ";
|
|
106
|
+
s += i === rel ? INV(ch) : ch;
|
|
107
|
+
}
|
|
108
|
+
content = s;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const top = c.cyan("╭" + "─".repeat(W + 4) + "╮");
|
|
112
|
+
const mid = c.cyan("│") + " " + c.cyan(">") + " " + content + " " + c.cyan("│");
|
|
113
|
+
const bot = c.cyan("╰" + "─".repeat(W + 4) + "╯");
|
|
114
|
+
|
|
115
|
+
// Hint line — show paste indicator when images are queued
|
|
116
|
+
const imgTag = clipImages.length > 0
|
|
117
|
+
? " " + c.cyan(`[${clipImages.length} image${clipImages.length > 1 ? "s" : ""} pasted]`)
|
|
118
|
+
: "";
|
|
119
|
+
const hint = c.dim(" Enter send · Alt+V paste image · @file · /help") + imgTag;
|
|
120
|
+
|
|
121
|
+
process.stdout.write(top + "\n" + mid + "\n" + bot + "\n" + hint + "\n");
|
|
122
|
+
drawn = 4;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const finish = (result) => {
|
|
126
|
+
process.stdin.removeListener("keypress", onKey);
|
|
127
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
128
|
+
process.stdin.pause();
|
|
129
|
+
process.stdout.write(`\x1b[${drawn}A\x1b[J` + SHOW);
|
|
130
|
+
|
|
131
|
+
// Append clipboard image temp paths so agent.js's @-parser picks them up.
|
|
132
|
+
let finalResult = result;
|
|
133
|
+
if (result !== null && result !== EXIT_SIGNAL && clipImages.length > 0) {
|
|
134
|
+
const userText = (result || "").trim();
|
|
135
|
+
const imgRefs = clipImages.map((p) => `@"${p}"`).join(" ");
|
|
136
|
+
finalResult = userText ? `${userText} ${imgRefs}` : `What is in this image? ${imgRefs}`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Echo the submitted line so it persists in scrollback.
|
|
140
|
+
if (finalResult && finalResult !== EXIT_SIGNAL) {
|
|
141
|
+
const echoText = (result || "").trim();
|
|
142
|
+
const suffix = clipImages.length > 0
|
|
143
|
+
? c.cyan(` [+${clipImages.length} image${clipImages.length > 1 ? "s" : ""}]`)
|
|
144
|
+
: "";
|
|
145
|
+
if (echoText || clipImages.length > 0) {
|
|
146
|
+
process.stdout.write(c.cyan("> ") + (echoText || c.dim("(image)")) + suffix + "\n");
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
resolve(finalResult);
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const onKey = (str, key) => {
|
|
153
|
+
if (!key) return;
|
|
154
|
+
const k = key.name;
|
|
155
|
+
|
|
156
|
+
if (k === "return" || k === "enter") return finish(buf);
|
|
157
|
+
if (key.ctrl && k === "c") {
|
|
158
|
+
const now = Date.now();
|
|
159
|
+
if (now - lastSigint < 1500 || (buf.length === 0 && clipImages.length === 0)) return finish(EXIT_SIGNAL);
|
|
160
|
+
lastSigint = now;
|
|
161
|
+
buf = ""; pos = 0; clipImages.length = 0; draw(false);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (key.ctrl && k === "d") return finish(buf.length === 0 ? EXIT_SIGNAL : buf);
|
|
165
|
+
|
|
166
|
+
// Alt+V or Ctrl+V: paste image from clipboard
|
|
167
|
+
if ((key.meta && k === "v") || (key.ctrl && k === "v")) {
|
|
168
|
+
const imgPath = grabClipboardImage();
|
|
169
|
+
if (imgPath) {
|
|
170
|
+
clipImages.push(imgPath);
|
|
171
|
+
}
|
|
172
|
+
draw(false);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (k === "backspace") { if (pos > 0) { buf = buf.slice(0, pos - 1) + buf.slice(pos); pos--; } }
|
|
177
|
+
else if (k === "delete") { buf = buf.slice(0, pos) + buf.slice(pos + 1); }
|
|
178
|
+
else if (k === "left") { if (pos > 0) pos--; }
|
|
179
|
+
else if (k === "right") { if (pos < buf.length) pos++; }
|
|
180
|
+
else if (k === "home" || (key.ctrl && k === "a")) pos = 0;
|
|
181
|
+
else if (k === "end" || (key.ctrl && k === "e")) pos = buf.length;
|
|
182
|
+
else if (key.ctrl && k === "u") { buf = ""; pos = 0; }
|
|
183
|
+
else if (k === "up") {
|
|
184
|
+
if (histIdx === hist.length) draft = buf;
|
|
185
|
+
if (histIdx > 0) { histIdx--; buf = hist[histIdx] ?? ""; pos = buf.length; }
|
|
186
|
+
}
|
|
187
|
+
else if (k === "down") {
|
|
188
|
+
if (histIdx < hist.length) {
|
|
189
|
+
histIdx++;
|
|
190
|
+
buf = histIdx === hist.length ? draft : (hist[histIdx] ?? "");
|
|
191
|
+
pos = buf.length;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
else if (str && !key.ctrl && !key.meta && str >= " " && str !== "\x7f") {
|
|
195
|
+
buf = buf.slice(0, pos) + str + buf.slice(pos);
|
|
196
|
+
pos += str.length;
|
|
197
|
+
}
|
|
198
|
+
else return; // ignore unhandled keys without a redraw
|
|
199
|
+
draw(false);
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
process.stdout.write(HIDE);
|
|
203
|
+
readline.emitKeypressEvents(process.stdin);
|
|
204
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
205
|
+
process.stdin.resume();
|
|
206
|
+
process.stdin.on("keypress", onKey);
|
|
207
|
+
draw(true);
|
|
208
|
+
});
|
|
209
|
+
}
|
package/src/repl.js
CHANGED
|
@@ -1,291 +1,249 @@
|
|
|
1
|
-
// Interactive REPL — Claude-CLI-style. Launched when `aether-code` is run
|
|
2
|
-
// without a task argument.
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
console.log(
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
} else {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
"Enhanced spinner with mm:ss timing",
|
|
251
|
-
];
|
|
252
|
-
|
|
253
|
-
function printBanner(state) {
|
|
254
|
-
const mode = state.autoYes ? (state.unsafePaths ? "skip-permissions" : "auto-yes") : "review mode";
|
|
255
|
-
const credits = state.balance != null ? `${state.balance.toLocaleString()} credits` : "";
|
|
256
|
-
const left = boxLines(
|
|
257
|
-
[
|
|
258
|
-
c.bold(c.magenta("aether")) + c.gray(" · ") + c.bold(state.modelName) + c.gray(" v" + VERSION),
|
|
259
|
-
c.gray("uncensored AI coding agent"),
|
|
260
|
-
c.gray((credits ? credits + " · " : "") + mode),
|
|
261
|
-
c.gray(shortenPath(state.cwd, 52)),
|
|
262
|
-
],
|
|
263
|
-
{ color: c.magenta, padX: 2 },
|
|
264
|
-
);
|
|
265
|
-
const right = boxLines(
|
|
266
|
-
[c.bold(c.cyan("What's new")), ...WHATS_NEW.map((s) => c.gray("• " + s))],
|
|
267
|
-
{ color: c.cyan, padX: 2 },
|
|
268
|
-
);
|
|
269
|
-
|
|
270
|
-
console.log("");
|
|
271
|
-
const cols = process.stdout.columns || 80;
|
|
272
|
-
// Only place the two boxes side by side when there's room; otherwise the
|
|
273
|
-
// banner alone (narrow terminals would wrap and tear the second box).
|
|
274
|
-
if (cols >= visLen(left[0]) + visLen(right[0]) + 4) {
|
|
275
|
-
console.log(sideBySide(left, right, 3));
|
|
276
|
-
} else {
|
|
277
|
-
console.log(left.join("\n"));
|
|
278
|
-
}
|
|
279
|
-
console.log("");
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
function printStatusLine(state) {
|
|
283
|
-
const parts = [];
|
|
284
|
-
parts.push(c.gray(`session: ${state.sessionCredits} cr · ${state.sessionIn}→${state.sessionOut} tokens`));
|
|
285
|
-
if (state.balance != null) parts.push(c.gray(`balance: ${state.balance.toLocaleString()}`));
|
|
286
|
-
if (state.messages.length > 0) {
|
|
287
|
-
parts.push(c.gray(`history: ${state.messages.length} msg${state.messages.length === 1 ? "" : "s"}`));
|
|
288
|
-
}
|
|
289
|
-
parts.push(c.cyan(state.autoYes ? "auto-yes" : "review"));
|
|
290
|
-
console.log(c.dim(parts.join(" · ")));
|
|
291
|
-
}
|
|
1
|
+
// Interactive REPL — Claude-CLI-style. Launched when `aether-code` is run
|
|
2
|
+
// without a task argument.
|
|
3
|
+
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { runAgent } from "./agent.js";
|
|
6
|
+
import { fetchBalance, AetherError } from "./api.js";
|
|
7
|
+
import { runSetup } from "./setup.js";
|
|
8
|
+
import { c, boxLines, errorLine } from "./render.js";
|
|
9
|
+
import { autoUpdate } from "./update-check.js";
|
|
10
|
+
import { promptBox, EXIT_SIGNAL } from "./box-input.js";
|
|
11
|
+
|
|
12
|
+
const VERSION = "0.33.1";
|
|
13
|
+
const MODEL_NAME = "Aether Core";
|
|
14
|
+
|
|
15
|
+
const MODEL_ALIASES = {
|
|
16
|
+
core: { id: null, name: "Aether Core" },
|
|
17
|
+
gemma: { id: null, name: "Aether Core" },
|
|
18
|
+
ultra: { id: "claude-opus-4-6", name: "Aether Ultra" },
|
|
19
|
+
opus: { id: "claude-opus-4-6", name: "Aether Ultra" },
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const SHORTCUTS = `
|
|
23
|
+
${c.cyan("/help")} Show this help
|
|
24
|
+
${c.cyan("/exit")} Exit (or Ctrl+C twice)
|
|
25
|
+
${c.cyan("/clear")} Clear conversation history (start fresh)
|
|
26
|
+
${c.cyan("/balance")} Refresh and show credit balance
|
|
27
|
+
${c.cyan("/cwd")} ${c.gray("[path]")} Show or change working directory
|
|
28
|
+
${c.cyan("/yes")} Toggle auto-approve mode (skip y/N prompts)
|
|
29
|
+
${c.cyan("/turns")} ${c.gray("<n>")} Set max turns per prompt (default 25)
|
|
30
|
+
${c.cyan("/model")} ${c.gray("[core|ultra]")} Show or switch model (ultra=Opus — premium)
|
|
31
|
+
|
|
32
|
+
${c.gray("Anything else is sent to the agent as your next message.")}
|
|
33
|
+
${c.gray("Conversation history is kept across messages until you /clear.")}
|
|
34
|
+
`;
|
|
35
|
+
|
|
36
|
+
export async function runRepl({ cwd: initialCwd, autoYes: initialAutoYes, unsafePaths: initialUnsafePaths, maxTurns: initialMaxTurns, model: initialModel = null, mcpManager = null }) {
|
|
37
|
+
const initialAlias = Object.values(MODEL_ALIASES).find((a) => a.id === initialModel);
|
|
38
|
+
const state = {
|
|
39
|
+
cwd: initialCwd,
|
|
40
|
+
autoYes: !!initialAutoYes,
|
|
41
|
+
unsafePaths: !!initialUnsafePaths,
|
|
42
|
+
maxTurns: initialMaxTurns ?? 25,
|
|
43
|
+
model: initialModel ?? null,
|
|
44
|
+
modelName: initialAlias?.name ?? MODEL_NAME,
|
|
45
|
+
messages: [],
|
|
46
|
+
balance: null,
|
|
47
|
+
sessionCredits: 0,
|
|
48
|
+
sessionIn: 0,
|
|
49
|
+
sessionOut: 0,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Auto-update + balance fetch in parallel — zero startup latency added.
|
|
53
|
+
const updatePromise = autoUpdate().catch(() => null);
|
|
54
|
+
|
|
55
|
+
let needsSetup = false;
|
|
56
|
+
try {
|
|
57
|
+
const me = await fetchBalance();
|
|
58
|
+
state.balance = me.balance;
|
|
59
|
+
state.plan = me.plan || "FREE";
|
|
60
|
+
} catch (err) {
|
|
61
|
+
if (err instanceof AetherError && (err.code === "NO_API_KEY" || err.status === 401)) {
|
|
62
|
+
needsSetup = true;
|
|
63
|
+
} else {
|
|
64
|
+
console.log(c.gray(`(could not fetch balance: ${err.message})`));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (needsSetup) {
|
|
69
|
+
const ok = await runSetup();
|
|
70
|
+
if (!ok) {
|
|
71
|
+
console.log(c.gray("Aborting — no valid API key."));
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const me = await fetchBalance();
|
|
76
|
+
state.balance = me.balance;
|
|
77
|
+
state.plan = me.plan || "FREE";
|
|
78
|
+
} catch { /* tolerate */ }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
printBanner(state);
|
|
82
|
+
const updateMsg = await updatePromise;
|
|
83
|
+
if (updateMsg) console.log(updateMsg + "\n");
|
|
84
|
+
|
|
85
|
+
const inputHistory = [];
|
|
86
|
+
|
|
87
|
+
while (true) {
|
|
88
|
+
const raw = await promptBox({ history: inputHistory });
|
|
89
|
+
if (raw === EXIT_SIGNAL) { console.log(c.gray("bye.")); return; }
|
|
90
|
+
const line = (raw ?? "").trim();
|
|
91
|
+
if (!line) continue;
|
|
92
|
+
|
|
93
|
+
inputHistory.push(line);
|
|
94
|
+
|
|
95
|
+
if (line.startsWith("/") || line === "?") {
|
|
96
|
+
const handled = await handleSlash(line, state);
|
|
97
|
+
if (handled === "exit") return;
|
|
98
|
+
printStatusLine(state);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const result = await runAgent({
|
|
103
|
+
initialPrompt: line,
|
|
104
|
+
priorMessages: state.messages.length > 0 ? state.messages : undefined,
|
|
105
|
+
cwd: state.cwd,
|
|
106
|
+
autoYes: state.autoYes,
|
|
107
|
+
unsafePaths: state.unsafePaths,
|
|
108
|
+
model: state.model,
|
|
109
|
+
maxTurns: state.maxTurns,
|
|
110
|
+
mcpManager,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
state.sessionCredits += result.totalCredits ?? 0;
|
|
114
|
+
state.sessionIn += result.totalIn ?? 0;
|
|
115
|
+
state.sessionOut += result.totalOut ?? 0;
|
|
116
|
+
if (typeof result.balance === "number") state.balance = result.balance;
|
|
117
|
+
if (result.messages) state.messages = result.messages;
|
|
118
|
+
|
|
119
|
+
if (!result.ok && result.error) {
|
|
120
|
+
console.log("\n" + errorLine(result.error.message || String(result.error)));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
printStatusLine(state);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/* ───────── slash commands ───────── */
|
|
128
|
+
|
|
129
|
+
async function handleSlash(line, state) {
|
|
130
|
+
const [cmd, ...rest] = line.replace(/^\//, "").split(/\s+/);
|
|
131
|
+
const arg = rest.join(" ").trim();
|
|
132
|
+
|
|
133
|
+
switch ((cmd || "").toLowerCase()) {
|
|
134
|
+
case "":
|
|
135
|
+
case "help":
|
|
136
|
+
case "?":
|
|
137
|
+
console.log(SHORTCUTS);
|
|
138
|
+
break;
|
|
139
|
+
case "exit":
|
|
140
|
+
case "quit":
|
|
141
|
+
case "q":
|
|
142
|
+
console.log(c.gray("bye."));
|
|
143
|
+
return "exit";
|
|
144
|
+
case "clear":
|
|
145
|
+
state.messages = [];
|
|
146
|
+
state.sessionCredits = 0;
|
|
147
|
+
state.sessionIn = 0;
|
|
148
|
+
state.sessionOut = 0;
|
|
149
|
+
console.log(c.gray("Conversation cleared."));
|
|
150
|
+
break;
|
|
151
|
+
case "balance": {
|
|
152
|
+
try {
|
|
153
|
+
const me = await fetchBalance();
|
|
154
|
+
state.balance = me.balance;
|
|
155
|
+
state.plan = me.plan || "FREE";
|
|
156
|
+
console.log(
|
|
157
|
+
c.cyan(`balance: ${me.balance.toLocaleString()} credits`) +
|
|
158
|
+
c.gray(` · plan: ${me.plan || "FREE"} (${me.planCredits ?? 0} plan + ${me.topupCredits ?? 0} topup)`),
|
|
159
|
+
);
|
|
160
|
+
} catch (err) {
|
|
161
|
+
console.log(errorLine(err.message || String(err)));
|
|
162
|
+
}
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
case "cwd":
|
|
166
|
+
if (arg) {
|
|
167
|
+
const next = path.resolve(arg);
|
|
168
|
+
try {
|
|
169
|
+
process.chdir(next);
|
|
170
|
+
state.cwd = next;
|
|
171
|
+
console.log(c.gray(`cwd → ${state.cwd}`));
|
|
172
|
+
} catch (err) {
|
|
173
|
+
console.log(errorLine(err.message));
|
|
174
|
+
}
|
|
175
|
+
} else {
|
|
176
|
+
console.log(c.gray(`cwd: ${state.cwd}`));
|
|
177
|
+
}
|
|
178
|
+
break;
|
|
179
|
+
case "yes":
|
|
180
|
+
state.autoYes = !state.autoYes;
|
|
181
|
+
console.log(c.gray(`auto-yes: ${state.autoYes ? "on (writes/shells will skip y/N)" : "off"}`));
|
|
182
|
+
break;
|
|
183
|
+
case "turns": {
|
|
184
|
+
const n = parseInt(arg, 10);
|
|
185
|
+
if (!Number.isFinite(n) || n < 1 || n > 200) {
|
|
186
|
+
console.log(errorLine(`/turns expects 1..200, got "${arg}"`));
|
|
187
|
+
} else {
|
|
188
|
+
state.maxTurns = n;
|
|
189
|
+
console.log(c.gray(`max turns: ${state.maxTurns}`));
|
|
190
|
+
}
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
case "model": {
|
|
194
|
+
const key = (arg || "").trim().toLowerCase();
|
|
195
|
+
if (!key) {
|
|
196
|
+
console.log(c.gray(`model: ${state.modelName}`));
|
|
197
|
+
console.log(c.gray("switch with /model core | ultra (ultra=Opus — premium, need credits)"));
|
|
198
|
+
} else if (MODEL_ALIASES[key]) {
|
|
199
|
+
state.model = MODEL_ALIASES[key].id;
|
|
200
|
+
state.modelName = MODEL_ALIASES[key].name;
|
|
201
|
+
const premium = state.model !== null;
|
|
202
|
+
console.log(
|
|
203
|
+
c.gray(`model → ${c.cyan(state.modelName)}`) +
|
|
204
|
+
(premium ? c.gray(" (premium — needs purchased credits, else falls back to Core)") : ""),
|
|
205
|
+
);
|
|
206
|
+
} else {
|
|
207
|
+
console.log(errorLine(`Unknown model "${key}". Try: core, ultra`));
|
|
208
|
+
}
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
default:
|
|
212
|
+
console.log(errorLine(`Unknown command: /${cmd}. Type ${c.cyan("/help")} for shortcuts.`));
|
|
213
|
+
}
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/* ───────── banner + status ───────── */
|
|
218
|
+
|
|
219
|
+
const shortenPath = (p, max) => (p.length <= max ? p : "..." + p.slice(p.length - max + 3));
|
|
220
|
+
|
|
221
|
+
function printBanner(state) {
|
|
222
|
+
const credits = state.balance != null ? `${state.balance.toLocaleString()} credits` : "";
|
|
223
|
+
|
|
224
|
+
// Single clean banner box — Claude Code style
|
|
225
|
+
const lines = [
|
|
226
|
+
c.bold(c.cyan("✦ Aether Code")) + c.gray(" v" + VERSION),
|
|
227
|
+
"",
|
|
228
|
+
c.gray(" " + state.modelName + (credits ? " · " + credits : "")),
|
|
229
|
+
c.gray(" cwd: " + shortenPath(state.cwd, 52)),
|
|
230
|
+
"",
|
|
231
|
+
c.dim(" Ctrl+V paste · @file image · /help"),
|
|
232
|
+
];
|
|
233
|
+
|
|
234
|
+
const banner = boxLines(lines, { color: c.cyan, padX: 1, minInner: 50 });
|
|
235
|
+
console.log("");
|
|
236
|
+
console.log(banner.join("\n"));
|
|
237
|
+
console.log("");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function printStatusLine(state) {
|
|
241
|
+
const parts = [];
|
|
242
|
+
parts.push(c.gray(`session: ${state.sessionCredits} cr · ${state.sessionIn}→${state.sessionOut} tokens`));
|
|
243
|
+
if (state.balance != null) parts.push(c.gray(`balance: ${state.balance.toLocaleString()}`));
|
|
244
|
+
if (state.messages.length > 0) {
|
|
245
|
+
parts.push(c.gray(`history: ${state.messages.length} msg${state.messages.length === 1 ? "" : "s"}`));
|
|
246
|
+
}
|
|
247
|
+
parts.push(c.cyan(state.autoYes ? "auto-yes" : "review"));
|
|
248
|
+
console.log(c.dim(parts.join(" · ")));
|
|
249
|
+
}
|
package/src/update-check.js
CHANGED
|
@@ -1,60 +1,117 @@
|
|
|
1
|
-
// Startup update check
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
import { readFileSync } from "node:fs";
|
|
6
|
-
import { fileURLToPath } from "node:url";
|
|
7
|
-
import { dirname, join } from "node:path";
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
return null;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
1
|
+
// Startup update check + auto-update. On every REPL launch, checks the npm
|
|
2
|
+
// registry and silently installs the latest version in the background if one
|
|
3
|
+
// exists. Never throws, bounded by a short timeout so it can't slow startup.
|
|
4
|
+
|
|
5
|
+
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { dirname, join } from "node:path";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { spawn } from "node:child_process";
|
|
10
|
+
import { c } from "./render.js";
|
|
11
|
+
|
|
12
|
+
export function currentVersion() {
|
|
13
|
+
try {
|
|
14
|
+
const p = join(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
|
|
15
|
+
return JSON.parse(readFileSync(p, "utf8")).version || null;
|
|
16
|
+
} catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Compare two "x.y.z" strings. Returns >0 if a is newer than b.
|
|
22
|
+
export function cmpVersion(a, b) {
|
|
23
|
+
const pa = String(a).split(".").map(Number);
|
|
24
|
+
const pb = String(b).split(".").map(Number);
|
|
25
|
+
for (let i = 0; i < 3; i++) {
|
|
26
|
+
const d = (pa[i] || 0) - (pb[i] || 0);
|
|
27
|
+
if (d !== 0) return d;
|
|
28
|
+
}
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const COOLDOWN_FILE = join(tmpdir(), "aether-update-ts");
|
|
33
|
+
const COOLDOWN_MS = 3600_000; // 1 hour
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Check for updates. If a newer version exists on npm, spawn a background
|
|
37
|
+
* `npm install -g` to auto-install it. Returns a one-line status message
|
|
38
|
+
* or null. Cooldown: only checks once per hour.
|
|
39
|
+
*/
|
|
40
|
+
export async function autoUpdate({ fetchImpl = fetch, timeoutMs = 2000 } = {}) {
|
|
41
|
+
const current = currentVersion();
|
|
42
|
+
if (!current) return null;
|
|
43
|
+
|
|
44
|
+
// Skip if we checked less than 1 hour ago
|
|
45
|
+
try {
|
|
46
|
+
const ts = parseInt(readFileSync(COOLDOWN_FILE, "utf8"), 10);
|
|
47
|
+
if (Date.now() - ts < COOLDOWN_MS) return null;
|
|
48
|
+
} catch { /* no file or parse error — check now */ }
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const ac = new AbortController();
|
|
52
|
+
const t = setTimeout(() => ac.abort(), timeoutMs);
|
|
53
|
+
let res;
|
|
54
|
+
try {
|
|
55
|
+
res = await fetchImpl("https://registry.npmjs.org/aether-code/latest", { signal: ac.signal });
|
|
56
|
+
} finally {
|
|
57
|
+
clearTimeout(t);
|
|
58
|
+
}
|
|
59
|
+
if (!res || !res.ok) return null;
|
|
60
|
+
const latest = (await res.json())?.version;
|
|
61
|
+
|
|
62
|
+
// Write cooldown regardless of whether an update is available
|
|
63
|
+
try { writeFileSync(COOLDOWN_FILE, String(Date.now())); } catch { /* ignore */ }
|
|
64
|
+
|
|
65
|
+
if (!latest || cmpVersion(latest, current) <= 0) return null;
|
|
66
|
+
|
|
67
|
+
// Spawn background npm install — detached so it doesn't block the CLI.
|
|
68
|
+
// On Windows, `npm` is a cmd script, so shell:true is required.
|
|
69
|
+
const child = spawn("npm", ["install", "-g", `aether-code@${latest}`], {
|
|
70
|
+
detached: true,
|
|
71
|
+
stdio: "ignore",
|
|
72
|
+
shell: true,
|
|
73
|
+
windowsHide: true,
|
|
74
|
+
});
|
|
75
|
+
child.unref();
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
c.cyan("↑ auto-updating ") +
|
|
79
|
+
c.gray(current) + c.gray(" → ") + c.bold(c.green(latest)) +
|
|
80
|
+
c.gray(" (background — applies next launch)")
|
|
81
|
+
);
|
|
82
|
+
} catch {
|
|
83
|
+
/* offline / aborted / parse error — stay silent */
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Legacy nudge-only check (kept for the test suite). Same as before: returns
|
|
90
|
+
* a one-line "update available" string or null.
|
|
91
|
+
*/
|
|
92
|
+
export async function checkForUpdate({ fetchImpl = fetch, timeoutMs = 2000 } = {}) {
|
|
93
|
+
const current = currentVersion();
|
|
94
|
+
if (!current) return null;
|
|
95
|
+
try {
|
|
96
|
+
const ac = new AbortController();
|
|
97
|
+
const t = setTimeout(() => ac.abort(), timeoutMs);
|
|
98
|
+
let res;
|
|
99
|
+
try {
|
|
100
|
+
res = await fetchImpl("https://registry.npmjs.org/aether-code/latest", { signal: ac.signal });
|
|
101
|
+
} finally {
|
|
102
|
+
clearTimeout(t);
|
|
103
|
+
}
|
|
104
|
+
if (!res || !res.ok) return null;
|
|
105
|
+
const latest = (await res.json())?.version;
|
|
106
|
+
if (latest && cmpVersion(latest, current) > 0) {
|
|
107
|
+
return (
|
|
108
|
+
c.yellow("update available: ") +
|
|
109
|
+
c.gray(current) + " -> " + c.bold(c.green(latest)) +
|
|
110
|
+
c.gray(" · run ") + c.cyan("npm i -g aether-code@latest")
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
} catch {
|
|
114
|
+
/* offline / aborted / parse error — stay silent */
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|