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.
@@ -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.9.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.9.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
+ }