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/src/repl.js CHANGED
@@ -1,247 +1,247 @@
1
- // Interactive REPL — Claude-CLI-style. Launched when `aether-code` is run
2
- // without a task argument.
3
- //
4
- // Behaviors:
5
- // - Banner on startup (version, model, balance, cwd, mode)
6
- // - Persistent message history across prompts within the session
7
- // - Slash commands: /help, /exit, /clear, /balance, /cwd, /yes, /turns
8
- // - Bottom status line is rendered after each turn
9
- // - Ctrl+C: first press cancels in-progress turn, second press exits
10
-
11
- import readline from "node:readline";
12
- import path from "node:path";
13
- import { runAgent } from "./agent.js";
14
- import { fetchBalance, AetherError } from "./api.js";
15
- import { runSetup } from "./setup.js";
16
- import { c, errorLine } from "./render.js";
17
-
18
- const VERSION = "0.2.0";
19
- const MODEL_NAME = "Aether Core";
20
-
21
- const SHORTCUTS = `
22
- ${c.cyan("/help")} Show this help
23
- ${c.cyan("/exit")} Exit (or Ctrl+C twice)
24
- ${c.cyan("/clear")} Clear conversation history (start fresh)
25
- ${c.cyan("/balance")} Refresh and show credit balance
26
- ${c.cyan("/cwd")} ${c.gray("[path]")} Show or change working directory
27
- ${c.cyan("/yes")} Toggle auto-approve mode (skip y/N prompts)
28
- ${c.cyan("/turns")} ${c.gray("<n>")} Set max turns per prompt (default 25)
29
- ${c.cyan("/model")} Show current model
30
-
31
- ${c.gray("Anything else is sent to the agent as your next message.")}
32
- ${c.gray("Conversation history is kept across messages until you /clear.")}
33
- `;
34
-
35
- export async function runRepl({ cwd: initialCwd, autoYes: initialAutoYes, maxTurns: initialMaxTurns, mcpManager = null }) {
36
- const state = {
37
- cwd: initialCwd,
38
- autoYes: !!initialAutoYes,
39
- maxTurns: initialMaxTurns ?? 25,
40
- messages: [], // accumulates across turns
41
- balance: null,
42
- sessionCredits: 0,
43
- sessionIn: 0,
44
- sessionOut: 0,
45
- };
46
-
47
- // Free balance check up front. If no key configured, walk through first-time
48
- // setup flow (open browser → paste key → verify → save).
49
- let needsSetup = false;
50
- try {
51
- const me = await fetchBalance();
52
- state.balance = me.balance;
53
- state.plan = me.plan;
54
- } catch (err) {
55
- if (err instanceof AetherError && (err.code === "NO_API_KEY" || err.status === 401)) {
56
- needsSetup = true;
57
- } else {
58
- // Transient network or other — surface but don't block
59
- console.log(c.gray(`(could not fetch balance: ${err.message})`));
60
- }
61
- }
62
-
63
- if (needsSetup) {
64
- const ok = await runSetup();
65
- if (!ok) {
66
- console.log(c.gray("Aborting — no valid API key."));
67
- process.exit(1);
68
- }
69
- // Re-fetch balance now that the key is saved
70
- try {
71
- const me = await fetchBalance();
72
- state.balance = me.balance;
73
- state.plan = me.plan;
74
- } catch { /* tolerate — banner just won't show balance */ }
75
- }
76
-
77
- printBanner(state);
78
-
79
- const rl = readline.createInterface({
80
- input: process.stdin,
81
- output: process.stdout,
82
- prompt: c.magenta("> "),
83
- historySize: 200,
84
- });
85
-
86
- // Two-stage Ctrl+C: first cancels current line, second exits
87
- let lastSigint = 0;
88
- rl.on("SIGINT", () => {
89
- const now = Date.now();
90
- if (now - lastSigint < 1500) {
91
- console.log(c.gray("\nbye."));
92
- rl.close();
93
- process.exit(0);
94
- }
95
- lastSigint = now;
96
- console.log(c.gray(`\n(Press Ctrl+C again within 1.5s to exit, or type ${c.cyan("/exit")})`));
97
- rl.prompt();
98
- });
99
-
100
- rl.prompt();
101
-
102
- for await (const rawLine of rl) {
103
- const line = rawLine.trim();
104
- if (!line) {
105
- rl.prompt();
106
- continue;
107
- }
108
-
109
- // Slash command?
110
- if (line.startsWith("/") || line === "?") {
111
- const handled = await handleSlash(line, state);
112
- if (handled === "exit") {
113
- rl.close();
114
- return;
115
- }
116
- printStatusLine(state);
117
- rl.prompt();
118
- continue;
119
- }
120
-
121
- // Otherwise — send as a message to the agent
122
- const result = await runAgent({
123
- initialPrompt: line,
124
- priorMessages: state.messages.length > 0 ? state.messages : undefined,
125
- cwd: state.cwd,
126
- autoYes: state.autoYes,
127
- maxTurns: state.maxTurns,
128
- mcpManager,
129
- });
130
-
131
- state.sessionCredits += result.totalCredits ?? 0;
132
- state.sessionIn += result.totalIn ?? 0;
133
- state.sessionOut += result.totalOut ?? 0;
134
- if (typeof result.balance === "number") state.balance = result.balance;
135
- if (result.messages) state.messages = result.messages;
136
-
137
- if (!result.ok && result.error) {
138
- console.log("\n" + errorLine(result.error.message || String(result.error)));
139
- // Don't kill the session on a single error — surface it and continue.
140
- }
141
-
142
- printStatusLine(state);
143
- rl.prompt();
144
- }
145
- }
146
-
147
- /* ───────── slash commands ───────── */
148
-
149
- async function handleSlash(line, state) {
150
- const [cmd, ...rest] = line.replace(/^\//, "").split(/\s+/);
151
- const arg = rest.join(" ").trim();
152
-
153
- switch ((cmd || "").toLowerCase()) {
154
- case "":
155
- case "help":
156
- case "?":
157
- console.log(SHORTCUTS);
158
- break;
159
- case "exit":
160
- case "quit":
161
- case "q":
162
- console.log(c.gray("bye."));
163
- return "exit";
164
- case "clear":
165
- state.messages = [];
166
- state.sessionCredits = 0;
167
- state.sessionIn = 0;
168
- state.sessionOut = 0;
169
- console.log(c.gray("Conversation cleared."));
170
- break;
171
- case "balance": {
172
- try {
173
- const me = await fetchBalance();
174
- state.balance = me.balance;
175
- state.plan = me.plan;
176
- console.log(
177
- c.cyan(`balance: ${me.balance.toLocaleString()} credits`) +
178
- c.gray(` · plan: ${me.plan} (${me.planCredits} plan + ${me.topupCredits} topup)`),
179
- );
180
- } catch (err) {
181
- console.log(errorLine(err.message || String(err)));
182
- }
183
- break;
184
- }
185
- case "cwd":
186
- if (arg) {
187
- const next = path.resolve(arg);
188
- try {
189
- process.chdir(next);
190
- state.cwd = next;
191
- console.log(c.gray(`cwd → ${state.cwd}`));
192
- } catch (err) {
193
- console.log(errorLine(err.message));
194
- }
195
- } else {
196
- console.log(c.gray(`cwd: ${state.cwd}`));
197
- }
198
- break;
199
- case "yes":
200
- state.autoYes = !state.autoYes;
201
- console.log(c.gray(`auto-yes: ${state.autoYes ? "on (writes/shells will skip y/N)" : "off"}`));
202
- break;
203
- case "turns": {
204
- const n = parseInt(arg, 10);
205
- if (!Number.isFinite(n) || n < 1 || n > 200) {
206
- console.log(errorLine(`/turns expects 1..200, got "${arg}"`));
207
- } else {
208
- state.maxTurns = n;
209
- console.log(c.gray(`max turns: ${state.maxTurns}`));
210
- }
211
- break;
212
- }
213
- case "model":
214
- console.log(c.gray(`model: ${MODEL_NAME} · 1M context · uncensored`));
215
- break;
216
- default:
217
- console.log(errorLine(`Unknown command: /${cmd}. Type ${c.cyan("/help")} for shortcuts.`));
218
- }
219
- return null;
220
- }
221
-
222
- /* ───────── banner + status ───────── */
223
-
224
- function printBanner(state) {
225
- console.log("");
226
- console.log(c.bold(c.magenta(`aether-code`)) + c.gray(` v${VERSION}`));
227
- console.log(
228
- c.gray(`${MODEL_NAME} (1M context) · uncensored · `) +
229
- c.cyan(state.autoYes ? "auto-yes" : "review mode") +
230
- (state.balance != null ? c.gray(` · ${state.balance.toLocaleString()} credits`) : ""),
231
- );
232
- console.log(c.gray(state.cwd));
233
- console.log("");
234
- console.log(c.gray(`Type ${c.cyan("/help")} for shortcuts. ${c.cyan("/exit")} or Ctrl+C twice to quit.`));
235
- console.log("");
236
- }
237
-
238
- function printStatusLine(state) {
239
- const parts = [];
240
- parts.push(c.gray(`session: ${state.sessionCredits} cr · ${state.sessionIn}→${state.sessionOut} tokens`));
241
- if (state.balance != null) parts.push(c.gray(`balance: ${state.balance.toLocaleString()}`));
242
- if (state.messages.length > 0) {
243
- parts.push(c.gray(`history: ${state.messages.length} msg${state.messages.length === 1 ? "" : "s"}`));
244
- }
245
- parts.push(c.cyan(state.autoYes ? "auto-yes" : "review"));
246
- console.log(c.dim(parts.join(" · ")));
247
- }
1
+ // Interactive REPL — Claude-CLI-style. Launched when `aether-code` is run
2
+ // without a task argument.
3
+ //
4
+ // Behaviors:
5
+ // - Banner on startup (version, model, balance, cwd, mode)
6
+ // - Persistent message history across prompts within the session
7
+ // - Slash commands: /help, /exit, /clear, /balance, /cwd, /yes, /turns
8
+ // - Bottom status line is rendered after each turn
9
+ // - Ctrl+C: first press cancels in-progress turn, second press exits
10
+
11
+ import readline from "node:readline";
12
+ import path from "node:path";
13
+ import { runAgent } from "./agent.js";
14
+ import { fetchBalance, AetherError } from "./api.js";
15
+ import { runSetup } from "./setup.js";
16
+ import { c, errorLine } from "./render.js";
17
+
18
+ const VERSION = "0.13.0";
19
+ const MODEL_NAME = "Aether Core";
20
+
21
+ const SHORTCUTS = `
22
+ ${c.cyan("/help")} Show this help
23
+ ${c.cyan("/exit")} Exit (or Ctrl+C twice)
24
+ ${c.cyan("/clear")} Clear conversation history (start fresh)
25
+ ${c.cyan("/balance")} Refresh and show credit balance
26
+ ${c.cyan("/cwd")} ${c.gray("[path]")} Show or change working directory
27
+ ${c.cyan("/yes")} Toggle auto-approve mode (skip y/N prompts)
28
+ ${c.cyan("/turns")} ${c.gray("<n>")} Set max turns per prompt (default 25)
29
+ ${c.cyan("/model")} Show current model
30
+
31
+ ${c.gray("Anything else is sent to the agent as your next message.")}
32
+ ${c.gray("Conversation history is kept across messages until you /clear.")}
33
+ `;
34
+
35
+ export async function runRepl({ cwd: initialCwd, autoYes: initialAutoYes, maxTurns: initialMaxTurns, mcpManager = null }) {
36
+ const state = {
37
+ cwd: initialCwd,
38
+ autoYes: !!initialAutoYes,
39
+ maxTurns: initialMaxTurns ?? 25,
40
+ messages: [], // accumulates across turns
41
+ balance: null,
42
+ sessionCredits: 0,
43
+ sessionIn: 0,
44
+ sessionOut: 0,
45
+ };
46
+
47
+ // Free balance check up front. If no key configured, walk through first-time
48
+ // setup flow (open browser → paste key → verify → save).
49
+ let needsSetup = false;
50
+ try {
51
+ const me = await fetchBalance();
52
+ state.balance = me.balance;
53
+ state.plan = me.plan;
54
+ } catch (err) {
55
+ if (err instanceof AetherError && (err.code === "NO_API_KEY" || err.status === 401)) {
56
+ needsSetup = true;
57
+ } else {
58
+ // Transient network or other — surface but don't block
59
+ console.log(c.gray(`(could not fetch balance: ${err.message})`));
60
+ }
61
+ }
62
+
63
+ if (needsSetup) {
64
+ const ok = await runSetup();
65
+ if (!ok) {
66
+ console.log(c.gray("Aborting — no valid API key."));
67
+ process.exit(1);
68
+ }
69
+ // Re-fetch balance now that the key is saved
70
+ try {
71
+ const me = await fetchBalance();
72
+ state.balance = me.balance;
73
+ state.plan = me.plan;
74
+ } catch { /* tolerate — banner just won't show balance */ }
75
+ }
76
+
77
+ printBanner(state);
78
+
79
+ const rl = readline.createInterface({
80
+ input: process.stdin,
81
+ output: process.stdout,
82
+ prompt: c.magenta("> "),
83
+ historySize: 200,
84
+ });
85
+
86
+ // Two-stage Ctrl+C: first cancels current line, second exits
87
+ let lastSigint = 0;
88
+ rl.on("SIGINT", () => {
89
+ const now = Date.now();
90
+ if (now - lastSigint < 1500) {
91
+ console.log(c.gray("\nbye."));
92
+ rl.close();
93
+ process.exit(0);
94
+ }
95
+ lastSigint = now;
96
+ console.log(c.gray(`\n(Press Ctrl+C again within 1.5s to exit, or type ${c.cyan("/exit")})`));
97
+ rl.prompt();
98
+ });
99
+
100
+ rl.prompt();
101
+
102
+ for await (const rawLine of rl) {
103
+ const line = rawLine.trim();
104
+ if (!line) {
105
+ rl.prompt();
106
+ continue;
107
+ }
108
+
109
+ // Slash command?
110
+ if (line.startsWith("/") || line === "?") {
111
+ const handled = await handleSlash(line, state);
112
+ if (handled === "exit") {
113
+ rl.close();
114
+ return;
115
+ }
116
+ printStatusLine(state);
117
+ rl.prompt();
118
+ continue;
119
+ }
120
+
121
+ // Otherwise — send as a message to the agent
122
+ const result = await runAgent({
123
+ initialPrompt: line,
124
+ priorMessages: state.messages.length > 0 ? state.messages : undefined,
125
+ cwd: state.cwd,
126
+ autoYes: state.autoYes,
127
+ maxTurns: state.maxTurns,
128
+ mcpManager,
129
+ });
130
+
131
+ state.sessionCredits += result.totalCredits ?? 0;
132
+ state.sessionIn += result.totalIn ?? 0;
133
+ state.sessionOut += result.totalOut ?? 0;
134
+ if (typeof result.balance === "number") state.balance = result.balance;
135
+ if (result.messages) state.messages = result.messages;
136
+
137
+ if (!result.ok && result.error) {
138
+ console.log("\n" + errorLine(result.error.message || String(result.error)));
139
+ // Don't kill the session on a single error — surface it and continue.
140
+ }
141
+
142
+ printStatusLine(state);
143
+ rl.prompt();
144
+ }
145
+ }
146
+
147
+ /* ───────── slash commands ───────── */
148
+
149
+ async function handleSlash(line, state) {
150
+ const [cmd, ...rest] = line.replace(/^\//, "").split(/\s+/);
151
+ const arg = rest.join(" ").trim();
152
+
153
+ switch ((cmd || "").toLowerCase()) {
154
+ case "":
155
+ case "help":
156
+ case "?":
157
+ console.log(SHORTCUTS);
158
+ break;
159
+ case "exit":
160
+ case "quit":
161
+ case "q":
162
+ console.log(c.gray("bye."));
163
+ return "exit";
164
+ case "clear":
165
+ state.messages = [];
166
+ state.sessionCredits = 0;
167
+ state.sessionIn = 0;
168
+ state.sessionOut = 0;
169
+ console.log(c.gray("Conversation cleared."));
170
+ break;
171
+ case "balance": {
172
+ try {
173
+ const me = await fetchBalance();
174
+ state.balance = me.balance;
175
+ state.plan = me.plan;
176
+ console.log(
177
+ c.cyan(`balance: ${me.balance.toLocaleString()} credits`) +
178
+ c.gray(` · plan: ${me.plan} (${me.planCredits} plan + ${me.topupCredits} topup)`),
179
+ );
180
+ } catch (err) {
181
+ console.log(errorLine(err.message || String(err)));
182
+ }
183
+ break;
184
+ }
185
+ case "cwd":
186
+ if (arg) {
187
+ const next = path.resolve(arg);
188
+ try {
189
+ process.chdir(next);
190
+ state.cwd = next;
191
+ console.log(c.gray(`cwd → ${state.cwd}`));
192
+ } catch (err) {
193
+ console.log(errorLine(err.message));
194
+ }
195
+ } else {
196
+ console.log(c.gray(`cwd: ${state.cwd}`));
197
+ }
198
+ break;
199
+ case "yes":
200
+ state.autoYes = !state.autoYes;
201
+ console.log(c.gray(`auto-yes: ${state.autoYes ? "on (writes/shells will skip y/N)" : "off"}`));
202
+ break;
203
+ case "turns": {
204
+ const n = parseInt(arg, 10);
205
+ if (!Number.isFinite(n) || n < 1 || n > 200) {
206
+ console.log(errorLine(`/turns expects 1..200, got "${arg}"`));
207
+ } else {
208
+ state.maxTurns = n;
209
+ console.log(c.gray(`max turns: ${state.maxTurns}`));
210
+ }
211
+ break;
212
+ }
213
+ case "model":
214
+ console.log(c.gray(`model: ${MODEL_NAME} · 1M context · uncensored`));
215
+ break;
216
+ default:
217
+ console.log(errorLine(`Unknown command: /${cmd}. Type ${c.cyan("/help")} for shortcuts.`));
218
+ }
219
+ return null;
220
+ }
221
+
222
+ /* ───────── banner + status ───────── */
223
+
224
+ function printBanner(state) {
225
+ console.log("");
226
+ console.log(c.bold(c.magenta(`aether-code`)) + c.gray(` v${VERSION}`));
227
+ console.log(
228
+ c.gray(`${MODEL_NAME} (1M context) · uncensored · `) +
229
+ c.cyan(state.autoYes ? "auto-yes" : "review mode") +
230
+ (state.balance != null ? c.gray(` · ${state.balance.toLocaleString()} credits`) : ""),
231
+ );
232
+ console.log(c.gray(state.cwd));
233
+ console.log("");
234
+ console.log(c.gray(`Type ${c.cyan("/help")} for shortcuts. ${c.cyan("/exit")} or Ctrl+C twice to quit.`));
235
+ console.log("");
236
+ }
237
+
238
+ function printStatusLine(state) {
239
+ const parts = [];
240
+ parts.push(c.gray(`session: ${state.sessionCredits} cr · ${state.sessionIn}→${state.sessionOut} tokens`));
241
+ if (state.balance != null) parts.push(c.gray(`balance: ${state.balance.toLocaleString()}`));
242
+ if (state.messages.length > 0) {
243
+ parts.push(c.gray(`history: ${state.messages.length} msg${state.messages.length === 1 ? "" : "s"}`));
244
+ }
245
+ parts.push(c.cyan(state.autoYes ? "auto-yes" : "review"));
246
+ console.log(c.dim(parts.join(" · ")));
247
+ }