aether-code 0.6.1 → 0.8.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.
@@ -1,379 +1,228 @@
1
- #!/usr/bin/env node
2
- // aether-code — uncensored AI coding agent.
3
- //
4
- // Examples:
5
- // aether-code "build me a TypeScript todo CLI in this folder"
6
- // aether-code --yes "add JSDoc to every exported function in src/"
7
- // aether-code --cwd ./my-project "fix the failing tests"
8
- // aether-code --max-turns 40 "refactor the auth module to use bcrypt"
9
-
10
- import process from "node:process";
11
- import path from "node:path";
12
- import { runAgent } from "../src/agent.js";
13
- import { runRepl } from "../src/repl.js";
14
- import { runSetup } from "../src/setup.js";
15
- import { fetchBalance, AetherError } from "../src/api.js";
16
- import { writeConfigFile, getConfig, CONFIG_PATH } from "../src/config.js";
17
- import { generateAndApprovePlan } from "../src/plan.js";
18
- import { listSessions, loadSession, loadMostRecent, deleteSession, truncatePreview, sessionsDir } from "../src/sessions.js";
19
- import { c, errorLine, divider } from "../src/render.js";
20
-
21
- const VERSION = "0.6.1";
22
-
23
- const HELP = `${c.bold("aether")} — uncensored AI coding agent
24
-
25
- ${c.bold("USAGE")}
26
- aether Launch interactive REPL (Claude-CLI-style)
27
- aether [flags] "<task>" Run agent once on a single task
28
- aether <subcommand> [args] Run a utility subcommand
29
-
30
- ${c.bold("SUBCOMMANDS")}
31
- ${c.cyan("login")} Open browser, paste API key, save
32
- ${c.cyan("logout")} Clear saved API key
33
- ${c.cyan("balance")} Show plan + credit balance
34
- ${c.cyan("config")} show|set|set-base|path Manage config file
35
- ${c.cyan("sessions")} [list|show|delete] [id] List, show, or delete saved sessions
36
-
37
- ${c.bold("EXAMPLES")}
38
- aether # interactive REPL
39
- aether login # first-time setup
40
- aether balance # quick credit check
41
- aether "build a TypeScript todo CLI in this folder"
42
- aether --yes "add JSDoc to every exported function"
43
- aether --cwd ./my-project "fix the failing tests"
44
-
45
- ${c.bold("FLAGS")}
46
- --plan Show a numbered plan first; approve / refine / cancel before any tools fire.
47
- --resume [id] Continue a prior conversation. No id = most recent.
48
- --no-save Don't persist this session to ~/.aether/sessions/.
49
- --yes Auto-approve all writes and shell commands. Use with care.
50
- --cwd <path> Working directory for the agent (default: current dir).
51
- --max-turns <n> Maximum turns before stopping (default: 25).
52
- --unsafe-paths Allow the agent to read/write outside cwd.
53
- --help, -h Show this help.
54
- --version, -v Print version.
55
-
56
- ${c.bold("CONFIG")}
57
- Same config as aether-cli uses ${c.cyan("AETHER_API_KEY")} or ${c.cyan("~/.aetherrc")}.
58
- Get a key at ${c.blue("https://trynoguard.com/account")}.
59
-
60
- ${c.bold("SAFETY")}
61
- - File writes show a unified diff and require y/N confirmation by default.
62
- - Shell commands show what's about to run and require y/N confirmation.
63
- - Paths are clamped to ${c.cyan("--cwd")} (override with ${c.cyan("--unsafe-paths")}).
64
- - Each shell command has a 2-minute hard timeout.
65
-
66
- ${c.gray(`v${VERSION}`)}
67
- `;
68
-
69
- function parseArgs(argv) {
70
- const args = { _: [], flags: {} };
71
- for (let i = 0; i < argv.length; i++) {
72
- const a = argv[i];
73
- if (a === "--yes") { args.flags.yes = true; }
74
- else if (a === "--plan") { args.flags.plan = true; }
75
- else if (a === "--no-save") { args.flags.noSave = true; }
76
- else if (a === "--resume") {
77
- // --resume optionally takes a session id. If next arg starts with "--"
78
- // or doesn't exist, default to "latest".
79
- const nxt = argv[i + 1];
80
- if (!nxt || nxt.startsWith("--")) {
81
- args.flags.resume = "latest";
82
- } else {
83
- args.flags.resume = nxt;
84
- i++;
85
- }
86
- }
87
- else if (a === "--unsafe-paths") { args.flags.unsafePaths = true; }
88
- else if (a === "--help" || a === "-h") { args.flags.help = true; }
89
- else if (a === "--version" || a === "-v") { args.flags.version = true; }
90
- else if (a === "--cwd") { args.flags.cwd = argv[++i]; }
91
- else if (a === "--max-turns") { args.flags.maxTurns = parseInt(argv[++i], 10); }
92
- else if (a.startsWith("--")) {
93
- const eq = a.indexOf("=");
94
- if (eq >= 0) args.flags[a.slice(2, eq)] = a.slice(eq + 1);
95
- else args.flags[a.slice(2)] = true;
96
- } else {
97
- args._.push(a);
98
- }
99
- }
100
- return args;
101
- }
102
-
103
- function die(msg, code = 1) {
104
- process.stderr.write(errorLine(msg) + "\n");
105
- process.exit(code);
106
- }
107
-
108
- async function main() {
109
- const args = parseArgs(process.argv.slice(2));
110
-
111
- if (args.flags.help) {
112
- process.stdout.write(HELP);
113
- return;
114
- }
115
- if (args.flags.version) {
116
- process.stdout.write(`aether-code ${VERSION}\n`);
117
- return;
118
- }
119
-
120
- const cwd = args.flags.cwd ? path.resolve(args.flags.cwd) : process.cwd();
121
- const autoYes = !!args.flags.yes;
122
- const unsafePaths = !!args.flags.unsafePaths;
123
- const maxTurns = Number.isInteger(args.flags.maxTurns) ? args.flags.maxTurns : 25;
124
-
125
- // Subcommand routing — these shadow the "task as positional arg" mode
126
- const sub = args._[0]?.toLowerCase();
127
- if (sub === "login" || sub === "auth") {
128
- const ok = await runSetup();
129
- process.exit(ok ? 0 : 1);
130
- }
131
- if (sub === "logout") {
132
- writeConfigFile({ apiKey: "" });
133
- console.log(c.gray(`Cleared API key from ${CONFIG_PATH}.`));
134
- return;
135
- }
136
- if (sub === "config") {
137
- await handleConfig(args._.slice(1));
138
- return;
139
- }
140
- if (sub === "balance") {
141
- await handleBalance();
142
- return;
143
- }
144
- if (sub === "sessions") {
145
- await handleSessions(args._.slice(1));
146
- return;
147
- }
148
-
149
- const prompt = args._.join(" ").trim();
150
-
151
- // Resume from a prior session if --resume was passed
152
- let resumed = null;
153
- if (args.flags.resume) {
154
- resumed =
155
- args.flags.resume === "latest"
156
- ? loadMostRecent()
157
- : loadSession(args.flags.resume);
158
- if (!resumed) {
159
- die(`No session found for '${args.flags.resume}'. Run 'aether sessions' to see what's available.`);
160
- }
161
- }
162
-
163
- // No task AND not resuming → drop into interactive REPL.
164
- // No task BUT resuming → drop into REPL with the prior history loaded.
165
- if (!prompt) {
166
- if (cwd !== process.cwd()) process.chdir(cwd);
167
- await runRepl({ cwd, autoYes, maxTurns, resumed });
168
- return;
169
- }
170
-
171
- // One-shot mode also needs an API key. If missing, run setup before the task.
172
- const cfg = getConfig();
173
- if (!cfg.apiKey) {
174
- const ok = await runSetup();
175
- if (!ok) process.exit(1);
176
- }
177
-
178
- console.log(divider());
179
- console.log(c.magenta(c.bold("aether-code")) + c.gray(` · cwd ${cwd}${autoYes ? " · auto-yes" : ""}${unsafePaths ? " · unsafe-paths" : ""}${args.flags.plan ? " · plan-mode" : ""}${resumed ? ` · resumed ${resumed.id}` : ""}`));
180
- console.log(c.gray(`task: `) + prompt);
181
- console.log(divider());
182
-
183
- // Plan mode: generate plan first, get approval, then execute with the plan
184
- // injected into the conversation as context.
185
- let priorMessages = resumed ? resumed.messages : undefined;
186
- if (args.flags.plan) {
187
- let currentTask = prompt;
188
- // Up to 3 plan/refine cycles before bailing
189
- for (let attempt = 0; attempt < 3; attempt++) {
190
- const planResult = await generateAndApprovePlan({ initialPrompt: currentTask, cwd });
191
- if (planResult.cancelled) {
192
- console.log(c.gray("\nCancelled. No tools were run."));
193
- return;
194
- }
195
- if (planResult.approved) {
196
- // Inject the plan as prior context so the agent remembers it as it executes
197
- priorMessages = [
198
- { role: "user", content: currentTask },
199
- { role: "assistant", content: `Plan:\n${planResult.planText}` },
200
- ];
201
- break;
202
- }
203
- if (planResult.refinement) {
204
- console.log(c.gray(`Refining: ${planResult.refinement}`));
205
- currentTask = `${currentTask}\n\nRefinement: ${planResult.refinement}`;
206
- }
207
- }
208
- if (!priorMessages) {
209
- console.log(c.yellow("\nGave up after 3 refinement cycles. Try a clearer task."));
210
- return;
211
- }
212
- console.log("");
213
- }
214
-
215
- const result = await runAgent({
216
- initialPrompt: prompt,
217
- priorMessages,
218
- cwd,
219
- autoYes,
220
- unsafePaths,
221
- maxTurns,
222
- sessionId: resumed?.id, // reuse the prior id when resuming
223
- sessionCreatedAt: resumed?.createdAt,
224
- saveSessions: !args.flags.noSave,
225
- priorTotalCredits: resumed?.totalCredits ?? 0,
226
- priorTotalIn: resumed?.totalIn ?? 0,
227
- priorTotalOut: resumed?.totalOut ?? 0,
228
- });
229
-
230
- console.log("\n" + divider());
231
- if (result.ok) {
232
- console.log(c.green(c.bold("✓ Done")) + c.gray(` ${result.turns} turn${result.turns === 1 ? "" : "s"} · ${result.totalCredits} credits · ${result.totalIn}→${result.totalOut} tokens`));
233
- if (typeof result.balance === "number") {
234
- console.log(c.gray(` balance: ${result.balance.toLocaleString()} credits`));
235
- }
236
- } else {
237
- console.log(c.red(c.bold("✗ Stopped")) + c.gray(` ${result.totalCredits} credits used · ${result.totalIn}→${result.totalOut} tokens`));
238
- if (result.error) console.log(errorLine(result.error.message));
239
- }
240
- if (result.sessionId) {
241
- console.log(c.gray(` session: ${result.sessionId} — resume with `) + c.cyan(`aether --resume ${result.sessionId}`));
242
- }
243
- console.log(divider());
244
- }
245
-
246
- async function handleConfig(rest) {
247
- const sub = (rest[0] || "").toLowerCase();
248
- if (sub === "show" || !sub) {
249
- const cfg = getConfig();
250
- console.log(`Config file: ${cfg.configPath}`);
251
- console.log(`API key: ${cfg.apiKey ? cfg.apiKey.slice(0, 12) + "…" + cfg.apiKey.slice(-4) : c.gray("(none)")}`);
252
- console.log(`Base URL: ${cfg.baseUrl}`);
253
- console.log(`Source: ${process.env.AETHER_API_KEY ? "AETHER_API_KEY env" : "config file"}`);
254
- return;
255
- }
256
- if (sub === "set") {
257
- const key = rest[1];
258
- if (!key) die("config set: missing API key argument.");
259
- if (!key.startsWith("ak_live_")) {
260
- process.stderr.write(c.yellow("warning: keys normally start with ak_live_; saving anyway.\n"));
261
- }
262
- writeConfigFile({ apiKey: key });
263
- console.log(`${c.green("✓")} API key saved to ${CONFIG_PATH}`);
264
- return;
265
- }
266
- if (sub === "set-base") {
267
- const url = rest[1];
268
- if (!url) die("config set-base: missing URL argument.");
269
- writeConfigFile({ baseUrl: url });
270
- console.log(`${c.green("✓")} Base URL saved.`);
271
- return;
272
- }
273
- if (sub === "path") {
274
- console.log(CONFIG_PATH);
275
- return;
276
- }
277
- die(`config: unknown subcommand "${sub}". Try 'config show', 'config set <key>', 'config path'.`);
278
- }
279
-
280
- async function handleBalance() {
281
- try {
282
- const me = await fetchBalance();
283
- console.log(c.bold(c.magenta("Aether")));
284
- console.log(c.gray("─".repeat(50)));
285
- console.log(`Plan ${c.cyan(me.plan)}${me.role !== "USER" ? c.gray(` · ${me.role}`) : ""}`);
286
- console.log(`Balance ${c.bold(me.balance.toLocaleString())} credits`);
287
- console.log(` plan ${me.planCredits.toLocaleString()}`);
288
- console.log(` topup ${me.topupCredits.toLocaleString()}`);
289
- if (me.rate) {
290
- console.log(`Rate ${me.rate.used}/${me.rate.limit} this hour${me.rate.resetIn ? ` · resets in ${me.rate.resetIn}s` : ""}`);
291
- }
292
- if (me.isSuspended) console.log(c.red("\n⚠ Account is suspended."));
293
- } catch (err) {
294
- if (err instanceof AetherError && err.code === "NO_API_KEY") {
295
- console.log(errorLine("No API key. Run `aether login` first."));
296
- } else {
297
- die(err.message || String(err));
298
- }
299
- }
300
- }
301
-
302
- async function handleSessions(rest) {
303
- const sub = (rest[0] || "list").toLowerCase();
304
-
305
- if (sub === "list" || sub === "ls") {
306
- const sessions = listSessions(50);
307
- if (sessions.length === 0) {
308
- console.log(c.gray("No sessions saved yet. They appear here after you run aether on any task."));
309
- console.log(c.gray(`Sessions live in: ${sessionsDir()}`));
310
- return;
311
- }
312
- console.log(c.bold(c.magenta("Recent sessions")) + c.gray(` (${sessions.length})`));
313
- console.log(c.gray("─".repeat(72)));
314
- for (const s of sessions) {
315
- const when = new Date(s.lastUsedAt).toLocaleString("en-US", {
316
- month: "short", day: "numeric", hour: "numeric", minute: "2-digit",
317
- });
318
- const credits = s.totalCredits ? c.gray(` · ${s.totalCredits} cr`) : "";
319
- console.log(c.cyan(s.id) + c.gray(` · ${when}`) + credits);
320
- console.log(c.gray(" ") + truncatePreview(s.firstUserMessage, 64));
321
- console.log(c.dim(` in ${truncatePreview(s.cwd, 64)}`));
322
- console.log("");
323
- }
324
- console.log(c.gray(`Resume with `) + c.cyan(`aether --resume <id>`) + c.gray(` (or just `) + c.cyan(`aether --resume`) + c.gray(` for the most recent)`));
325
- return;
326
- }
327
-
328
- if (sub === "show") {
329
- const id = rest[1];
330
- if (!id) die("sessions show: missing session id");
331
- const s = loadSession(id);
332
- if (!s) die(`No session found for '${id}'`);
333
- console.log(c.bold(c.magenta(`Session ${s.id}`)));
334
- console.log(c.gray(`Created: ${s.createdAt}`));
335
- console.log(c.gray(`Last used: ${s.lastUsedAt}`));
336
- console.log(c.gray(`Cwd: ${s.cwd}`));
337
- console.log(c.gray(`Credits: ${s.totalCredits ?? 0}, Messages: ${s.messages?.length ?? 0}`));
338
- console.log(c.gray("─".repeat(72)));
339
- for (const m of s.messages || []) {
340
- const label =
341
- m.role === "user" ? c.cyan("user")
342
- : m.role === "assistant" ? c.magenta("assistant")
343
- : m.role === "tool" ? c.green("tool")
344
- : c.gray(m.role);
345
- const text = typeof m.content === "string" ? m.content : JSON.stringify(m.content);
346
- console.log(`${label}: ${truncatePreview(text, 200)}`);
347
- if (m.tool_calls?.length) {
348
- for (const tc of m.tool_calls) {
349
- console.log(c.gray(` ↪ ${tc.function?.name}(${tc.function?.arguments?.slice(0, 80) ?? ""})`));
350
- }
351
- }
352
- }
353
- return;
354
- }
355
-
356
- if (sub === "delete" || sub === "rm") {
357
- const id = rest[1];
358
- if (!id) die("sessions delete: missing session id");
359
- if (deleteSession(id)) {
360
- console.log(`${c.green("✓")} Deleted session ${id}`);
361
- } else {
362
- die(`No session found for '${id}'`);
363
- }
364
- return;
365
- }
366
-
367
- if (sub === "dir" || sub === "path") {
368
- console.log(sessionsDir());
369
- return;
370
- }
371
-
372
- die(`sessions: unknown subcommand '${sub}'. Try: list, show <id>, delete <id>, dir`);
373
- }
374
-
375
- main().catch((err) => {
376
- console.error(errorLine(err.message || String(err)));
377
- if (process.env.DEBUG) console.error(err);
378
- process.exit(1);
379
- });
1
+ #!/usr/bin/env node
2
+ // aether-code — uncensored AI coding agent.
3
+ //
4
+ // Examples:
5
+ // aether-code "build me a TypeScript todo CLI in this folder"
6
+ // aether-code --yes "add JSDoc to every exported function in src/"
7
+ // aether-code --cwd ./my-project "fix the failing tests"
8
+ // aether-code --max-turns 40 "refactor the auth module to use bcrypt"
9
+
10
+ import process from "node:process";
11
+ import path from "node:path";
12
+ import { runAgent } from "../src/agent.js";
13
+ import { runRepl } from "../src/repl.js";
14
+ import { runSetup } from "../src/setup.js";
15
+ import { fetchBalance, AetherError } from "../src/api.js";
16
+ import { writeConfigFile, getConfig, CONFIG_PATH } from "../src/config.js";
17
+ import { c, errorLine, divider } from "../src/render.js";
18
+
19
+ const VERSION = "0.3.0";
20
+
21
+ const HELP = `${c.bold("aether")} — uncensored AI coding agent
22
+
23
+ ${c.bold("USAGE")}
24
+ aether Launch interactive REPL (Claude-CLI-style)
25
+ aether [flags] "<task>" Run agent once on a single task
26
+ aether <subcommand> [args] Run a utility subcommand
27
+
28
+ ${c.bold("SUBCOMMANDS")}
29
+ ${c.cyan("login")} Open browser, paste API key, save
30
+ ${c.cyan("logout")} Clear saved API key
31
+ ${c.cyan("balance")} Show plan + credit balance
32
+ ${c.cyan("config")} show|set|set-base|path Manage config file
33
+
34
+ ${c.bold("EXAMPLES")}
35
+ aether # interactive REPL
36
+ aether login # first-time setup
37
+ aether balance # quick credit check
38
+ aether "build a TypeScript todo CLI in this folder"
39
+ aether --yes "add JSDoc to every exported function"
40
+ aether --cwd ./my-project "fix the failing tests"
41
+
42
+ ${c.bold("FLAGS")}
43
+ --yes Auto-approve all writes and shell commands. Use with care.
44
+ --cwd <path> Working directory for the agent (default: current dir).
45
+ --max-turns <n> Maximum turns before stopping (default: 25).
46
+ --unsafe-paths Allow the agent to read/write outside cwd.
47
+ --help, -h Show this help.
48
+ --version, -v Print version.
49
+
50
+ ${c.bold("CONFIG")}
51
+ Same config as aether-cli uses ${c.cyan("AETHER_API_KEY")} or ${c.cyan("~/.aetherrc")}.
52
+ Get a key at ${c.blue("https://trynoguard.com/account")}.
53
+
54
+ ${c.bold("SAFETY")}
55
+ - File writes show a unified diff and require y/N confirmation by default.
56
+ - Shell commands show what's about to run and require y/N confirmation.
57
+ - Paths are clamped to ${c.cyan("--cwd")} (override with ${c.cyan("--unsafe-paths")}).
58
+ - Each shell command has a 2-minute hard timeout.
59
+
60
+ ${c.gray(`v${VERSION}`)}
61
+ `;
62
+
63
+ function parseArgs(argv) {
64
+ const args = { _: [], flags: {} };
65
+ for (let i = 0; i < argv.length; i++) {
66
+ const a = argv[i];
67
+ if (a === "--yes") { args.flags.yes = true; }
68
+ else if (a === "--unsafe-paths") { args.flags.unsafePaths = true; }
69
+ else if (a === "--help" || a === "-h") { args.flags.help = true; }
70
+ else if (a === "--version" || a === "-v") { args.flags.version = true; }
71
+ else if (a === "--cwd") { args.flags.cwd = argv[++i]; }
72
+ else if (a === "--max-turns") { args.flags.maxTurns = parseInt(argv[++i], 10); }
73
+ else if (a.startsWith("--")) {
74
+ const eq = a.indexOf("=");
75
+ if (eq >= 0) args.flags[a.slice(2, eq)] = a.slice(eq + 1);
76
+ else args.flags[a.slice(2)] = true;
77
+ } else {
78
+ args._.push(a);
79
+ }
80
+ }
81
+ return args;
82
+ }
83
+
84
+ function die(msg, code = 1) {
85
+ process.stderr.write(errorLine(msg) + "\n");
86
+ process.exit(code);
87
+ }
88
+
89
+ async function main() {
90
+ const args = parseArgs(process.argv.slice(2));
91
+
92
+ if (args.flags.help) {
93
+ process.stdout.write(HELP);
94
+ return;
95
+ }
96
+ if (args.flags.version) {
97
+ process.stdout.write(`aether-code ${VERSION}\n`);
98
+ return;
99
+ }
100
+
101
+ const cwd = args.flags.cwd ? path.resolve(args.flags.cwd) : process.cwd();
102
+ const autoYes = !!args.flags.yes;
103
+ const unsafePaths = !!args.flags.unsafePaths;
104
+ const maxTurns = Number.isInteger(args.flags.maxTurns) ? args.flags.maxTurns : 25;
105
+
106
+ // Subcommand routing — these shadow the "task as positional arg" mode
107
+ const sub = args._[0]?.toLowerCase();
108
+ if (sub === "login" || sub === "auth") {
109
+ const ok = await runSetup();
110
+ process.exit(ok ? 0 : 1);
111
+ }
112
+ if (sub === "logout") {
113
+ writeConfigFile({ apiKey: "" });
114
+ console.log(c.gray(`Cleared API key from ${CONFIG_PATH}.`));
115
+ return;
116
+ }
117
+ if (sub === "config") {
118
+ await handleConfig(args._.slice(1));
119
+ return;
120
+ }
121
+ if (sub === "balance") {
122
+ await handleBalance();
123
+ return;
124
+ }
125
+
126
+ const prompt = args._.join(" ").trim();
127
+
128
+ // No task drop into interactive REPL (Claude-CLI-style)
129
+ if (!prompt) {
130
+ if (cwd !== process.cwd()) process.chdir(cwd);
131
+ await runRepl({ cwd, autoYes, maxTurns });
132
+ return;
133
+ }
134
+
135
+ // One-shot mode also needs an API key. If missing, run setup before the task.
136
+ const cfg = getConfig();
137
+ if (!cfg.apiKey) {
138
+ const ok = await runSetup();
139
+ if (!ok) process.exit(1);
140
+ }
141
+
142
+ console.log(divider());
143
+ console.log(c.magenta(c.bold("aether-code")) + c.gray(` · cwd ${cwd}${autoYes ? " · auto-yes" : ""}${unsafePaths ? " · unsafe-paths" : ""}`));
144
+ console.log(c.gray(`task: `) + prompt);
145
+ console.log(divider());
146
+
147
+ const result = await runAgent({
148
+ initialPrompt: prompt,
149
+ cwd,
150
+ autoYes,
151
+ unsafePaths,
152
+ maxTurns,
153
+ });
154
+
155
+ console.log("\n" + divider());
156
+ if (result.ok) {
157
+ console.log(c.green(c.bold("✓ Done")) + c.gray(` ${result.turns} turn${result.turns === 1 ? "" : "s"} · ${result.totalCredits} credits · ${result.totalIn}→${result.totalOut} tokens`));
158
+ if (typeof result.balance === "number") {
159
+ console.log(c.gray(` balance: ${result.balance.toLocaleString()} credits`));
160
+ }
161
+ } else {
162
+ console.log(c.red(c.bold("✗ Stopped")) + c.gray(` ${result.totalCredits} credits used · ${result.totalIn}→${result.totalOut} tokens`));
163
+ if (result.error) console.log(errorLine(result.error.message));
164
+ }
165
+ console.log(divider());
166
+ }
167
+
168
+ async function handleConfig(rest) {
169
+ const sub = (rest[0] || "").toLowerCase();
170
+ if (sub === "show" || !sub) {
171
+ const cfg = getConfig();
172
+ console.log(`Config file: ${cfg.configPath}`);
173
+ console.log(`API key: ${cfg.apiKey ? cfg.apiKey.slice(0, 12) + "…" + cfg.apiKey.slice(-4) : c.gray("(none)")}`);
174
+ console.log(`Base URL: ${cfg.baseUrl}`);
175
+ console.log(`Source: ${process.env.AETHER_API_KEY ? "AETHER_API_KEY env" : "config file"}`);
176
+ return;
177
+ }
178
+ if (sub === "set") {
179
+ const key = rest[1];
180
+ if (!key) die("config set: missing API key argument.");
181
+ if (!key.startsWith("ak_live_")) {
182
+ process.stderr.write(c.yellow("warning: keys normally start with ak_live_; saving anyway.\n"));
183
+ }
184
+ writeConfigFile({ apiKey: key });
185
+ console.log(`${c.green("✓")} API key saved to ${CONFIG_PATH}`);
186
+ return;
187
+ }
188
+ if (sub === "set-base") {
189
+ const url = rest[1];
190
+ if (!url) die("config set-base: missing URL argument.");
191
+ writeConfigFile({ baseUrl: url });
192
+ console.log(`${c.green("✓")} Base URL saved.`);
193
+ return;
194
+ }
195
+ if (sub === "path") {
196
+ console.log(CONFIG_PATH);
197
+ return;
198
+ }
199
+ die(`config: unknown subcommand "${sub}". Try 'config show', 'config set <key>', 'config path'.`);
200
+ }
201
+
202
+ async function handleBalance() {
203
+ try {
204
+ const me = await fetchBalance();
205
+ console.log(c.bold(c.magenta("Aether")));
206
+ console.log(c.gray("─".repeat(50)));
207
+ console.log(`Plan ${c.cyan(me.plan)}${me.role !== "USER" ? c.gray(` · ${me.role}`) : ""}`);
208
+ console.log(`Balance ${c.bold(me.balance.toLocaleString())} credits`);
209
+ console.log(` plan ${me.planCredits.toLocaleString()}`);
210
+ console.log(` topup ${me.topupCredits.toLocaleString()}`);
211
+ if (me.rate) {
212
+ console.log(`Rate ${me.rate.used}/${me.rate.limit} this hour${me.rate.resetIn ? ` · resets in ${me.rate.resetIn}s` : ""}`);
213
+ }
214
+ if (me.isSuspended) console.log(c.red("\n⚠ Account is suspended."));
215
+ } catch (err) {
216
+ if (err instanceof AetherError && err.code === "NO_API_KEY") {
217
+ console.log(errorLine("No API key. Run `aether login` first."));
218
+ } else {
219
+ die(err.message || String(err));
220
+ }
221
+ }
222
+ }
223
+
224
+ main().catch((err) => {
225
+ console.error(errorLine(err.message || String(err)));
226
+ if (process.env.DEBUG) console.error(err);
227
+ process.exit(1);
228
+ });