aether-code 0.11.1 → 0.13.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 +17 -2
- package/bin/aether-code.js +457 -361
- package/package.json +69 -68
- package/src/agent.js +197 -197
- package/src/api.js +287 -234
- package/src/config.js +38 -38
- package/src/diff.js +48 -48
- package/src/mcp-registry.js +266 -0
- package/src/render.js +58 -58
- package/src/repl.js +247 -247
- package/src/setup.js +139 -139
- package/src/skills.js +3 -0
- package/src/tools.js +803 -621
package/bin/aether-code.js
CHANGED
|
@@ -1,361 +1,457 @@
|
|
|
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 { addServer, removeServer, listServers } from "../src/mcp-cli.js";
|
|
19
|
-
import {
|
|
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
|
-
${c.bold("
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
--
|
|
84
|
-
--
|
|
85
|
-
|
|
86
|
-
${c.bold("
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
-
|
|
92
|
-
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
${c.
|
|
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
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
if (sub === "
|
|
162
|
-
await
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
if (
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
//
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const
|
|
180
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
if (sub === "
|
|
223
|
-
const
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
console.log(
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
const
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
if (
|
|
360
|
-
|
|
361
|
-
|
|
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 { addServer, removeServer, listServers } from "../src/mcp-cli.js";
|
|
19
|
+
import {
|
|
20
|
+
MCP_REGISTRY,
|
|
21
|
+
findRegistryEntry,
|
|
22
|
+
resolveEntry,
|
|
23
|
+
searchRegistry,
|
|
24
|
+
suggestSimilar,
|
|
25
|
+
} from "../src/mcp-registry.js";
|
|
26
|
+
import readline from "node:readline";
|
|
27
|
+
import { c, errorLine, divider } from "../src/render.js";
|
|
28
|
+
|
|
29
|
+
const VERSION = "0.13.0";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Try to start MCP servers from ~/.aether/mcp.json. Returns a started
|
|
33
|
+
* MCPManager (possibly with zero servers) or null if no config exists.
|
|
34
|
+
* Prints a one-line summary so the user can see what attached.
|
|
35
|
+
*/
|
|
36
|
+
async function bootMcp() {
|
|
37
|
+
let config;
|
|
38
|
+
try {
|
|
39
|
+
config = loadMcpConfig();
|
|
40
|
+
} catch (e) {
|
|
41
|
+
console.log(errorLine(`MCP config: ${e.message}`));
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
if (!config) return null;
|
|
45
|
+
const manager = new MCPManager();
|
|
46
|
+
const started = await manager.start(config);
|
|
47
|
+
const requested = Object.keys(config.mcpServers).length;
|
|
48
|
+
const failed = manager.startErrors.length;
|
|
49
|
+
if (started > 0 || failed > 0) {
|
|
50
|
+
const parts = [`${c.cyan("MCP")}`, `${started}/${requested} servers attached`];
|
|
51
|
+
const toolCount = manager.getToolDefinitions().length;
|
|
52
|
+
if (toolCount > 0) parts.push(`${toolCount} tools`);
|
|
53
|
+
console.log(c.gray(parts.join(" · ")));
|
|
54
|
+
for (const { serverName, error } of manager.startErrors) {
|
|
55
|
+
console.log(c.gray(` ${c.yellow("!")} ${serverName}: ${error}`));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Best-effort cleanup so child processes don't leak on normal exit.
|
|
59
|
+
process.on("exit", () => { manager.shutdown().catch(() => {}); });
|
|
60
|
+
process.on("SIGINT", () => { manager.shutdown().catch(() => {}); process.exit(130); });
|
|
61
|
+
return manager;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const HELP = `${c.bold("aether")} — uncensored AI coding agent
|
|
65
|
+
|
|
66
|
+
${c.bold("USAGE")}
|
|
67
|
+
aether Launch interactive REPL (Claude-CLI-style)
|
|
68
|
+
aether [flags] "<task>" Run agent once on a single task
|
|
69
|
+
aether <subcommand> [args] Run a utility subcommand
|
|
70
|
+
|
|
71
|
+
${c.bold("SUBCOMMANDS")}
|
|
72
|
+
${c.cyan("login")} Open browser, paste API key, save
|
|
73
|
+
${c.cyan("logout")} Clear saved API key
|
|
74
|
+
${c.cyan("balance")} Show plan + credit balance
|
|
75
|
+
${c.cyan("config")} show|set|set-base|path Manage config file
|
|
76
|
+
${c.cyan("mcp")} list|search|install|add|remove Manage MCP server connections
|
|
77
|
+
|
|
78
|
+
${c.bold("EXAMPLES")}
|
|
79
|
+
aether # interactive REPL
|
|
80
|
+
aether login # first-time setup
|
|
81
|
+
aether balance # quick credit check
|
|
82
|
+
aether "build a TypeScript todo CLI in this folder"
|
|
83
|
+
aether --yes "add JSDoc to every exported function"
|
|
84
|
+
aether --cwd ./my-project "fix the failing tests"
|
|
85
|
+
|
|
86
|
+
${c.bold("FLAGS")}
|
|
87
|
+
--yes Auto-approve all writes and shell commands. Use with care.
|
|
88
|
+
--cwd <path> Working directory for the agent (default: current dir).
|
|
89
|
+
--max-turns <n> Maximum turns before stopping (default: 25).
|
|
90
|
+
--unsafe-paths Allow the agent to read/write outside cwd.
|
|
91
|
+
--help, -h Show this help.
|
|
92
|
+
--version, -v Print version.
|
|
93
|
+
|
|
94
|
+
${c.bold("CONFIG")}
|
|
95
|
+
Same config as aether-cli — uses ${c.cyan("AETHER_API_KEY")} or ${c.cyan("~/.aetherrc")}.
|
|
96
|
+
Get a key at ${c.blue("https://trynoguard.com/account")}.
|
|
97
|
+
|
|
98
|
+
${c.bold("SAFETY")}
|
|
99
|
+
- File writes show a unified diff and require y/N confirmation by default.
|
|
100
|
+
- Shell commands show what's about to run and require y/N confirmation.
|
|
101
|
+
- Paths are clamped to ${c.cyan("--cwd")} (override with ${c.cyan("--unsafe-paths")}).
|
|
102
|
+
- Each shell command has a 2-minute hard timeout.
|
|
103
|
+
|
|
104
|
+
${c.gray(`v${VERSION}`)}
|
|
105
|
+
`;
|
|
106
|
+
|
|
107
|
+
function parseArgs(argv) {
|
|
108
|
+
const args = { _: [], flags: {} };
|
|
109
|
+
for (let i = 0; i < argv.length; i++) {
|
|
110
|
+
const a = argv[i];
|
|
111
|
+
if (a === "--yes") { args.flags.yes = true; }
|
|
112
|
+
else if (a === "--unsafe-paths") { args.flags.unsafePaths = true; }
|
|
113
|
+
else if (a === "--help" || a === "-h") { args.flags.help = true; }
|
|
114
|
+
else if (a === "--version" || a === "-v") { args.flags.version = true; }
|
|
115
|
+
else if (a === "--cwd") { args.flags.cwd = argv[++i]; }
|
|
116
|
+
else if (a === "--max-turns") { args.flags.maxTurns = parseInt(argv[++i], 10); }
|
|
117
|
+
else if (a.startsWith("--")) {
|
|
118
|
+
const eq = a.indexOf("=");
|
|
119
|
+
if (eq >= 0) args.flags[a.slice(2, eq)] = a.slice(eq + 1);
|
|
120
|
+
else args.flags[a.slice(2)] = true;
|
|
121
|
+
} else {
|
|
122
|
+
args._.push(a);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return args;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function die(msg, code = 1) {
|
|
129
|
+
process.stderr.write(errorLine(msg) + "\n");
|
|
130
|
+
process.exit(code);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function main() {
|
|
134
|
+
const args = parseArgs(process.argv.slice(2));
|
|
135
|
+
|
|
136
|
+
if (args.flags.help) {
|
|
137
|
+
process.stdout.write(HELP);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (args.flags.version) {
|
|
141
|
+
process.stdout.write(`aether-code ${VERSION}\n`);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const cwd = args.flags.cwd ? path.resolve(args.flags.cwd) : process.cwd();
|
|
146
|
+
const autoYes = !!args.flags.yes;
|
|
147
|
+
const unsafePaths = !!args.flags.unsafePaths;
|
|
148
|
+
const maxTurns = Number.isInteger(args.flags.maxTurns) ? args.flags.maxTurns : 25;
|
|
149
|
+
|
|
150
|
+
// Subcommand routing — these shadow the "task as positional arg" mode
|
|
151
|
+
const sub = args._[0]?.toLowerCase();
|
|
152
|
+
if (sub === "login" || sub === "auth") {
|
|
153
|
+
const ok = await runSetup();
|
|
154
|
+
process.exit(ok ? 0 : 1);
|
|
155
|
+
}
|
|
156
|
+
if (sub === "logout") {
|
|
157
|
+
writeConfigFile({ apiKey: "" });
|
|
158
|
+
console.log(c.gray(`Cleared API key from ${CONFIG_PATH}.`));
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (sub === "config") {
|
|
162
|
+
await handleConfig(args._.slice(1));
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (sub === "balance") {
|
|
166
|
+
await handleBalance();
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (sub === "mcp") {
|
|
170
|
+
await handleMcp(args._.slice(1));
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const prompt = args._.join(" ").trim();
|
|
175
|
+
|
|
176
|
+
// No task → drop into interactive REPL (Claude-CLI-style)
|
|
177
|
+
if (!prompt) {
|
|
178
|
+
if (cwd !== process.cwd()) process.chdir(cwd);
|
|
179
|
+
const mcpManager = await bootMcp();
|
|
180
|
+
await runRepl({ cwd, autoYes, maxTurns, mcpManager });
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// One-shot mode also needs an API key. If missing, run setup before the task.
|
|
185
|
+
const cfg = getConfig();
|
|
186
|
+
if (!cfg.apiKey) {
|
|
187
|
+
const ok = await runSetup();
|
|
188
|
+
if (!ok) process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
console.log(divider());
|
|
192
|
+
console.log(c.magenta(c.bold("aether-code")) + c.gray(` · cwd ${cwd}${autoYes ? " · auto-yes" : ""}${unsafePaths ? " · unsafe-paths" : ""}`));
|
|
193
|
+
console.log(c.gray(`task: `) + prompt);
|
|
194
|
+
console.log(divider());
|
|
195
|
+
|
|
196
|
+
const mcpManager = await bootMcp();
|
|
197
|
+
const result = await runAgent({
|
|
198
|
+
initialPrompt: prompt,
|
|
199
|
+
cwd,
|
|
200
|
+
autoYes,
|
|
201
|
+
unsafePaths,
|
|
202
|
+
maxTurns,
|
|
203
|
+
mcpManager,
|
|
204
|
+
});
|
|
205
|
+
if (mcpManager) await mcpManager.shutdown().catch(() => {});
|
|
206
|
+
|
|
207
|
+
console.log("\n" + divider());
|
|
208
|
+
if (result.ok) {
|
|
209
|
+
console.log(c.green(c.bold("✓ Done")) + c.gray(` ${result.turns} turn${result.turns === 1 ? "" : "s"} · ${result.totalCredits} credits · ${result.totalIn}→${result.totalOut} tokens`));
|
|
210
|
+
if (typeof result.balance === "number") {
|
|
211
|
+
console.log(c.gray(` balance: ${result.balance.toLocaleString()} credits`));
|
|
212
|
+
}
|
|
213
|
+
} else {
|
|
214
|
+
console.log(c.red(c.bold("✗ Stopped")) + c.gray(` ${result.totalCredits} credits used · ${result.totalIn}→${result.totalOut} tokens`));
|
|
215
|
+
if (result.error) console.log(errorLine(result.error.message));
|
|
216
|
+
}
|
|
217
|
+
console.log(divider());
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function handleConfig(rest) {
|
|
221
|
+
const sub = (rest[0] || "").toLowerCase();
|
|
222
|
+
if (sub === "show" || !sub) {
|
|
223
|
+
const cfg = getConfig();
|
|
224
|
+
console.log(`Config file: ${cfg.configPath}`);
|
|
225
|
+
console.log(`API key: ${cfg.apiKey ? cfg.apiKey.slice(0, 12) + "…" + cfg.apiKey.slice(-4) : c.gray("(none)")}`);
|
|
226
|
+
console.log(`Base URL: ${cfg.baseUrl}`);
|
|
227
|
+
console.log(`Source: ${process.env.AETHER_API_KEY ? "AETHER_API_KEY env" : "config file"}`);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (sub === "set") {
|
|
231
|
+
const key = rest[1];
|
|
232
|
+
if (!key) die("config set: missing API key argument.");
|
|
233
|
+
if (!key.startsWith("ak_live_")) {
|
|
234
|
+
process.stderr.write(c.yellow("warning: keys normally start with ak_live_; saving anyway.\n"));
|
|
235
|
+
}
|
|
236
|
+
writeConfigFile({ apiKey: key });
|
|
237
|
+
console.log(`${c.green("✓")} API key saved to ${CONFIG_PATH}`);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (sub === "set-base") {
|
|
241
|
+
const url = rest[1];
|
|
242
|
+
if (!url) die("config set-base: missing URL argument.");
|
|
243
|
+
writeConfigFile({ baseUrl: url });
|
|
244
|
+
console.log(`${c.green("✓")} Base URL saved.`);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (sub === "path") {
|
|
248
|
+
console.log(CONFIG_PATH);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
die(`config: unknown subcommand "${sub}". Try 'config show', 'config set <key>', 'config path'.`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function handleBalance() {
|
|
255
|
+
try {
|
|
256
|
+
const me = await fetchBalance();
|
|
257
|
+
console.log(c.bold(c.magenta("Aether")));
|
|
258
|
+
console.log(c.gray("─".repeat(50)));
|
|
259
|
+
console.log(`Plan ${c.cyan(me.plan)}${me.role !== "USER" ? c.gray(` · ${me.role}`) : ""}`);
|
|
260
|
+
console.log(`Balance ${c.bold(me.balance.toLocaleString())} credits`);
|
|
261
|
+
console.log(` plan ${me.planCredits.toLocaleString()}`);
|
|
262
|
+
console.log(` topup ${me.topupCredits.toLocaleString()}`);
|
|
263
|
+
if (me.rate) {
|
|
264
|
+
console.log(`Rate ${me.rate.used}/${me.rate.limit} this hour${me.rate.resetIn ? ` · resets in ${me.rate.resetIn}s` : ""}`);
|
|
265
|
+
}
|
|
266
|
+
if (me.isSuspended) console.log(c.red("\n⚠ Account is suspended."));
|
|
267
|
+
} catch (err) {
|
|
268
|
+
if (err instanceof AetherError && err.code === "NO_API_KEY") {
|
|
269
|
+
console.log(errorLine("No API key. Run `aether login` first."));
|
|
270
|
+
} else {
|
|
271
|
+
die(err.message || String(err));
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async function handleMcp(rest) {
|
|
277
|
+
const sub = (rest[0] || "list").toLowerCase();
|
|
278
|
+
|
|
279
|
+
if (sub === "list" || sub === "ls") {
|
|
280
|
+
const servers = listServers();
|
|
281
|
+
if (servers.length === 0) {
|
|
282
|
+
console.log(c.gray("No MCP servers configured."));
|
|
283
|
+
console.log(c.gray("Add one with:"));
|
|
284
|
+
console.log(c.gray(" aether mcp add <name> -- <command> [args...]"));
|
|
285
|
+
console.log(c.gray("Example:"));
|
|
286
|
+
console.log(
|
|
287
|
+
c.gray(' aether mcp add filesystem -- npx -y @modelcontextprotocol/server-filesystem /path'),
|
|
288
|
+
);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
console.log(c.bold(`Configured MCP servers (${servers.length}):`));
|
|
292
|
+
for (const [name, cfg] of servers) {
|
|
293
|
+
const argsStr = cfg.args && cfg.args.length > 0 ? " " + cfg.args.join(" ") : "";
|
|
294
|
+
console.log(` ${c.cyan(name)}: ${cfg.command}${argsStr}`);
|
|
295
|
+
if (cfg.env && Object.keys(cfg.env).length > 0) {
|
|
296
|
+
for (const [k, v] of Object.entries(cfg.env)) {
|
|
297
|
+
console.log(c.gray(` env ${k}=${v}`));
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (sub === "add") {
|
|
305
|
+
// Syntax: aether mcp add <name> [--env KEY=VAL]... -- <command> [args...]
|
|
306
|
+
const tail = rest.slice(1);
|
|
307
|
+
const dashIdx = tail.indexOf("--");
|
|
308
|
+
if (dashIdx === -1) {
|
|
309
|
+
die(
|
|
310
|
+
'Usage: aether mcp add <name> [--env KEY=VAL]... -- <command> [args...]\n' +
|
|
311
|
+
'Example: aether mcp add fs -- npx -y @modelcontextprotocol/server-filesystem /tmp',
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
const pre = tail.slice(0, dashIdx);
|
|
315
|
+
const post = tail.slice(dashIdx + 1);
|
|
316
|
+
const name = pre[0];
|
|
317
|
+
if (!name) die("aether mcp add: missing <name>");
|
|
318
|
+
if (post.length === 0) die("aether mcp add: missing <command> after '--'");
|
|
319
|
+
|
|
320
|
+
const env = {};
|
|
321
|
+
for (let i = 1; i < pre.length; i++) {
|
|
322
|
+
if (pre[i] === "--env") {
|
|
323
|
+
const kv = pre[++i];
|
|
324
|
+
if (!kv) die("--env needs a KEY=VAL argument");
|
|
325
|
+
const eq = kv.indexOf("=");
|
|
326
|
+
if (eq <= 0) die(`--env value must be KEY=VAL, got: ${kv}`);
|
|
327
|
+
env[kv.slice(0, eq)] = kv.slice(eq + 1);
|
|
328
|
+
} else {
|
|
329
|
+
die(`aether mcp add: unrecognized option "${pre[i]}" before the '--' separator`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const command = post[0];
|
|
334
|
+
const cmdArgs = post.slice(1);
|
|
335
|
+
try {
|
|
336
|
+
const entry = addServer({ name, command, args: cmdArgs, env });
|
|
337
|
+
console.log(`${c.green("✓")} Added MCP server "${c.cyan(name)}".`);
|
|
338
|
+
const argsStr = entry.args && entry.args.length > 0 ? " " + entry.args.join(" ") : "";
|
|
339
|
+
console.log(c.gray(` ${entry.command}${argsStr}`));
|
|
340
|
+
console.log(c.gray("Restart the agent (or run `aether`) to attach it."));
|
|
341
|
+
} catch (e) {
|
|
342
|
+
die(e.message || String(e));
|
|
343
|
+
}
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (sub === "remove" || sub === "rm" || sub === "delete") {
|
|
348
|
+
const name = rest[1];
|
|
349
|
+
if (!name) die("aether mcp remove: missing <name>");
|
|
350
|
+
try {
|
|
351
|
+
removeServer({ name });
|
|
352
|
+
console.log(`${c.green("✓")} Removed MCP server "${c.cyan(name)}".`);
|
|
353
|
+
} catch (e) {
|
|
354
|
+
die(e.message || String(e));
|
|
355
|
+
}
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (sub === "search" || sub === "find") {
|
|
360
|
+
const query = rest.slice(1).join(" ").trim();
|
|
361
|
+
const results = searchRegistry(query);
|
|
362
|
+
if (results.length === 0) {
|
|
363
|
+
console.log(c.gray(`No MCP servers in the registry match "${query}".`));
|
|
364
|
+
console.log(c.gray("Browse the full list: ") + c.cyan("aether mcp search"));
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
console.log(
|
|
368
|
+
c.bold(query ? `MCP servers matching "${query}":` : `Available MCP servers (${results.length}):`),
|
|
369
|
+
);
|
|
370
|
+
for (const e of results) {
|
|
371
|
+
const sourceTag = e.source === "official"
|
|
372
|
+
? c.gray("(official)")
|
|
373
|
+
: c.yellow("(community)");
|
|
374
|
+
console.log(` ${c.cyan(e.id.padEnd(16))} ${sourceTag} ${e.description}`);
|
|
375
|
+
}
|
|
376
|
+
console.log("");
|
|
377
|
+
console.log(c.gray("Install one with: ") + c.cyan("aether mcp install <name>"));
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (sub === "install" || sub === "get") {
|
|
382
|
+
const name = rest[1];
|
|
383
|
+
if (!name) {
|
|
384
|
+
die(
|
|
385
|
+
"aether mcp install: missing <name>.\n" +
|
|
386
|
+
"Try `aether mcp search` to see what's available.",
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
const entry = findRegistryEntry(name);
|
|
390
|
+
if (!entry) {
|
|
391
|
+
const suggestions = suggestSimilar(name);
|
|
392
|
+
let msg = `aether mcp install: unknown server "${name}".`;
|
|
393
|
+
if (suggestions.length > 0) {
|
|
394
|
+
msg += `\nDid you mean: ${suggestions.map((s) => c.cyan(s)).join(", ")}?`;
|
|
395
|
+
} else {
|
|
396
|
+
msg += `\nTry \`aether mcp search\` to browse the registry.`;
|
|
397
|
+
}
|
|
398
|
+
die(msg);
|
|
399
|
+
}
|
|
400
|
+
// Prompt for any required values (placeholders in args + env)
|
|
401
|
+
const allRequired = [...(entry.requires ?? []), ...(entry.requiresEnv ?? [])];
|
|
402
|
+
const values = {};
|
|
403
|
+
if (allRequired.length > 0) {
|
|
404
|
+
console.log(c.gray(`Installing ${c.cyan(entry.id)} — needs ${allRequired.length} input${allRequired.length === 1 ? "" : "s"}:`));
|
|
405
|
+
for (const key of allRequired) {
|
|
406
|
+
const promptText = entry.prompts?.[key] ?? key;
|
|
407
|
+
// eslint-disable-next-line no-await-in-loop -- sequential prompts are intentional
|
|
408
|
+
values[key] = await promptUser(` ${promptText}: `);
|
|
409
|
+
if (!values[key]) die(`Cancelled — "${key}" is required.`);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
let resolved;
|
|
413
|
+
try {
|
|
414
|
+
resolved = resolveEntry(entry, values);
|
|
415
|
+
} catch (e) {
|
|
416
|
+
die(e.message);
|
|
417
|
+
}
|
|
418
|
+
try {
|
|
419
|
+
const added = addServer({
|
|
420
|
+
name: entry.id,
|
|
421
|
+
command: resolved.command,
|
|
422
|
+
args: resolved.args,
|
|
423
|
+
env: resolved.env,
|
|
424
|
+
});
|
|
425
|
+
console.log(`${c.green("✓")} Installed MCP server "${c.cyan(entry.id)}".`);
|
|
426
|
+
console.log(c.gray(` ${added.command}${added.args ? " " + added.args.join(" ") : ""}`));
|
|
427
|
+
console.log(c.gray("Restart aether (or run `aether`) to attach it."));
|
|
428
|
+
} catch (e) {
|
|
429
|
+
die(e.message || String(e));
|
|
430
|
+
}
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
die(
|
|
435
|
+
`aether mcp: unknown subcommand "${sub}".\n` +
|
|
436
|
+
"Try one of: list, add, install, search, remove.",
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function promptUser(question) {
|
|
441
|
+
if (!process.stdin.isTTY) {
|
|
442
|
+
return Promise.reject(new Error("Interactive prompt unavailable (non-TTY)"));
|
|
443
|
+
}
|
|
444
|
+
return new Promise((resolve) => {
|
|
445
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
446
|
+
rl.question(question, (answer) => {
|
|
447
|
+
rl.close();
|
|
448
|
+
resolve(answer.trim());
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
main().catch((err) => {
|
|
454
|
+
console.error(errorLine(err.message || String(err)));
|
|
455
|
+
if (process.env.DEBUG) console.error(err);
|
|
456
|
+
process.exit(1);
|
|
457
|
+
});
|