aether-code 0.12.0 → 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/bin/aether-code.js +457 -457
- 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/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/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.
|
|
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
|
+
}
|