aether-code 0.32.2 → 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/agent.js +119 -17
- package/src/box-input.js +209 -132
- package/src/render.js +45 -9
- package/src/repl.js +249 -291
- package/src/update-check.js +117 -60
package/src/render.js
CHANGED
|
@@ -98,33 +98,69 @@ export function setTerminalTitle(title) {
|
|
|
98
98
|
if (process.stdout.isTTY) process.stdout.write(`\x1b]0;${title}\x07`);
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
// Live "working…" spinner
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
101
|
+
// Live "working…" spinner with optional live metrics (token count, status).
|
|
102
|
+
// Rewrites a single line in place (\r) with a rotating half-circle + elapsed
|
|
103
|
+
// time (mm:ss when > 60s) + any metrics fed via update(). stop() clears the
|
|
104
|
+
// line and restores the cursor. No-op in non-TTY.
|
|
105
105
|
export function startSpinner(label) {
|
|
106
|
-
if (!process.stdout.isTTY) return { stop: () => {} };
|
|
106
|
+
if (!process.stdout.isTTY) return { stop: () => {}, update: () => {} };
|
|
107
107
|
const frames = ["◐", "◓", "◑", "◒"];
|
|
108
108
|
const t0 = Date.now();
|
|
109
109
|
let i = 0;
|
|
110
|
+
let tokenCount = 0;
|
|
111
|
+
let statusText = "";
|
|
110
112
|
process.stdout.write("\x1b[?25l"); // hide cursor
|
|
111
113
|
const render = () => {
|
|
112
|
-
const
|
|
113
|
-
const
|
|
114
|
-
|
|
114
|
+
const ms = Date.now() - t0;
|
|
115
|
+
const secs = Math.floor(ms / 1000);
|
|
116
|
+
const timeFmt = secs >= 60
|
|
117
|
+
? `${Math.floor(secs / 60)}m ${String(secs % 60).padStart(2, "0")}s`
|
|
118
|
+
: `${secs}s`;
|
|
119
|
+
const parts = [];
|
|
120
|
+
if (secs > 0) parts.push(timeFmt);
|
|
121
|
+
if (tokenCount > 0) parts.push(`↓ ${fmtTokens(tokenCount)} tokens`);
|
|
122
|
+
if (statusText) parts.push(statusText);
|
|
123
|
+
const metrics = parts.length > 0 ? c.gray(` (${parts.join(" · ")})`) : "";
|
|
124
|
+
process.stdout.write(`\r\x1b[K${c.cyan(frames[i % frames.length])} ${c.gray(label + "…")}${metrics}`);
|
|
115
125
|
i++;
|
|
116
126
|
};
|
|
117
127
|
render();
|
|
118
128
|
const timer = setInterval(render, 120);
|
|
119
129
|
if (typeof timer.unref === "function") timer.unref(); // don't keep the process alive
|
|
120
130
|
return {
|
|
121
|
-
|
|
131
|
+
update(data) {
|
|
132
|
+
if (data.tokens != null) tokenCount = data.tokens;
|
|
133
|
+
if (data.status != null) statusText = data.status;
|
|
134
|
+
},
|
|
135
|
+
stop() {
|
|
122
136
|
clearInterval(timer);
|
|
123
137
|
process.stdout.write("\r\x1b[K\x1b[?25h"); // clear line + show cursor
|
|
124
138
|
},
|
|
125
139
|
};
|
|
126
140
|
}
|
|
127
141
|
|
|
142
|
+
// Human-friendly token count (14.2k, 1.3M, etc.)
|
|
143
|
+
export function fmtTokens(n) {
|
|
144
|
+
if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + "M";
|
|
145
|
+
if (n >= 1_000) return (n / 1_000).toFixed(1) + "k";
|
|
146
|
+
return String(n);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Compact one-line turn summary printed after each agent turn completes.
|
|
150
|
+
export function turnStats({ elapsed, tokensIn, tokensOut, tools, credits }) {
|
|
151
|
+
const parts = [];
|
|
152
|
+
if (elapsed > 0) {
|
|
153
|
+
const s = Math.floor(elapsed / 1000);
|
|
154
|
+
parts.push(s >= 60 ? `${Math.floor(s / 60)}m ${String(s % 60).padStart(2, "0")}s` : `${s}s`);
|
|
155
|
+
}
|
|
156
|
+
if (tokensIn > 0 || tokensOut > 0) {
|
|
157
|
+
parts.push(`${fmtTokens(tokensIn)}→${fmtTokens(tokensOut)} tokens`);
|
|
158
|
+
}
|
|
159
|
+
if (tools > 0) parts.push(`${tools} tool call${tools === 1 ? "" : "s"}`);
|
|
160
|
+
if (credits > 0) parts.push(`${credits} cr`);
|
|
161
|
+
return parts.length > 0 ? c.dim(` ── ${parts.join(" · ")}`) : "";
|
|
162
|
+
}
|
|
163
|
+
|
|
128
164
|
// Terse one-line result summary instead of dumping raw JSON / file contents.
|
|
129
165
|
// Tools whose handlers already render rich output (diffs, the shell stream, the
|
|
130
166
|
// plan) just get a check — the detail was already printed.
|
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
|
-
"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
|
-
}
|
|
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
|
+
}
|