aether-code 0.31.0 → 0.32.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/LICENSE +21 -21
- package/README.md +222 -220
- package/bin/aether-code.js +516 -516
- package/package.json +77 -75
- package/scripts/postinstall.js +182 -0
- package/src/agent.js +332 -332
- package/src/api.js +323 -323
- package/src/box-input.js +132 -132
- package/src/config.js +38 -38
- package/src/diff.js +49 -49
- package/src/ink-input.js +91 -91
- package/src/menu.js +83 -83
- package/src/render.js +221 -221
- package/src/repl.js +291 -293
- package/src/setup.js +139 -139
- package/src/skills.js +198 -198
- package/src/tools.js +911 -911
- package/src/update-check.js +60 -60
package/src/repl.js
CHANGED
|
@@ -1,293 +1,291 @@
|
|
|
1
|
-
// Interactive REPL — Claude-CLI-style. Launched when `aether-code` is run
|
|
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
|
|
10
|
-
|
|
11
|
-
import path from "node:path";
|
|
12
|
-
import { runAgent } from "./agent.js";
|
|
13
|
-
import { fetchBalance, AetherError } from "./api.js";
|
|
14
|
-
import { runSetup } from "./setup.js";
|
|
15
|
-
import { c, errorLine, boxLines, sideBySide } from "./render.js";
|
|
16
|
-
import { checkForUpdate } from "./update-check.js";
|
|
17
|
-
import { promptBox, EXIT_SIGNAL } from "./box-input.js";
|
|
18
|
-
|
|
19
|
-
const VERSION = "0.
|
|
20
|
-
const MODEL_NAME = "Aether Core";
|
|
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.
|
|
25
|
-
const MODEL_ALIASES = {
|
|
26
|
-
core: { id: null, name: "Aether Core" },
|
|
27
|
-
gemma: { id: null, name: "Aether Core" },
|
|
28
|
-
ultra: { id: "claude-opus-4-6", name: "Aether Ultra" },
|
|
29
|
-
opus: { id: "claude-opus-4-6", name: "Aether Ultra" },
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
${c.cyan("/
|
|
36
|
-
${c.cyan("/
|
|
37
|
-
${c.cyan("/
|
|
38
|
-
${c.cyan("/
|
|
39
|
-
${c.cyan("/
|
|
40
|
-
${c.cyan("/
|
|
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
|
-
const
|
|
110
|
-
if (
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
state.
|
|
138
|
-
state.
|
|
139
|
-
state.
|
|
140
|
-
|
|
141
|
-
if (result.
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
case "":
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
case "
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
state.
|
|
171
|
-
state.
|
|
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
|
-
console.log(
|
|
197
|
-
}
|
|
198
|
-
|
|
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
|
-
state.model
|
|
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
|
-
"
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
c.
|
|
261
|
-
c.gray(
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
);
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
console.log(
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
console.log(c.dim(parts.join(" · ")));
|
|
293
|
-
}
|
|
1
|
+
// Interactive REPL — Claude-CLI-style. Launched when `aether-code` is run
|
|
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
|
|
10
|
+
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { runAgent } from "./agent.js";
|
|
13
|
+
import { fetchBalance, AetherError } from "./api.js";
|
|
14
|
+
import { runSetup } from "./setup.js";
|
|
15
|
+
import { c, errorLine, boxLines, sideBySide } from "./render.js";
|
|
16
|
+
import { checkForUpdate } from "./update-check.js";
|
|
17
|
+
import { promptBox, EXIT_SIGNAL } from "./box-input.js";
|
|
18
|
+
|
|
19
|
+
const VERSION = "0.32.0";
|
|
20
|
+
const MODEL_NAME = "Aether Core";
|
|
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.
|
|
25
|
+
const MODEL_ALIASES = {
|
|
26
|
+
core: { id: null, name: "Aether Core" },
|
|
27
|
+
gemma: { id: null, name: "Aether Core" },
|
|
28
|
+
ultra: { id: "claude-opus-4-6", name: "Aether Ultra" },
|
|
29
|
+
opus: { id: "claude-opus-4-6", name: "Aether Ultra" },
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const SHORTCUTS = `
|
|
33
|
+
${c.cyan("/help")} Show this help
|
|
34
|
+
${c.cyan("/exit")} Exit (or Ctrl+C twice)
|
|
35
|
+
${c.cyan("/clear")} Clear conversation history (start fresh)
|
|
36
|
+
${c.cyan("/balance")} Refresh and show credit balance
|
|
37
|
+
${c.cyan("/cwd")} ${c.gray("[path]")} Show or change working directory
|
|
38
|
+
${c.cyan("/yes")} Toggle auto-approve mode (skip y/N prompts)
|
|
39
|
+
${c.cyan("/turns")} ${c.gray("<n>")} Set max turns per prompt (default 25)
|
|
40
|
+
${c.cyan("/model")} ${c.gray("[core|ultra]")} Show or switch model (ultra=Opus — premium)
|
|
41
|
+
|
|
42
|
+
${c.gray("Anything else is sent to the agent as your next message.")}
|
|
43
|
+
${c.gray("Conversation history is kept across messages until you /clear.")}
|
|
44
|
+
`;
|
|
45
|
+
|
|
46
|
+
export async function runRepl({ cwd: initialCwd, autoYes: initialAutoYes, unsafePaths: initialUnsafePaths, maxTurns: initialMaxTurns, model: initialModel = null, mcpManager = null }) {
|
|
47
|
+
const initialAlias = Object.values(MODEL_ALIASES).find((a) => a.id === initialModel);
|
|
48
|
+
const state = {
|
|
49
|
+
cwd: initialCwd,
|
|
50
|
+
autoYes: !!initialAutoYes,
|
|
51
|
+
unsafePaths: !!initialUnsafePaths,
|
|
52
|
+
maxTurns: initialMaxTurns ?? 25,
|
|
53
|
+
model: initialModel ?? null,
|
|
54
|
+
modelName: initialAlias?.name ?? MODEL_NAME,
|
|
55
|
+
messages: [], // accumulates across turns
|
|
56
|
+
balance: null,
|
|
57
|
+
sessionCredits: 0,
|
|
58
|
+
sessionIn: 0,
|
|
59
|
+
sessionOut: 0,
|
|
60
|
+
};
|
|
61
|
+
|
|
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);
|
|
65
|
+
|
|
66
|
+
// Free balance check up front. If no key configured, walk through first-time
|
|
67
|
+
// setup flow (open browser → paste key → verify → save).
|
|
68
|
+
let needsSetup = false;
|
|
69
|
+
try {
|
|
70
|
+
const me = await fetchBalance();
|
|
71
|
+
state.balance = me.balance;
|
|
72
|
+
state.plan = me.plan || "FREE";
|
|
73
|
+
} catch (err) {
|
|
74
|
+
if (err instanceof AetherError && (err.code === "NO_API_KEY" || err.status === 401)) {
|
|
75
|
+
needsSetup = true;
|
|
76
|
+
} else {
|
|
77
|
+
// Transient network or other — surface but don't block
|
|
78
|
+
console.log(c.gray(`(could not fetch balance: ${err.message})`));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (needsSetup) {
|
|
83
|
+
const ok = await runSetup();
|
|
84
|
+
if (!ok) {
|
|
85
|
+
console.log(c.gray("Aborting — no valid API key."));
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
// Re-fetch balance now that the key is saved
|
|
89
|
+
try {
|
|
90
|
+
const me = await fetchBalance();
|
|
91
|
+
state.balance = me.balance;
|
|
92
|
+
state.plan = me.plan || "FREE";
|
|
93
|
+
} catch { /* tolerate — banner just won't show balance */ }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
printBanner(state);
|
|
97
|
+
const updateNudge = await updatePromise;
|
|
98
|
+
if (updateNudge) console.log(updateNudge + "\n");
|
|
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.
|
|
104
|
+
const inputHistory = [];
|
|
105
|
+
|
|
106
|
+
while (true) {
|
|
107
|
+
const raw = await promptBox({ history: inputHistory });
|
|
108
|
+
if (raw === EXIT_SIGNAL) { console.log(c.gray("bye.")); return; }
|
|
109
|
+
const line = (raw ?? "").trim();
|
|
110
|
+
if (!line) continue;
|
|
111
|
+
|
|
112
|
+
// promptBox already echoes "› <line>" on submit, so no echo needed here.
|
|
113
|
+
inputHistory.push(line);
|
|
114
|
+
|
|
115
|
+
// Slash command?
|
|
116
|
+
if (line.startsWith("/") || line === "?") {
|
|
117
|
+
const handled = await handleSlash(line, state);
|
|
118
|
+
if (handled === "exit") return;
|
|
119
|
+
printStatusLine(state);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
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
|
+
});
|
|
134
|
+
|
|
135
|
+
state.sessionCredits += result.totalCredits ?? 0;
|
|
136
|
+
state.sessionIn += result.totalIn ?? 0;
|
|
137
|
+
state.sessionOut += result.totalOut ?? 0;
|
|
138
|
+
if (typeof result.balance === "number") state.balance = result.balance;
|
|
139
|
+
if (result.messages) state.messages = result.messages;
|
|
140
|
+
|
|
141
|
+
if (!result.ok && result.error) {
|
|
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.
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
printStatusLine(state);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/* ───────── slash commands ───────── */
|
|
151
|
+
|
|
152
|
+
async function handleSlash(line, state) {
|
|
153
|
+
const [cmd, ...rest] = line.replace(/^\//, "").split(/\s+/);
|
|
154
|
+
const arg = rest.join(" ").trim();
|
|
155
|
+
|
|
156
|
+
switch ((cmd || "").toLowerCase()) {
|
|
157
|
+
case "":
|
|
158
|
+
case "help":
|
|
159
|
+
case "?":
|
|
160
|
+
console.log(SHORTCUTS);
|
|
161
|
+
break;
|
|
162
|
+
case "exit":
|
|
163
|
+
case "quit":
|
|
164
|
+
case "q":
|
|
165
|
+
console.log(c.gray("bye."));
|
|
166
|
+
return "exit";
|
|
167
|
+
case "clear":
|
|
168
|
+
state.messages = [];
|
|
169
|
+
state.sessionCredits = 0;
|
|
170
|
+
state.sessionIn = 0;
|
|
171
|
+
state.sessionOut = 0;
|
|
172
|
+
console.log(c.gray("Conversation cleared."));
|
|
173
|
+
break;
|
|
174
|
+
case "balance": {
|
|
175
|
+
try {
|
|
176
|
+
const me = await fetchBalance();
|
|
177
|
+
state.balance = me.balance;
|
|
178
|
+
state.plan = me.plan || "FREE";
|
|
179
|
+
console.log(
|
|
180
|
+
c.cyan(`balance: ${me.balance.toLocaleString()} credits`) +
|
|
181
|
+
c.gray(` · plan: ${me.plan || "FREE"} (${me.planCredits ?? 0} plan + ${me.topupCredits ?? 0} topup)`),
|
|
182
|
+
);
|
|
183
|
+
} catch (err) {
|
|
184
|
+
console.log(errorLine(err.message || String(err)));
|
|
185
|
+
}
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
case "cwd":
|
|
189
|
+
if (arg) {
|
|
190
|
+
const next = path.resolve(arg);
|
|
191
|
+
try {
|
|
192
|
+
process.chdir(next);
|
|
193
|
+
state.cwd = next;
|
|
194
|
+
console.log(c.gray(`cwd → ${state.cwd}`));
|
|
195
|
+
} catch (err) {
|
|
196
|
+
console.log(errorLine(err.message));
|
|
197
|
+
}
|
|
198
|
+
} else {
|
|
199
|
+
console.log(c.gray(`cwd: ${state.cwd}`));
|
|
200
|
+
}
|
|
201
|
+
break;
|
|
202
|
+
case "yes":
|
|
203
|
+
state.autoYes = !state.autoYes;
|
|
204
|
+
console.log(c.gray(`auto-yes: ${state.autoYes ? "on (writes/shells will skip y/N)" : "off"}`));
|
|
205
|
+
break;
|
|
206
|
+
case "turns": {
|
|
207
|
+
const n = parseInt(arg, 10);
|
|
208
|
+
if (!Number.isFinite(n) || n < 1 || n > 200) {
|
|
209
|
+
console.log(errorLine(`/turns expects 1..200, got "${arg}"`));
|
|
210
|
+
} else {
|
|
211
|
+
state.maxTurns = n;
|
|
212
|
+
console.log(c.gray(`max turns: ${state.maxTurns}`));
|
|
213
|
+
}
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
case "model": {
|
|
217
|
+
const key = (arg || "").trim().toLowerCase();
|
|
218
|
+
if (!key) {
|
|
219
|
+
console.log(c.gray(`model: ${state.modelName}`));
|
|
220
|
+
console.log(c.gray("switch with /model core | ultra (ultra=Opus — premium, need credits)"));
|
|
221
|
+
} else if (MODEL_ALIASES[key]) {
|
|
222
|
+
state.model = MODEL_ALIASES[key].id;
|
|
223
|
+
state.modelName = MODEL_ALIASES[key].name;
|
|
224
|
+
const premium = state.model !== null;
|
|
225
|
+
console.log(
|
|
226
|
+
c.gray(`model → ${c.cyan(state.modelName)}`) +
|
|
227
|
+
(premium ? c.gray(" (premium — needs purchased credits, else falls back to Core)") : ""),
|
|
228
|
+
);
|
|
229
|
+
} else {
|
|
230
|
+
console.log(errorLine(`Unknown model "${key}". Try: core, ultra`));
|
|
231
|
+
}
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
default:
|
|
235
|
+
console.log(errorLine(`Unknown command: /${cmd}. Type ${c.cyan("/help")} for shortcuts.`));
|
|
236
|
+
}
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/* ───────── banner + status ───────── */
|
|
241
|
+
|
|
242
|
+
const visLen = (s) => s.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
243
|
+
const shortenPath = (p, max) => (p.length <= max ? p : "..." + p.slice(p.length - max + 3));
|
|
244
|
+
|
|
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
|
+
];
|
|
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
|
+
}
|