aether-code 0.6.2 → 0.9.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.
- package/LICENSE +21 -21
- package/README.md +140 -140
- package/bin/aether-code.js +266 -379
- package/package.json +42 -38
- package/src/agent.js +134 -201
- package/src/api.js +234 -236
- package/src/config.js +38 -38
- package/src/diff.js +48 -48
- package/src/mcp.js +259 -0
- package/src/render.js +58 -198
- package/src/repl.js +247 -292
- package/src/setup.js +139 -139
- package/src/tools.js +621 -358
- package/src/plan.js +0 -133
- package/src/sessions.js +0 -145
package/bin/aether-code.js
CHANGED
|
@@ -1,379 +1,266 @@
|
|
|
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 {
|
|
18
|
-
import {
|
|
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
|
-
${c.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
console.log(
|
|
179
|
-
console.log(c.
|
|
180
|
-
console.log(
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
212
|
-
console.log(
|
|
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
|
-
|
|
251
|
-
|
|
252
|
-
console.log(
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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 { loadMcpConfig, MCPManager } from "../src/mcp.js";
|
|
18
|
+
import { c, errorLine, divider } from "../src/render.js";
|
|
19
|
+
|
|
20
|
+
const VERSION = "0.9.0";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Try to start MCP servers from ~/.aether/mcp.json. Returns a started
|
|
24
|
+
* MCPManager (possibly with zero servers) or null if no config exists.
|
|
25
|
+
* Prints a one-line summary so the user can see what attached.
|
|
26
|
+
*/
|
|
27
|
+
async function bootMcp() {
|
|
28
|
+
let config;
|
|
29
|
+
try {
|
|
30
|
+
config = loadMcpConfig();
|
|
31
|
+
} catch (e) {
|
|
32
|
+
console.log(errorLine(`MCP config: ${e.message}`));
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
if (!config) return null;
|
|
36
|
+
const manager = new MCPManager();
|
|
37
|
+
const started = await manager.start(config);
|
|
38
|
+
const requested = Object.keys(config.mcpServers).length;
|
|
39
|
+
const failed = manager.startErrors.length;
|
|
40
|
+
if (started > 0 || failed > 0) {
|
|
41
|
+
const parts = [`${c.cyan("MCP")}`, `${started}/${requested} servers attached`];
|
|
42
|
+
const toolCount = manager.getToolDefinitions().length;
|
|
43
|
+
if (toolCount > 0) parts.push(`${toolCount} tools`);
|
|
44
|
+
console.log(c.gray(parts.join(" · ")));
|
|
45
|
+
for (const { serverName, error } of manager.startErrors) {
|
|
46
|
+
console.log(c.gray(` ${c.yellow("!")} ${serverName}: ${error}`));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Best-effort cleanup so child processes don't leak on normal exit.
|
|
50
|
+
process.on("exit", () => { manager.shutdown().catch(() => {}); });
|
|
51
|
+
process.on("SIGINT", () => { manager.shutdown().catch(() => {}); process.exit(130); });
|
|
52
|
+
return manager;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const HELP = `${c.bold("aether")} — uncensored AI coding agent
|
|
56
|
+
|
|
57
|
+
${c.bold("USAGE")}
|
|
58
|
+
aether Launch interactive REPL (Claude-CLI-style)
|
|
59
|
+
aether [flags] "<task>" Run agent once on a single task
|
|
60
|
+
aether <subcommand> [args] Run a utility subcommand
|
|
61
|
+
|
|
62
|
+
${c.bold("SUBCOMMANDS")}
|
|
63
|
+
${c.cyan("login")} Open browser, paste API key, save
|
|
64
|
+
${c.cyan("logout")} Clear saved API key
|
|
65
|
+
${c.cyan("balance")} Show plan + credit balance
|
|
66
|
+
${c.cyan("config")} show|set|set-base|path Manage config file
|
|
67
|
+
|
|
68
|
+
${c.bold("EXAMPLES")}
|
|
69
|
+
aether # interactive REPL
|
|
70
|
+
aether login # first-time setup
|
|
71
|
+
aether balance # quick credit check
|
|
72
|
+
aether "build a TypeScript todo CLI in this folder"
|
|
73
|
+
aether --yes "add JSDoc to every exported function"
|
|
74
|
+
aether --cwd ./my-project "fix the failing tests"
|
|
75
|
+
|
|
76
|
+
${c.bold("FLAGS")}
|
|
77
|
+
--yes Auto-approve all writes and shell commands. Use with care.
|
|
78
|
+
--cwd <path> Working directory for the agent (default: current dir).
|
|
79
|
+
--max-turns <n> Maximum turns before stopping (default: 25).
|
|
80
|
+
--unsafe-paths Allow the agent to read/write outside cwd.
|
|
81
|
+
--help, -h Show this help.
|
|
82
|
+
--version, -v Print version.
|
|
83
|
+
|
|
84
|
+
${c.bold("CONFIG")}
|
|
85
|
+
Same config as aether-cli — uses ${c.cyan("AETHER_API_KEY")} or ${c.cyan("~/.aetherrc")}.
|
|
86
|
+
Get a key at ${c.blue("https://trynoguard.com/account")}.
|
|
87
|
+
|
|
88
|
+
${c.bold("SAFETY")}
|
|
89
|
+
- File writes show a unified diff and require y/N confirmation by default.
|
|
90
|
+
- Shell commands show what's about to run and require y/N confirmation.
|
|
91
|
+
- Paths are clamped to ${c.cyan("--cwd")} (override with ${c.cyan("--unsafe-paths")}).
|
|
92
|
+
- Each shell command has a 2-minute hard timeout.
|
|
93
|
+
|
|
94
|
+
${c.gray(`v${VERSION}`)}
|
|
95
|
+
`;
|
|
96
|
+
|
|
97
|
+
function parseArgs(argv) {
|
|
98
|
+
const args = { _: [], flags: {} };
|
|
99
|
+
for (let i = 0; i < argv.length; i++) {
|
|
100
|
+
const a = argv[i];
|
|
101
|
+
if (a === "--yes") { args.flags.yes = true; }
|
|
102
|
+
else if (a === "--unsafe-paths") { args.flags.unsafePaths = true; }
|
|
103
|
+
else if (a === "--help" || a === "-h") { args.flags.help = true; }
|
|
104
|
+
else if (a === "--version" || a === "-v") { args.flags.version = true; }
|
|
105
|
+
else if (a === "--cwd") { args.flags.cwd = argv[++i]; }
|
|
106
|
+
else if (a === "--max-turns") { args.flags.maxTurns = parseInt(argv[++i], 10); }
|
|
107
|
+
else if (a.startsWith("--")) {
|
|
108
|
+
const eq = a.indexOf("=");
|
|
109
|
+
if (eq >= 0) args.flags[a.slice(2, eq)] = a.slice(eq + 1);
|
|
110
|
+
else args.flags[a.slice(2)] = true;
|
|
111
|
+
} else {
|
|
112
|
+
args._.push(a);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return args;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function die(msg, code = 1) {
|
|
119
|
+
process.stderr.write(errorLine(msg) + "\n");
|
|
120
|
+
process.exit(code);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function main() {
|
|
124
|
+
const args = parseArgs(process.argv.slice(2));
|
|
125
|
+
|
|
126
|
+
if (args.flags.help) {
|
|
127
|
+
process.stdout.write(HELP);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (args.flags.version) {
|
|
131
|
+
process.stdout.write(`aether-code ${VERSION}\n`);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const cwd = args.flags.cwd ? path.resolve(args.flags.cwd) : process.cwd();
|
|
136
|
+
const autoYes = !!args.flags.yes;
|
|
137
|
+
const unsafePaths = !!args.flags.unsafePaths;
|
|
138
|
+
const maxTurns = Number.isInteger(args.flags.maxTurns) ? args.flags.maxTurns : 25;
|
|
139
|
+
|
|
140
|
+
// Subcommand routing — these shadow the "task as positional arg" mode
|
|
141
|
+
const sub = args._[0]?.toLowerCase();
|
|
142
|
+
if (sub === "login" || sub === "auth") {
|
|
143
|
+
const ok = await runSetup();
|
|
144
|
+
process.exit(ok ? 0 : 1);
|
|
145
|
+
}
|
|
146
|
+
if (sub === "logout") {
|
|
147
|
+
writeConfigFile({ apiKey: "" });
|
|
148
|
+
console.log(c.gray(`Cleared API key from ${CONFIG_PATH}.`));
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (sub === "config") {
|
|
152
|
+
await handleConfig(args._.slice(1));
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (sub === "balance") {
|
|
156
|
+
await handleBalance();
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const prompt = args._.join(" ").trim();
|
|
161
|
+
|
|
162
|
+
// No task → drop into interactive REPL (Claude-CLI-style)
|
|
163
|
+
if (!prompt) {
|
|
164
|
+
if (cwd !== process.cwd()) process.chdir(cwd);
|
|
165
|
+
const mcpManager = await bootMcp();
|
|
166
|
+
await runRepl({ cwd, autoYes, maxTurns, mcpManager });
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// One-shot mode also needs an API key. If missing, run setup before the task.
|
|
171
|
+
const cfg = getConfig();
|
|
172
|
+
if (!cfg.apiKey) {
|
|
173
|
+
const ok = await runSetup();
|
|
174
|
+
if (!ok) process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
console.log(divider());
|
|
178
|
+
console.log(c.magenta(c.bold("aether-code")) + c.gray(` · cwd ${cwd}${autoYes ? " · auto-yes" : ""}${unsafePaths ? " · unsafe-paths" : ""}`));
|
|
179
|
+
console.log(c.gray(`task: `) + prompt);
|
|
180
|
+
console.log(divider());
|
|
181
|
+
|
|
182
|
+
const mcpManager = await bootMcp();
|
|
183
|
+
const result = await runAgent({
|
|
184
|
+
initialPrompt: prompt,
|
|
185
|
+
cwd,
|
|
186
|
+
autoYes,
|
|
187
|
+
unsafePaths,
|
|
188
|
+
maxTurns,
|
|
189
|
+
mcpManager,
|
|
190
|
+
});
|
|
191
|
+
if (mcpManager) await mcpManager.shutdown().catch(() => {});
|
|
192
|
+
|
|
193
|
+
console.log("\n" + divider());
|
|
194
|
+
if (result.ok) {
|
|
195
|
+
console.log(c.green(c.bold("✓ Done")) + c.gray(` ${result.turns} turn${result.turns === 1 ? "" : "s"} · ${result.totalCredits} credits · ${result.totalIn}→${result.totalOut} tokens`));
|
|
196
|
+
if (typeof result.balance === "number") {
|
|
197
|
+
console.log(c.gray(` balance: ${result.balance.toLocaleString()} credits`));
|
|
198
|
+
}
|
|
199
|
+
} else {
|
|
200
|
+
console.log(c.red(c.bold("✗ Stopped")) + c.gray(` ${result.totalCredits} credits used · ${result.totalIn}→${result.totalOut} tokens`));
|
|
201
|
+
if (result.error) console.log(errorLine(result.error.message));
|
|
202
|
+
}
|
|
203
|
+
console.log(divider());
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function handleConfig(rest) {
|
|
207
|
+
const sub = (rest[0] || "").toLowerCase();
|
|
208
|
+
if (sub === "show" || !sub) {
|
|
209
|
+
const cfg = getConfig();
|
|
210
|
+
console.log(`Config file: ${cfg.configPath}`);
|
|
211
|
+
console.log(`API key: ${cfg.apiKey ? cfg.apiKey.slice(0, 12) + "…" + cfg.apiKey.slice(-4) : c.gray("(none)")}`);
|
|
212
|
+
console.log(`Base URL: ${cfg.baseUrl}`);
|
|
213
|
+
console.log(`Source: ${process.env.AETHER_API_KEY ? "AETHER_API_KEY env" : "config file"}`);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (sub === "set") {
|
|
217
|
+
const key = rest[1];
|
|
218
|
+
if (!key) die("config set: missing API key argument.");
|
|
219
|
+
if (!key.startsWith("ak_live_")) {
|
|
220
|
+
process.stderr.write(c.yellow("warning: keys normally start with ak_live_; saving anyway.\n"));
|
|
221
|
+
}
|
|
222
|
+
writeConfigFile({ apiKey: key });
|
|
223
|
+
console.log(`${c.green("✓")} API key saved to ${CONFIG_PATH}`);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (sub === "set-base") {
|
|
227
|
+
const url = rest[1];
|
|
228
|
+
if (!url) die("config set-base: missing URL argument.");
|
|
229
|
+
writeConfigFile({ baseUrl: url });
|
|
230
|
+
console.log(`${c.green("✓")} Base URL saved.`);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (sub === "path") {
|
|
234
|
+
console.log(CONFIG_PATH);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
die(`config: unknown subcommand "${sub}". Try 'config show', 'config set <key>', 'config path'.`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function handleBalance() {
|
|
241
|
+
try {
|
|
242
|
+
const me = await fetchBalance();
|
|
243
|
+
console.log(c.bold(c.magenta("Aether")));
|
|
244
|
+
console.log(c.gray("─".repeat(50)));
|
|
245
|
+
console.log(`Plan ${c.cyan(me.plan)}${me.role !== "USER" ? c.gray(` · ${me.role}`) : ""}`);
|
|
246
|
+
console.log(`Balance ${c.bold(me.balance.toLocaleString())} credits`);
|
|
247
|
+
console.log(` plan ${me.planCredits.toLocaleString()}`);
|
|
248
|
+
console.log(` topup ${me.topupCredits.toLocaleString()}`);
|
|
249
|
+
if (me.rate) {
|
|
250
|
+
console.log(`Rate ${me.rate.used}/${me.rate.limit} this hour${me.rate.resetIn ? ` · resets in ${me.rate.resetIn}s` : ""}`);
|
|
251
|
+
}
|
|
252
|
+
if (me.isSuspended) console.log(c.red("\n⚠ Account is suspended."));
|
|
253
|
+
} catch (err) {
|
|
254
|
+
if (err instanceof AetherError && err.code === "NO_API_KEY") {
|
|
255
|
+
console.log(errorLine("No API key. Run `aether login` first."));
|
|
256
|
+
} else {
|
|
257
|
+
die(err.message || String(err));
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
main().catch((err) => {
|
|
263
|
+
console.error(errorLine(err.message || String(err)));
|
|
264
|
+
if (process.env.DEBUG) console.error(err);
|
|
265
|
+
process.exit(1);
|
|
266
|
+
});
|