aether-code 0.9.0 → 0.10.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/bin/aether-code.js +96 -1
- package/package.json +2 -2
- package/src/mcp-cli.js +94 -0
package/bin/aether-code.js
CHANGED
|
@@ -15,9 +15,10 @@ import { runSetup } from "../src/setup.js";
|
|
|
15
15
|
import { fetchBalance, AetherError } from "../src/api.js";
|
|
16
16
|
import { writeConfigFile, getConfig, CONFIG_PATH } from "../src/config.js";
|
|
17
17
|
import { loadMcpConfig, MCPManager } from "../src/mcp.js";
|
|
18
|
+
import { addServer, removeServer, listServers } from "../src/mcp-cli.js";
|
|
18
19
|
import { c, errorLine, divider } from "../src/render.js";
|
|
19
20
|
|
|
20
|
-
const VERSION = "0.
|
|
21
|
+
const VERSION = "0.10.0";
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* Try to start MCP servers from ~/.aether/mcp.json. Returns a started
|
|
@@ -64,6 +65,7 @@ ${c.bold("SUBCOMMANDS")}
|
|
|
64
65
|
${c.cyan("logout")} Clear saved API key
|
|
65
66
|
${c.cyan("balance")} Show plan + credit balance
|
|
66
67
|
${c.cyan("config")} show|set|set-base|path Manage config file
|
|
68
|
+
${c.cyan("mcp")} list|add|remove Manage MCP server connections
|
|
67
69
|
|
|
68
70
|
${c.bold("EXAMPLES")}
|
|
69
71
|
aether # interactive REPL
|
|
@@ -156,6 +158,10 @@ async function main() {
|
|
|
156
158
|
await handleBalance();
|
|
157
159
|
return;
|
|
158
160
|
}
|
|
161
|
+
if (sub === "mcp") {
|
|
162
|
+
await handleMcp(args._.slice(1));
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
159
165
|
|
|
160
166
|
const prompt = args._.join(" ").trim();
|
|
161
167
|
|
|
@@ -259,6 +265,95 @@ async function handleBalance() {
|
|
|
259
265
|
}
|
|
260
266
|
}
|
|
261
267
|
|
|
268
|
+
async function handleMcp(rest) {
|
|
269
|
+
const sub = (rest[0] || "list").toLowerCase();
|
|
270
|
+
|
|
271
|
+
if (sub === "list" || sub === "ls") {
|
|
272
|
+
const servers = listServers();
|
|
273
|
+
if (servers.length === 0) {
|
|
274
|
+
console.log(c.gray("No MCP servers configured."));
|
|
275
|
+
console.log(c.gray("Add one with:"));
|
|
276
|
+
console.log(c.gray(" aether mcp add <name> -- <command> [args...]"));
|
|
277
|
+
console.log(c.gray("Example:"));
|
|
278
|
+
console.log(
|
|
279
|
+
c.gray(' aether mcp add filesystem -- npx -y @modelcontextprotocol/server-filesystem /path'),
|
|
280
|
+
);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
console.log(c.bold(`Configured MCP servers (${servers.length}):`));
|
|
284
|
+
for (const [name, cfg] of servers) {
|
|
285
|
+
const argsStr = cfg.args && cfg.args.length > 0 ? " " + cfg.args.join(" ") : "";
|
|
286
|
+
console.log(` ${c.cyan(name)}: ${cfg.command}${argsStr}`);
|
|
287
|
+
if (cfg.env && Object.keys(cfg.env).length > 0) {
|
|
288
|
+
for (const [k, v] of Object.entries(cfg.env)) {
|
|
289
|
+
console.log(c.gray(` env ${k}=${v}`));
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (sub === "add") {
|
|
297
|
+
// Syntax: aether mcp add <name> [--env KEY=VAL]... -- <command> [args...]
|
|
298
|
+
const tail = rest.slice(1);
|
|
299
|
+
const dashIdx = tail.indexOf("--");
|
|
300
|
+
if (dashIdx === -1) {
|
|
301
|
+
die(
|
|
302
|
+
'Usage: aether mcp add <name> [--env KEY=VAL]... -- <command> [args...]\n' +
|
|
303
|
+
'Example: aether mcp add fs -- npx -y @modelcontextprotocol/server-filesystem /tmp',
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
const pre = tail.slice(0, dashIdx);
|
|
307
|
+
const post = tail.slice(dashIdx + 1);
|
|
308
|
+
const name = pre[0];
|
|
309
|
+
if (!name) die("aether mcp add: missing <name>");
|
|
310
|
+
if (post.length === 0) die("aether mcp add: missing <command> after '--'");
|
|
311
|
+
|
|
312
|
+
const env = {};
|
|
313
|
+
for (let i = 1; i < pre.length; i++) {
|
|
314
|
+
if (pre[i] === "--env") {
|
|
315
|
+
const kv = pre[++i];
|
|
316
|
+
if (!kv) die("--env needs a KEY=VAL argument");
|
|
317
|
+
const eq = kv.indexOf("=");
|
|
318
|
+
if (eq <= 0) die(`--env value must be KEY=VAL, got: ${kv}`);
|
|
319
|
+
env[kv.slice(0, eq)] = kv.slice(eq + 1);
|
|
320
|
+
} else {
|
|
321
|
+
die(`aether mcp add: unrecognized option "${pre[i]}" before the '--' separator`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const command = post[0];
|
|
326
|
+
const cmdArgs = post.slice(1);
|
|
327
|
+
try {
|
|
328
|
+
const entry = addServer({ name, command, args: cmdArgs, env });
|
|
329
|
+
console.log(`${c.green("✓")} Added MCP server "${c.cyan(name)}".`);
|
|
330
|
+
const argsStr = entry.args && entry.args.length > 0 ? " " + entry.args.join(" ") : "";
|
|
331
|
+
console.log(c.gray(` ${entry.command}${argsStr}`));
|
|
332
|
+
console.log(c.gray("Restart the agent (or run `aether`) to attach it."));
|
|
333
|
+
} catch (e) {
|
|
334
|
+
die(e.message || String(e));
|
|
335
|
+
}
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (sub === "remove" || sub === "rm" || sub === "delete") {
|
|
340
|
+
const name = rest[1];
|
|
341
|
+
if (!name) die("aether mcp remove: missing <name>");
|
|
342
|
+
try {
|
|
343
|
+
removeServer({ name });
|
|
344
|
+
console.log(`${c.green("✓")} Removed MCP server "${c.cyan(name)}".`);
|
|
345
|
+
} catch (e) {
|
|
346
|
+
die(e.message || String(e));
|
|
347
|
+
}
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
die(
|
|
352
|
+
`aether mcp: unknown subcommand "${sub}".\n` +
|
|
353
|
+
"Try one of: list, add, remove.",
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
|
|
262
357
|
main().catch((err) => {
|
|
263
358
|
console.error(errorLine(err.message || String(err)));
|
|
264
359
|
if (process.env.DEBUG) console.error(err);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aether-code",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Uncensored AI coding agent for your terminal. Type `aether` to launch the interactive REPL — like Claude Code, with no refusal layer.",
|
|
5
5
|
"homepage": "https://trynoguard.com",
|
|
6
6
|
"repository": {
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"node": ">=18"
|
|
23
23
|
},
|
|
24
24
|
"scripts": {
|
|
25
|
-
"lint": "node --check bin/aether-code.js src/agent.js src/api.js src/config.js src/render.js src/tools.js src/diff.js src/repl.js src/mcp.js",
|
|
25
|
+
"lint": "node --check bin/aether-code.js src/agent.js src/api.js src/config.js src/render.js src/tools.js src/diff.js src/repl.js src/mcp.js src/mcp-cli.js",
|
|
26
26
|
"test": "node --test \"test/**/*.test.js\""
|
|
27
27
|
},
|
|
28
28
|
"keywords": [
|
package/src/mcp-cli.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// `aether mcp` subcommand machinery: add / remove / list MCP servers
|
|
2
|
+
// in ~/.aether/mcp.json without the user hand-editing JSON.
|
|
3
|
+
//
|
|
4
|
+
// All read/write goes through this module so the validation (delegated to
|
|
5
|
+
// mcp.js) is consistent with what MCPManager will accept at runtime.
|
|
6
|
+
// configPath is injectable for tests; production code uses DEFAULT_PATH.
|
|
7
|
+
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import os from "node:os";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { validateMcpConfig } from "./mcp.js";
|
|
12
|
+
|
|
13
|
+
const DEFAULT_PATH = path.join(os.homedir(), ".aether", "mcp.json");
|
|
14
|
+
|
|
15
|
+
function resolvePath(opts) {
|
|
16
|
+
return opts?.configPath || DEFAULT_PATH;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function readConfig(opts) {
|
|
20
|
+
const p = resolvePath(opts);
|
|
21
|
+
if (!fs.existsSync(p)) return { mcpServers: {} };
|
|
22
|
+
let raw;
|
|
23
|
+
try {
|
|
24
|
+
raw = fs.readFileSync(p, "utf8");
|
|
25
|
+
} catch (e) {
|
|
26
|
+
throw new Error(`MCP config at ${p} unreadable: ${e.message}`);
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
const parsed = JSON.parse(raw);
|
|
30
|
+
if (!parsed.mcpServers || typeof parsed.mcpServers !== "object") {
|
|
31
|
+
return { ...parsed, mcpServers: {} };
|
|
32
|
+
}
|
|
33
|
+
return parsed;
|
|
34
|
+
} catch (e) {
|
|
35
|
+
throw new Error(`MCP config at ${p} is not valid JSON: ${e.message}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function writeConfig(config, opts) {
|
|
40
|
+
const p = resolvePath(opts);
|
|
41
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
42
|
+
// Pretty-print so users can read + hand-edit if they want, and trailing
|
|
43
|
+
// newline so diff tools don't bicker.
|
|
44
|
+
fs.writeFileSync(p, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Add a server entry. Mutates ~/.aether/mcp.json (creating it + the dir
|
|
49
|
+
* if missing). Validates the full resulting config via validateMcpConfig
|
|
50
|
+
* so a bad add can't poison the file with a config the runtime would
|
|
51
|
+
* later reject.
|
|
52
|
+
*/
|
|
53
|
+
export function addServer({ configPath, name, command, args = [], env = {} }) {
|
|
54
|
+
const cfg = readConfig({ configPath });
|
|
55
|
+
if (cfg.mcpServers[name]) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`MCP server "${name}" already configured. Remove it first with \`aether mcp remove ${name}\` or pick a different name.`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
const entry = { command };
|
|
61
|
+
if (args.length > 0) entry.args = args;
|
|
62
|
+
if (Object.keys(env).length > 0) entry.env = env;
|
|
63
|
+
const next = {
|
|
64
|
+
...cfg,
|
|
65
|
+
mcpServers: { ...cfg.mcpServers, [name]: entry },
|
|
66
|
+
};
|
|
67
|
+
validateMcpConfig(next, configPath || "<add>"); // throws on schema failure
|
|
68
|
+
writeConfig(next, { configPath });
|
|
69
|
+
return entry;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Remove a server entry by name. Throws if it doesn't exist (so the user
|
|
74
|
+
* notices typos instead of silently no-op'ing).
|
|
75
|
+
*/
|
|
76
|
+
export function removeServer({ configPath, name }) {
|
|
77
|
+
const cfg = readConfig({ configPath });
|
|
78
|
+
if (!cfg.mcpServers[name]) {
|
|
79
|
+
throw new Error(`MCP server "${name}" not configured.`);
|
|
80
|
+
}
|
|
81
|
+
const nextServers = { ...cfg.mcpServers };
|
|
82
|
+
delete nextServers[name];
|
|
83
|
+
const next = { ...cfg, mcpServers: nextServers };
|
|
84
|
+
writeConfig(next, { configPath });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Return [[name, entry], ...] for every configured server. Empty array
|
|
89
|
+
* when no config exists or mcpServers is empty.
|
|
90
|
+
*/
|
|
91
|
+
export function listServers(opts) {
|
|
92
|
+
const cfg = readConfig(opts);
|
|
93
|
+
return Object.entries(cfg.mcpServers);
|
|
94
|
+
}
|