aether-code 0.43.0 → 0.43.1

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/src/ink-input.js CHANGED
@@ -1,91 +1,91 @@
1
- // Ink-based boxed input for the REPL — the bordered live input box (Claude
2
- // Code-style). Rendered fresh for each prompt, then unmounted so the agent's
3
- // plain console.log streaming during a turn never fights Ink for the terminal.
4
- //
5
- // Uses a `single`-line border (broadly supported in cmd.exe Consolas) rather
6
- // than rounded corners, which render as tofu on some Windows fonts.
7
- //
8
- // No JSX / no build step: components are built with React.createElement so the
9
- // package ships as-is. `InputApp` is exported (pure UI + callbacks) so it can be
10
- // unit-tested with ink-testing-library without a real TTY.
11
-
12
- import React, { useState, useRef } from "react";
13
- import { render, Box, Text, useApp, useInput } from "ink";
14
- import TextInput from "ink-text-input";
15
-
16
- const h = React.createElement;
17
-
18
- // Sentinel promptBoxed() resolves with on a double Ctrl+C, so the caller can
19
- // distinguish "user wants to quit" from an empty submitted line. Plain ASCII so
20
- // it can never be confused with a real keystroke or hide invisible chars.
21
- export const EXIT_SIGNAL = "<<aether-exit>>";
22
-
23
- /**
24
- * Pure input UI. Calls onSubmit(value) on Enter and onExit() on a double Ctrl+C.
25
- * Up/Down arrows walk `history` (newest last). No process/terminal side effects
26
- * of its own, so it's testable in isolation with ink-testing-library.
27
- */
28
- export function InputApp({ onSubmit, onExit, statusLeft = "", statusRight = "", history = [] }) {
29
- const [value, setValue] = useState("");
30
- const lastCtrlC = useRef(0);
31
- const histIdx = useRef(history.length);
32
-
33
- useInput((input, key) => {
34
- if (key.ctrl && input === "c") {
35
- const now = Date.now();
36
- if (now - lastCtrlC.current < 1500) onExit();
37
- else { lastCtrlC.current = now; setValue(""); }
38
- return;
39
- }
40
- if (key.upArrow) {
41
- if (histIdx.current > 0) { histIdx.current -= 1; setValue(history[histIdx.current] ?? ""); }
42
- } else if (key.downArrow) {
43
- if (histIdx.current < history.length - 1) { histIdx.current += 1; setValue(history[histIdx.current] ?? ""); }
44
- else { histIdx.current = history.length; setValue(""); }
45
- }
46
- });
47
-
48
- return h(
49
- Box,
50
- { flexDirection: "column", width: "100%" },
51
- h(
52
- Box,
53
- { borderStyle: "single", borderColor: "magenta", paddingX: 1 },
54
- h(Text, { color: "magenta", bold: true }, "> "),
55
- h(TextInput, { value, onChange: setValue, onSubmit, placeholder: "" }),
56
- ),
57
- h(
58
- Box,
59
- { paddingX: 1, width: "100%", justifyContent: "space-between" },
60
- h(Text, { dimColor: true }, statusLeft),
61
- h(Text, { dimColor: true }, statusRight),
62
- ),
63
- );
64
- }
65
-
66
- /**
67
- * Render a one-shot boxed input and resolve with the submitted line. Resolves
68
- * EXIT_SIGNAL on a double Ctrl+C; resolves "" if the app exits without a submit.
69
- */
70
- export function promptBoxed(opts = {}) {
71
- return new Promise((resolve) => {
72
- let done = false;
73
- let appApi = null;
74
- const fin = (val) => {
75
- if (done) return;
76
- done = true;
77
- try { if (appApi) appApi.exit(); } catch { /* already exiting */ }
78
- resolve(val);
79
- };
80
- const Wrapper = () => {
81
- appApi = useApp();
82
- return h(InputApp, {
83
- ...opts,
84
- onSubmit: (v) => fin(v),
85
- onExit: () => fin(EXIT_SIGNAL),
86
- });
87
- };
88
- const instance = render(h(Wrapper), { exitOnCtrlC: false });
89
- instance.waitUntilExit().then(() => { if (!done) { done = true; resolve(""); } });
90
- });
91
- }
1
+ // Ink-based boxed input for the REPL — the bordered live input box (Claude
2
+ // Code-style). Rendered fresh for each prompt, then unmounted so the agent's
3
+ // plain console.log streaming during a turn never fights Ink for the terminal.
4
+ //
5
+ // Uses a `single`-line border (broadly supported in cmd.exe Consolas) rather
6
+ // than rounded corners, which render as tofu on some Windows fonts.
7
+ //
8
+ // No JSX / no build step: components are built with React.createElement so the
9
+ // package ships as-is. `InputApp` is exported (pure UI + callbacks) so it can be
10
+ // unit-tested with ink-testing-library without a real TTY.
11
+
12
+ import React, { useState, useRef } from "react";
13
+ import { render, Box, Text, useApp, useInput } from "ink";
14
+ import TextInput from "ink-text-input";
15
+
16
+ const h = React.createElement;
17
+
18
+ // Sentinel promptBoxed() resolves with on a double Ctrl+C, so the caller can
19
+ // distinguish "user wants to quit" from an empty submitted line. Plain ASCII so
20
+ // it can never be confused with a real keystroke or hide invisible chars.
21
+ export const EXIT_SIGNAL = "<<aether-exit>>";
22
+
23
+ /**
24
+ * Pure input UI. Calls onSubmit(value) on Enter and onExit() on a double Ctrl+C.
25
+ * Up/Down arrows walk `history` (newest last). No process/terminal side effects
26
+ * of its own, so it's testable in isolation with ink-testing-library.
27
+ */
28
+ export function InputApp({ onSubmit, onExit, statusLeft = "", statusRight = "", history = [] }) {
29
+ const [value, setValue] = useState("");
30
+ const lastCtrlC = useRef(0);
31
+ const histIdx = useRef(history.length);
32
+
33
+ useInput((input, key) => {
34
+ if (key.ctrl && input === "c") {
35
+ const now = Date.now();
36
+ if (now - lastCtrlC.current < 1500) onExit();
37
+ else { lastCtrlC.current = now; setValue(""); }
38
+ return;
39
+ }
40
+ if (key.upArrow) {
41
+ if (histIdx.current > 0) { histIdx.current -= 1; setValue(history[histIdx.current] ?? ""); }
42
+ } else if (key.downArrow) {
43
+ if (histIdx.current < history.length - 1) { histIdx.current += 1; setValue(history[histIdx.current] ?? ""); }
44
+ else { histIdx.current = history.length; setValue(""); }
45
+ }
46
+ });
47
+
48
+ return h(
49
+ Box,
50
+ { flexDirection: "column", width: "100%" },
51
+ h(
52
+ Box,
53
+ { borderStyle: "single", borderColor: "magenta", paddingX: 1 },
54
+ h(Text, { color: "magenta", bold: true }, "> "),
55
+ h(TextInput, { value, onChange: setValue, onSubmit, placeholder: "" }),
56
+ ),
57
+ h(
58
+ Box,
59
+ { paddingX: 1, width: "100%", justifyContent: "space-between" },
60
+ h(Text, { dimColor: true }, statusLeft),
61
+ h(Text, { dimColor: true }, statusRight),
62
+ ),
63
+ );
64
+ }
65
+
66
+ /**
67
+ * Render a one-shot boxed input and resolve with the submitted line. Resolves
68
+ * EXIT_SIGNAL on a double Ctrl+C; resolves "" if the app exits without a submit.
69
+ */
70
+ export function promptBoxed(opts = {}) {
71
+ return new Promise((resolve) => {
72
+ let done = false;
73
+ let appApi = null;
74
+ const fin = (val) => {
75
+ if (done) return;
76
+ done = true;
77
+ try { if (appApi) appApi.exit(); } catch { /* already exiting */ }
78
+ resolve(val);
79
+ };
80
+ const Wrapper = () => {
81
+ appApi = useApp();
82
+ return h(InputApp, {
83
+ ...opts,
84
+ onSubmit: (v) => fin(v),
85
+ onExit: () => fin(EXIT_SIGNAL),
86
+ });
87
+ };
88
+ const instance = render(h(Wrapper), { exitOnCtrlC: false });
89
+ instance.waitUntilExit().then(() => { if (!done) { done = true; resolve(""); } });
90
+ });
91
+ }
package/src/mcp-cli.js CHANGED
@@ -1,94 +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
- }
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
+ }