aether-code 0.36.7 → 0.36.8
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/LICENSE +21 -21
- package/README.md +0 -2
- package/bin/aether-code.js +516 -516
- package/package.json +75 -77
- package/skills/adult-creative-writing.md +60 -60
- package/skills/debugging.md +51 -51
- package/skills/game-modding.md +73 -73
- package/skills/reverse-engineering.md +41 -41
- package/skills/scraping-automation.md +77 -77
- package/skills/security-research.md +67 -67
- package/src/agent.js +24 -159
- package/src/api.js +5 -2
- package/src/box-input.js +36 -140
- package/src/config.js +38 -38
- package/src/diff.js +49 -49
- package/src/ink-input.js +91 -91
- package/src/mcp-cli.js +94 -94
- package/src/mcp-registry.js +266 -266
- package/src/mcp.js +259 -259
- package/src/menu.js +83 -83
- package/src/render.js +69 -136
- package/src/repl.js +93 -207
- package/src/setup.js +141 -139
- package/src/tools.js +911 -911
- package/src/update-check.js +7 -64
- package/scripts/postinstall.js +0 -182
package/src/repl.js
CHANGED
|
@@ -1,17 +1,27 @@
|
|
|
1
1
|
// Interactive REPL — Claude-CLI-style. Launched when `aether-code` is run
|
|
2
2
|
// without a task argument.
|
|
3
|
+
//
|
|
4
|
+
// Behaviors:
|
|
5
|
+
// - Banner on startup (version, model, balance, cwd, mode)
|
|
6
|
+
// - Persistent message history across prompts within the session
|
|
7
|
+
// - Slash commands: /help, /exit, /clear, /balance, /cwd, /yes, /turns
|
|
8
|
+
// - Bottom status line is rendered after each turn
|
|
9
|
+
// - Ctrl+C: first press cancels in-progress turn, second press exits
|
|
3
10
|
|
|
4
11
|
import path from "node:path";
|
|
5
12
|
import { runAgent } from "./agent.js";
|
|
6
13
|
import { fetchBalance, AetherError } from "./api.js";
|
|
7
14
|
import { runSetup } from "./setup.js";
|
|
8
|
-
import { c, errorLine,
|
|
9
|
-
import {
|
|
15
|
+
import { c, errorLine, boxLines, sideBySide } from "./render.js";
|
|
16
|
+
import { checkForUpdate } from "./update-check.js";
|
|
10
17
|
import { promptBox, EXIT_SIGNAL } from "./box-input.js";
|
|
11
18
|
|
|
12
|
-
const VERSION = "0.
|
|
19
|
+
const VERSION = "0.32.0";
|
|
13
20
|
const MODEL_NAME = "Aether Core";
|
|
14
21
|
|
|
22
|
+
// /model aliases → { id, name }. id=null = server default (gemma / Aether Core).
|
|
23
|
+
// Premium models are gated SERVER-SIDE: if you haven't purchased credits, the
|
|
24
|
+
// server transparently uses Core instead — selecting one here can't bypass that.
|
|
15
25
|
const MODEL_ALIASES = {
|
|
16
26
|
core: { id: null, name: "Aether Core" },
|
|
17
27
|
gemma: { id: null, name: "Aether Core" },
|
|
@@ -33,39 +43,6 @@ ${c.gray("Anything else is sent to the agent as your next message.")}
|
|
|
33
43
|
${c.gray("Conversation history is kept across messages until you /clear.")}
|
|
34
44
|
`;
|
|
35
45
|
|
|
36
|
-
const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
37
|
-
|
|
38
|
-
function getVisibleRows() {
|
|
39
|
-
let rows = process.stdout.rows || 24;
|
|
40
|
-
if (process.platform === "win32" && !process.env.WT_SESSION && rows > 60) {
|
|
41
|
-
rows = 24;
|
|
42
|
-
}
|
|
43
|
-
return rows;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function pushPromptToBottom(contentLines) {
|
|
47
|
-
const visibleRows = getVisibleRows();
|
|
48
|
-
const promptHeight = 4;
|
|
49
|
-
const gap = Math.max(0, visibleRows - contentLines - promptHeight);
|
|
50
|
-
if (gap > 0) process.stdout.write("\n".repeat(gap));
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function drawStaticPrompt(startRow, state) {
|
|
54
|
-
const termCols = process.stdout.columns || 80;
|
|
55
|
-
const ruleStr = `\x1b[36m ${"─".repeat(Math.max(0, termCols - 2))}\x1b[0m`;
|
|
56
|
-
const modeStr = state.autoYes ? "auto-yes" : "review";
|
|
57
|
-
const blkFn = (code) => `\x1b[${code}m \x1b[0m`;
|
|
58
|
-
const statusBar = blkFn(46) + blkFn(44) +
|
|
59
|
-
`\x1b[2m ${modeStr} mode \xB7 ? for shortcuts \xB7 @file for images\x1b[0m`;
|
|
60
|
-
process.stdout.write(
|
|
61
|
-
`\x1b[${startRow};1H` +
|
|
62
|
-
ruleStr + "\n" +
|
|
63
|
-
`\x1b[2m> \x1b[0m` + "\n" +
|
|
64
|
-
ruleStr + "\n" +
|
|
65
|
-
statusBar,
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
46
|
export async function runRepl({ cwd: initialCwd, autoYes: initialAutoYes, unsafePaths: initialUnsafePaths, maxTurns: initialMaxTurns, model: initialModel = null, mcpManager = null }) {
|
|
70
47
|
const initialAlias = Object.values(MODEL_ALIASES).find((a) => a.id === initialModel);
|
|
71
48
|
const state = {
|
|
@@ -75,24 +52,29 @@ export async function runRepl({ cwd: initialCwd, autoYes: initialAutoYes, unsafe
|
|
|
75
52
|
maxTurns: initialMaxTurns ?? 25,
|
|
76
53
|
model: initialModel ?? null,
|
|
77
54
|
modelName: initialAlias?.name ?? MODEL_NAME,
|
|
78
|
-
messages: [],
|
|
55
|
+
messages: [], // accumulates across turns
|
|
79
56
|
balance: null,
|
|
80
57
|
sessionCredits: 0,
|
|
81
58
|
sessionIn: 0,
|
|
82
59
|
sessionOut: 0,
|
|
83
60
|
};
|
|
84
61
|
|
|
85
|
-
|
|
62
|
+
// Kick off the npm update check concurrently with the balance fetch so it
|
|
63
|
+
// adds no startup latency; the nudge (if any) prints just under the banner.
|
|
64
|
+
const updatePromise = checkForUpdate().catch(() => null);
|
|
86
65
|
|
|
66
|
+
// Free balance check up front. If no key configured, walk through first-time
|
|
67
|
+
// setup flow (open browser → paste key → verify → save).
|
|
87
68
|
let needsSetup = false;
|
|
88
69
|
try {
|
|
89
70
|
const me = await fetchBalance();
|
|
90
71
|
state.balance = me.balance;
|
|
91
|
-
state.plan = me.plan
|
|
72
|
+
state.plan = me.plan;
|
|
92
73
|
} catch (err) {
|
|
93
74
|
if (err instanceof AetherError && (err.code === "NO_API_KEY" || err.status === 401)) {
|
|
94
75
|
needsSetup = true;
|
|
95
76
|
} else {
|
|
77
|
+
// Transient network or other — surface but don't block
|
|
96
78
|
console.log(c.gray(`(could not fetch balance: ${err.message})`));
|
|
97
79
|
}
|
|
98
80
|
}
|
|
@@ -103,108 +85,52 @@ export async function runRepl({ cwd: initialCwd, autoYes: initialAutoYes, unsafe
|
|
|
103
85
|
console.log(c.gray("Aborting — no valid API key."));
|
|
104
86
|
process.exit(1);
|
|
105
87
|
}
|
|
88
|
+
// Re-fetch balance now that the key is saved
|
|
106
89
|
try {
|
|
107
90
|
const me = await fetchBalance();
|
|
108
91
|
state.balance = me.balance;
|
|
109
|
-
state.plan = me.plan
|
|
110
|
-
} catch { /* tolerate */ }
|
|
92
|
+
state.plan = me.plan;
|
|
93
|
+
} catch { /* tolerate — banner just won't show balance */ }
|
|
111
94
|
}
|
|
112
95
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const bannerLineCount = printBanner(state);
|
|
117
|
-
|
|
118
|
-
const updateMsg = await updatePromise;
|
|
119
|
-
let extraLines = 0;
|
|
120
|
-
if (updateMsg) { console.log(updateMsg); extraLines++; }
|
|
121
|
-
|
|
122
|
-
pushPromptToBottom(bannerLineCount + extraLines);
|
|
96
|
+
printBanner(state);
|
|
97
|
+
const updateNudge = await updatePromise;
|
|
98
|
+
if (updateNudge) console.log(updateNudge + "\n");
|
|
123
99
|
|
|
100
|
+
// Input: a bordered raw-mode box (box-input.js) — cmd.exe-safe and identical
|
|
101
|
+
// everywhere. (Ink's live redraw ghosts in the legacy Windows console, so we
|
|
102
|
+
// don't use it.) Each prompt is independent; the tool y/N prompts and the
|
|
103
|
+
// ask_user menu run BETWEEN prompts in raw mode too, so nothing overlaps.
|
|
124
104
|
const inputHistory = [];
|
|
125
105
|
|
|
126
|
-
const handlePromptResize = () => {
|
|
127
|
-
const vr = getVisibleRows();
|
|
128
|
-
process.stdout.write("\x1b[2J\x1b[H");
|
|
129
|
-
printBanner(state);
|
|
130
|
-
process.stdout.write(`\x1b[${Math.max(1, vr - 3)};1H`);
|
|
131
|
-
};
|
|
132
|
-
|
|
133
106
|
while (true) {
|
|
134
|
-
const raw = await promptBox({ history: inputHistory
|
|
107
|
+
const raw = await promptBox({ history: inputHistory });
|
|
135
108
|
if (raw === EXIT_SIGNAL) { console.log(c.gray("bye.")); return; }
|
|
136
109
|
const line = (raw ?? "").trim();
|
|
137
110
|
if (!line) continue;
|
|
138
111
|
|
|
112
|
+
// promptBox already echoes "› <line>" on submit, so no echo needed here.
|
|
139
113
|
inputHistory.push(line);
|
|
140
114
|
|
|
115
|
+
// Slash command?
|
|
141
116
|
if (line.startsWith("/") || line === "?") {
|
|
142
117
|
const handled = await handleSlash(line, state);
|
|
143
118
|
if (handled === "exit") return;
|
|
119
|
+
printStatusLine(state);
|
|
144
120
|
continue;
|
|
145
121
|
}
|
|
146
122
|
|
|
147
|
-
//
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
// Clear screen, print banner + echo, draw static prompt at bottom,
|
|
160
|
-
// then set scroll region so agent output stays above the prompt.
|
|
161
|
-
process.stdout.write(
|
|
162
|
-
"\x1b[r" +
|
|
163
|
-
"\x1b[2J\x1b[H" +
|
|
164
|
-
bLines.join("\n") + "\n" + echoBar + "\n",
|
|
165
|
-
);
|
|
166
|
-
drawStaticPrompt(promptRow, state);
|
|
167
|
-
|
|
168
|
-
const agentRow = Math.min(bLines.length + 2, scrollEnd);
|
|
169
|
-
process.stdout.write(`\x1b[1;${scrollEnd}r\x1b[${agentRow};1H`);
|
|
170
|
-
|
|
171
|
-
const onResize = () => {
|
|
172
|
-
const newVr = getVisibleRows();
|
|
173
|
-
const newScrollEnd = Math.max(4, newVr - 4);
|
|
174
|
-
const newPromptRow = newScrollEnd + 1;
|
|
175
|
-
process.stdout.write(
|
|
176
|
-
"\x1b7" +
|
|
177
|
-
"\x1b[r" +
|
|
178
|
-
`\x1b[${promptRow};1H\x1b[J`,
|
|
179
|
-
);
|
|
180
|
-
drawStaticPrompt(newPromptRow, state);
|
|
181
|
-
process.stdout.write(
|
|
182
|
-
`\x1b[1;${newScrollEnd}r` +
|
|
183
|
-
"\x1b8",
|
|
184
|
-
);
|
|
185
|
-
scrollEnd = newScrollEnd;
|
|
186
|
-
promptRow = newPromptRow;
|
|
187
|
-
};
|
|
188
|
-
process.stdout.on("resize", onResize);
|
|
189
|
-
|
|
190
|
-
const t0 = Date.now();
|
|
191
|
-
let result;
|
|
192
|
-
try {
|
|
193
|
-
result = await runAgent({
|
|
194
|
-
initialPrompt: line,
|
|
195
|
-
priorMessages: state.messages.length > 0 ? state.messages : undefined,
|
|
196
|
-
cwd: state.cwd,
|
|
197
|
-
autoYes: state.autoYes,
|
|
198
|
-
unsafePaths: state.unsafePaths,
|
|
199
|
-
model: state.model,
|
|
200
|
-
maxTurns: state.maxTurns,
|
|
201
|
-
mcpManager,
|
|
202
|
-
});
|
|
203
|
-
} finally {
|
|
204
|
-
process.stdout.removeListener("resize", onResize);
|
|
205
|
-
process.stdout.write("\x1b[r");
|
|
206
|
-
}
|
|
207
|
-
const elapsed = Date.now() - t0;
|
|
123
|
+
// Otherwise — send as a message to the agent
|
|
124
|
+
const result = await runAgent({
|
|
125
|
+
initialPrompt: line,
|
|
126
|
+
priorMessages: state.messages.length > 0 ? state.messages : undefined,
|
|
127
|
+
cwd: state.cwd,
|
|
128
|
+
autoYes: state.autoYes,
|
|
129
|
+
unsafePaths: state.unsafePaths,
|
|
130
|
+
model: state.model,
|
|
131
|
+
maxTurns: state.maxTurns,
|
|
132
|
+
mcpManager,
|
|
133
|
+
});
|
|
208
134
|
|
|
209
135
|
state.sessionCredits += result.totalCredits ?? 0;
|
|
210
136
|
state.sessionIn += result.totalIn ?? 0;
|
|
@@ -214,20 +140,10 @@ export async function runRepl({ cwd: initialCwd, autoYes: initialAutoYes, unsafe
|
|
|
214
140
|
|
|
215
141
|
if (!result.ok && result.error) {
|
|
216
142
|
console.log("\n" + errorLine(result.error.message || String(result.error)));
|
|
143
|
+
// Don't kill the session on a single error — surface it and continue.
|
|
217
144
|
}
|
|
218
145
|
|
|
219
|
-
|
|
220
|
-
elapsed,
|
|
221
|
-
tokensIn: 0,
|
|
222
|
-
tokensOut: 0,
|
|
223
|
-
tools: 0,
|
|
224
|
-
credits: result.totalCredits ?? 0,
|
|
225
|
-
});
|
|
226
|
-
if (stats) console.log(stats);
|
|
227
|
-
console.log("");
|
|
228
|
-
|
|
229
|
-
// Position cursor at prompt area for the interactive prompt
|
|
230
|
-
process.stdout.write(`\x1b[${promptRow};1H\x1b[J`);
|
|
146
|
+
printStatusLine(state);
|
|
231
147
|
}
|
|
232
148
|
}
|
|
233
149
|
|
|
@@ -259,10 +175,10 @@ async function handleSlash(line, state) {
|
|
|
259
175
|
try {
|
|
260
176
|
const me = await fetchBalance();
|
|
261
177
|
state.balance = me.balance;
|
|
262
|
-
state.plan = me.plan
|
|
178
|
+
state.plan = me.plan;
|
|
263
179
|
console.log(
|
|
264
180
|
c.cyan(`balance: ${me.balance.toLocaleString()} credits`) +
|
|
265
|
-
c.gray(`
|
|
181
|
+
c.gray(` · plan: ${me.plan} (${me.planCredits} plan + ${me.topupCredits} topup)`),
|
|
266
182
|
);
|
|
267
183
|
} catch (err) {
|
|
268
184
|
console.log(errorLine(err.message || String(err)));
|
|
@@ -275,7 +191,7 @@ async function handleSlash(line, state) {
|
|
|
275
191
|
try {
|
|
276
192
|
process.chdir(next);
|
|
277
193
|
state.cwd = next;
|
|
278
|
-
console.log(c.gray(`cwd
|
|
194
|
+
console.log(c.gray(`cwd → ${state.cwd}`));
|
|
279
195
|
} catch (err) {
|
|
280
196
|
console.log(errorLine(err.message));
|
|
281
197
|
}
|
|
@@ -307,7 +223,7 @@ async function handleSlash(line, state) {
|
|
|
307
223
|
state.modelName = MODEL_ALIASES[key].name;
|
|
308
224
|
const premium = state.model !== null;
|
|
309
225
|
console.log(
|
|
310
|
-
c.gray(`model
|
|
226
|
+
c.gray(`model → ${c.cyan(state.modelName)}`) +
|
|
311
227
|
(premium ? c.gray(" (premium — needs purchased credits, else falls back to Core)") : ""),
|
|
312
228
|
);
|
|
313
229
|
} else {
|
|
@@ -321,85 +237,55 @@ async function handleSlash(line, state) {
|
|
|
321
237
|
return null;
|
|
322
238
|
}
|
|
323
239
|
|
|
324
|
-
/* ───────── banner
|
|
240
|
+
/* ───────── banner + status ───────── */
|
|
325
241
|
|
|
242
|
+
const visLen = (s) => s.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
326
243
|
const shortenPath = (p, max) => (p.length <= max ? p : "..." + p.slice(p.length - max + 3));
|
|
327
244
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
const visible = stripAnsi(str).length;
|
|
336
|
-
if (visible >= width) return str;
|
|
337
|
-
const left = Math.floor((width - visible) / 2);
|
|
338
|
-
return " ".repeat(left) + str;
|
|
339
|
-
}
|
|
245
|
+
// Short, hand-curated highlights shown in the "What's new" box. Update with
|
|
246
|
+
// notable releases — keep it to ~3 lines so it balances the banner height.
|
|
247
|
+
const WHATS_NEW = [
|
|
248
|
+
"Files now save to your Desktop",
|
|
249
|
+
"Live thinking spinner + loop guard",
|
|
250
|
+
"Asks you when a task is unclear",
|
|
251
|
+
];
|
|
340
252
|
|
|
341
|
-
function
|
|
342
|
-
const
|
|
253
|
+
function printBanner(state) {
|
|
254
|
+
const mode = state.autoYes ? (state.unsafePaths ? "skip-permissions" : "auto-yes") : "review mode";
|
|
343
255
|
const credits = state.balance != null ? `${state.balance.toLocaleString()} credits` : "";
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
const twoCol = cols >= 70;
|
|
358
|
-
|
|
359
|
-
const titleText = " Aether Code v" + VERSION + " ";
|
|
360
|
-
const titleRuleRight = Math.max(0, ruleW - 3 - titleText.length);
|
|
361
|
-
const topRule = c.cyan("──" + titleText + "─".repeat(titleRuleRight));
|
|
362
|
-
|
|
363
|
-
const VL = c.cyan("│");
|
|
364
|
-
const lines = [];
|
|
365
|
-
|
|
366
|
-
lines.push("");
|
|
367
|
-
lines.push(" " + topRule);
|
|
368
|
-
|
|
369
|
-
const bottomRule = c.cyan(" " + "─".repeat(ruleW));
|
|
370
|
-
|
|
371
|
-
if (twoCol) {
|
|
372
|
-
const row = (left, right) => {
|
|
373
|
-
lines.push(" " + padRight(left, leftW) + " " + VL + " " + right);
|
|
374
|
-
};
|
|
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
|
+
);
|
|
375
269
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
row("", "");
|
|
383
|
-
row(centerIn(modelLine, leftW), "");
|
|
384
|
-
row(centerIn(c.dim(shortenPath(state.cwd, 50)), leftW), "");
|
|
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));
|
|
385
276
|
} else {
|
|
386
|
-
|
|
387
|
-
lines.push(centerIn(c.bold("Welcome back!"), cols - 4));
|
|
388
|
-
lines.push("");
|
|
389
|
-
lines.push(centerIn(`${C}${B} ${C}`, cols - 4));
|
|
390
|
-
lines.push(centerIn(`${B}${C} ${B}`, cols - 4));
|
|
391
|
-
lines.push(centerIn(`${C}${B} ${C}`, cols - 4));
|
|
392
|
-
lines.push("");
|
|
393
|
-
lines.push(centerIn(modelLine, cols - 4));
|
|
394
|
-
lines.push(centerIn(c.dim(shortenPath(state.cwd, 50)), cols - 4));
|
|
277
|
+
console.log(left.join("\n"));
|
|
395
278
|
}
|
|
396
|
-
|
|
397
|
-
lines.push(bottomRule);
|
|
398
|
-
return lines;
|
|
279
|
+
console.log("");
|
|
399
280
|
}
|
|
400
281
|
|
|
401
|
-
function
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
|
|
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(" · ")));
|
|
405
291
|
}
|