aether-code 0.42.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/LICENSE +21 -21
- package/README.md +2 -0
- package/bin/aether-code.js +515 -513
- package/package.json +77 -75
- package/scripts/postinstall.js +182 -0
- package/skills/adult-creative-writing.md +60 -60
- package/skills/debugging.md +51 -51
- package/skills/game-modding.md +73 -73
- package/skills/reverse-engineering.md +41 -41
- package/skills/scraping-automation.md +77 -77
- package/skills/security-research.md +67 -67
- package/src/agent.js +170 -219
- package/src/api.js +22 -32
- package/src/box-input.js +141 -36
- package/src/config.js +38 -38
- package/src/diff.js +49 -49
- package/src/ink-input.js +91 -91
- package/src/mcp-cli.js +94 -94
- package/src/mcp-registry.js +266 -266
- package/src/mcp.js +259 -260
- package/src/menu.js +83 -83
- package/src/render.js +141 -107
- package/src/repl.js +221 -257
- package/src/setup.js +139 -141
- package/src/tools.js +911 -1288
- package/src/update-check.js +64 -7
- package/src/project-context.js +0 -707
- package/src/sessions.js +0 -167
- package/src/version.js +0 -15
package/src/repl.js
CHANGED
|
@@ -1,34 +1,17 @@
|
|
|
1
1
|
// Interactive REPL — Claude-CLI-style. Launched when `aether-code` is run
|
|
2
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 fs from "node:fs";
|
|
3
|
+
|
|
12
4
|
import path from "node:path";
|
|
13
5
|
import { runAgent } from "./agent.js";
|
|
14
6
|
import { fetchBalance, AetherError } from "./api.js";
|
|
15
7
|
import { runSetup } from "./setup.js";
|
|
16
|
-
import { c, errorLine,
|
|
17
|
-
import {
|
|
8
|
+
import { c, errorLine, turnStats } from "./render.js";
|
|
9
|
+
import { autoUpdate } from "./update-check.js";
|
|
18
10
|
import { promptBox, EXIT_SIGNAL } from "./box-input.js";
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
generateSessionId, saveSession, loadSession, listSessions,
|
|
22
|
-
findRecentSession, deleteSession, extractSummary, formatAge,
|
|
23
|
-
} from "./sessions.js";
|
|
24
|
-
import { scanProjectQuick, generateProjectIndex } from "./project-context.js";
|
|
25
|
-
|
|
26
|
-
const VERSION = getVersion();
|
|
11
|
+
|
|
12
|
+
const VERSION = "0.43.1";
|
|
27
13
|
const MODEL_NAME = "Aether Core";
|
|
28
14
|
|
|
29
|
-
// /model aliases → { id, name }. id=null = server default (gemma / Aether Core).
|
|
30
|
-
// Premium models are gated SERVER-SIDE: if you haven't purchased credits, the
|
|
31
|
-
// server transparently uses Core instead — selecting one here can't bypass that.
|
|
32
15
|
const MODEL_ALIASES = {
|
|
33
16
|
core: { id: null, name: "Aether Core" },
|
|
34
17
|
gemma: { id: null, name: "Aether Core" },
|
|
@@ -37,23 +20,51 @@ const MODEL_ALIASES = {
|
|
|
37
20
|
};
|
|
38
21
|
|
|
39
22
|
const SHORTCUTS = `
|
|
40
|
-
${c.cyan("/help")}
|
|
41
|
-
${c.cyan("/exit")}
|
|
42
|
-
${c.cyan("/clear")}
|
|
43
|
-
${c.cyan("/balance")}
|
|
44
|
-
${c.cyan("/cwd")} ${c.gray("[path]")}
|
|
45
|
-
${c.cyan("/yes")}
|
|
46
|
-
${c.cyan("/turns")} ${c.gray("<n>")}
|
|
47
|
-
${c.cyan("/model")} ${c.gray("[
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
${c.cyan("/resume")} ${c.gray("[id]")} Resume a saved session (default: most recent)
|
|
51
|
-
${c.cyan("/new")} Start a fresh session (save current first)
|
|
52
|
-
|
|
53
|
-
${c.gray("Anything else is sent to the agent as your next message.")}
|
|
54
|
-
${c.gray("Sessions auto-save — close the terminal and come back later.")}
|
|
23
|
+
${c.cyan("/help")} Show this help
|
|
24
|
+
${c.cyan("/exit")} Exit (or Ctrl+C twice)
|
|
25
|
+
${c.cyan("/clear")} Clear conversation history
|
|
26
|
+
${c.cyan("/balance")} Show credit balance
|
|
27
|
+
${c.cyan("/cwd")} ${c.gray("[path]")} Show or change working directory
|
|
28
|
+
${c.cyan("/yes")} Toggle auto-accept mode (skip confirmations)
|
|
29
|
+
${c.cyan("/turns")} ${c.gray("<n>")} Set max agent turns per prompt (default 25)
|
|
30
|
+
${c.cyan("/model")} ${c.gray("[name]")} Switch model ${c.gray("(core | ultra)")}
|
|
31
|
+
|
|
32
|
+
${c.dim("Anything else is sent as your next message. History persists until /clear.")}
|
|
55
33
|
`;
|
|
56
34
|
|
|
35
|
+
const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
36
|
+
|
|
37
|
+
function getVisibleRows() {
|
|
38
|
+
let rows = process.stdout.rows || 24;
|
|
39
|
+
if (process.platform === "win32" && !process.env.WT_SESSION && rows > 60) {
|
|
40
|
+
rows = 24;
|
|
41
|
+
}
|
|
42
|
+
return rows;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function pushPromptToBottom(contentLines) {
|
|
46
|
+
const visibleRows = getVisibleRows();
|
|
47
|
+
const promptHeight = 4;
|
|
48
|
+
const gap = Math.max(0, visibleRows - contentLines - promptHeight);
|
|
49
|
+
if (gap > 0) process.stdout.write("\n".repeat(gap));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function drawStaticPrompt(startRow, state) {
|
|
53
|
+
const termCols = process.stdout.columns || 80;
|
|
54
|
+
const ruleStr = `\x1b[36m${"─".repeat(Math.max(0, termCols))}\x1b[0m`;
|
|
55
|
+
const modeStr = state.autoYes ? "auto-accept" : "confirm writes";
|
|
56
|
+
const blkFn = (code) => `\x1b[${code}m \x1b[0m`;
|
|
57
|
+
const statusBar = blkFn("48;2;209;107;52") + blkFn("48;2;0;178;169") +
|
|
58
|
+
`\x1b[2m ${modeStr} \xB7 /help for shortcuts \xB7 @file to include\x1b[0m`;
|
|
59
|
+
process.stdout.write(
|
|
60
|
+
`\x1b[${startRow};1H` +
|
|
61
|
+
ruleStr + "\n" +
|
|
62
|
+
`\x1b[2m> \x1b[0m` + "\n" +
|
|
63
|
+
ruleStr + "\n" +
|
|
64
|
+
statusBar,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
57
68
|
export async function runRepl({ cwd: initialCwd, autoYes: initialAutoYes, unsafePaths: initialUnsafePaths, maxTurns: initialMaxTurns, model: initialModel = null, mcpManager = null }) {
|
|
58
69
|
const initialAlias = Object.values(MODEL_ALIASES).find((a) => a.id === initialModel);
|
|
59
70
|
const state = {
|
|
@@ -68,17 +79,15 @@ export async function runRepl({ cwd: initialCwd, autoYes: initialAutoYes, unsafe
|
|
|
68
79
|
sessionCredits: 0,
|
|
69
80
|
sessionIn: 0,
|
|
70
81
|
sessionOut: 0,
|
|
71
|
-
sessionId: generateSessionId(initialCwd),
|
|
72
|
-
turnCount: 0,
|
|
73
82
|
};
|
|
74
83
|
|
|
75
|
-
const updatePromise =
|
|
84
|
+
const updatePromise = autoUpdate().catch(() => null);
|
|
76
85
|
|
|
77
86
|
let needsSetup = false;
|
|
78
87
|
try {
|
|
79
88
|
const me = await fetchBalance();
|
|
80
89
|
state.balance = me.balance;
|
|
81
|
-
state.plan = me.plan;
|
|
90
|
+
state.plan = me.plan || "FREE";
|
|
82
91
|
} catch (err) {
|
|
83
92
|
if (err instanceof AetherError && (err.code === "NO_API_KEY" || err.status === 401)) {
|
|
84
93
|
needsSetup = true;
|
|
@@ -96,54 +105,33 @@ export async function runRepl({ cwd: initialCwd, autoYes: initialAutoYes, unsafe
|
|
|
96
105
|
try {
|
|
97
106
|
const me = await fetchBalance();
|
|
98
107
|
state.balance = me.balance;
|
|
99
|
-
state.plan = me.plan;
|
|
108
|
+
state.plan = me.plan || "FREE";
|
|
100
109
|
} catch { /* tolerate */ }
|
|
101
110
|
}
|
|
102
111
|
|
|
103
|
-
//
|
|
104
|
-
|
|
105
|
-
try {
|
|
106
|
-
const recent = findRecentSession(state.cwd);
|
|
107
|
-
if (recent && recent.messageCount >= 2) {
|
|
108
|
-
const full = loadSession(recent.id);
|
|
109
|
-
if (full && full.messages?.length >= 2) {
|
|
110
|
-
console.log(
|
|
111
|
-
c.gray(`\n Found session from ${formatAge(recent.updatedAt)} — `) +
|
|
112
|
-
c.cyan(extractSummary(full.messages) || "(no summary)"),
|
|
113
|
-
);
|
|
114
|
-
console.log(c.gray(" Resuming automatically. Use /new to start fresh.\n"));
|
|
115
|
-
state.sessionId = full.id;
|
|
116
|
-
state.messages = full.messages;
|
|
117
|
-
state.sessionCredits = full.credits ?? 0;
|
|
118
|
-
state.sessionIn = full.tokensIn ?? 0;
|
|
119
|
-
state.sessionOut = full.tokensOut ?? 0;
|
|
120
|
-
state.turnCount = full.turnCount ?? 0;
|
|
121
|
-
if (full.model !== undefined) state.model = full.model;
|
|
122
|
-
if (full.modelName) state.modelName = full.modelName;
|
|
123
|
-
resumed = true;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
} catch { /* non-fatal */ }
|
|
112
|
+
// Clear screen — clean start, no leftover shell text (matches Claude Code)
|
|
113
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
127
114
|
|
|
128
|
-
|
|
129
|
-
let projectLine = "";
|
|
130
|
-
try {
|
|
131
|
-
projectLine = scanProjectQuick(state.cwd);
|
|
132
|
-
} catch { /* non-fatal */ }
|
|
115
|
+
const bannerLineCount = printBanner(state);
|
|
133
116
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (
|
|
117
|
+
const updateMsg = await updatePromise;
|
|
118
|
+
let extraLines = 0;
|
|
119
|
+
if (updateMsg) { console.log(updateMsg); extraLines++; }
|
|
120
|
+
|
|
121
|
+
pushPromptToBottom(bannerLineCount + extraLines);
|
|
137
122
|
|
|
138
123
|
const inputHistory = [];
|
|
139
124
|
|
|
125
|
+
const handlePromptResize = () => {
|
|
126
|
+
const vr = getVisibleRows();
|
|
127
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
128
|
+
printBanner(state);
|
|
129
|
+
process.stdout.write(`\x1b[${Math.max(1, vr - 3)};1H`);
|
|
130
|
+
};
|
|
131
|
+
|
|
140
132
|
while (true) {
|
|
141
|
-
const raw = await promptBox({ history: inputHistory });
|
|
142
|
-
if (raw === EXIT_SIGNAL) {
|
|
143
|
-
autoSave(state);
|
|
144
|
-
console.log(c.gray("bye."));
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
133
|
+
const raw = await promptBox({ history: inputHistory, mode: state.autoYes ? "auto-yes" : "review", suppressEcho: true, onResize: handlePromptResize });
|
|
134
|
+
if (raw === EXIT_SIGNAL) { console.log(c.gray("bye.")); return; }
|
|
147
135
|
const line = (raw ?? "").trim();
|
|
148
136
|
if (!line) continue;
|
|
149
137
|
|
|
@@ -151,25 +139,71 @@ export async function runRepl({ cwd: initialCwd, autoYes: initialAutoYes, unsafe
|
|
|
151
139
|
|
|
152
140
|
if (line.startsWith("/") || line === "?") {
|
|
153
141
|
const handled = await handleSlash(line, state);
|
|
154
|
-
if (handled === "exit")
|
|
155
|
-
autoSave(state);
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
printStatusLine(state);
|
|
142
|
+
if (handled === "exit") return;
|
|
159
143
|
continue;
|
|
160
144
|
}
|
|
161
145
|
|
|
162
|
-
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
146
|
+
// Build banner + echo as one buffer, write atomically (no flash, no garbling)
|
|
147
|
+
const bLines = buildBannerLines(state);
|
|
148
|
+
const echoDisplay = line.length > 200 ? line.slice(0, 197) + "..." : line;
|
|
149
|
+
const echoCols = process.stdout.columns || 80;
|
|
150
|
+
const echoLen = echoDisplay.length + 2;
|
|
151
|
+
const echoPad = Math.max(0, echoCols - echoLen);
|
|
152
|
+
const echoBar = `\x1b[48;5;236m ${echoDisplay}${" ".repeat(echoPad)}\x1b[0m`;
|
|
153
|
+
|
|
154
|
+
const vr = getVisibleRows();
|
|
155
|
+
let scrollEnd = Math.max(4, vr - 4);
|
|
156
|
+
let promptRow = scrollEnd + 1;
|
|
157
|
+
|
|
158
|
+
// Clear screen, print banner + echo, draw static prompt at bottom,
|
|
159
|
+
// then set scroll region so agent output stays above the prompt.
|
|
160
|
+
process.stdout.write(
|
|
161
|
+
"\x1b[r" +
|
|
162
|
+
"\x1b[2J\x1b[H" +
|
|
163
|
+
bLines.join("\n") + "\n" + echoBar + "\n",
|
|
164
|
+
);
|
|
165
|
+
drawStaticPrompt(promptRow, state);
|
|
166
|
+
|
|
167
|
+
const agentRow = Math.min(bLines.length + 2, scrollEnd);
|
|
168
|
+
process.stdout.write(`\x1b[1;${scrollEnd}r\x1b[${agentRow};1H`);
|
|
169
|
+
|
|
170
|
+
const onResize = () => {
|
|
171
|
+
const newVr = getVisibleRows();
|
|
172
|
+
const newScrollEnd = Math.max(4, newVr - 4);
|
|
173
|
+
const newPromptRow = newScrollEnd + 1;
|
|
174
|
+
process.stdout.write(
|
|
175
|
+
"\x1b7" +
|
|
176
|
+
"\x1b[r" +
|
|
177
|
+
`\x1b[${promptRow};1H\x1b[J`,
|
|
178
|
+
);
|
|
179
|
+
drawStaticPrompt(newPromptRow, state);
|
|
180
|
+
process.stdout.write(
|
|
181
|
+
`\x1b[1;${newScrollEnd}r` +
|
|
182
|
+
"\x1b8",
|
|
183
|
+
);
|
|
184
|
+
scrollEnd = newScrollEnd;
|
|
185
|
+
promptRow = newPromptRow;
|
|
186
|
+
};
|
|
187
|
+
process.stdout.on("resize", onResize);
|
|
188
|
+
|
|
189
|
+
const t0 = Date.now();
|
|
190
|
+
let result;
|
|
191
|
+
try {
|
|
192
|
+
result = await runAgent({
|
|
193
|
+
initialPrompt: line,
|
|
194
|
+
priorMessages: state.messages.length > 0 ? state.messages : undefined,
|
|
195
|
+
cwd: state.cwd,
|
|
196
|
+
autoYes: state.autoYes,
|
|
197
|
+
unsafePaths: state.unsafePaths,
|
|
198
|
+
model: state.model,
|
|
199
|
+
maxTurns: state.maxTurns,
|
|
200
|
+
mcpManager,
|
|
201
|
+
});
|
|
202
|
+
} finally {
|
|
203
|
+
process.stdout.removeListener("resize", onResize);
|
|
204
|
+
process.stdout.write("\x1b[r");
|
|
205
|
+
}
|
|
206
|
+
const elapsed = Date.now() - t0;
|
|
173
207
|
|
|
174
208
|
state.sessionCredits += result.totalCredits ?? 0;
|
|
175
209
|
state.sessionIn += result.totalIn ?? 0;
|
|
@@ -181,27 +215,19 @@ export async function runRepl({ cwd: initialCwd, autoYes: initialAutoYes, unsafe
|
|
|
181
215
|
console.log("\n" + errorLine(result.error.message || String(result.error)));
|
|
182
216
|
}
|
|
183
217
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
if (state.messages.length < 2) return;
|
|
191
|
-
try {
|
|
192
|
-
saveSession({
|
|
193
|
-
id: state.sessionId,
|
|
194
|
-
cwd: state.cwd,
|
|
195
|
-
model: state.model,
|
|
196
|
-
modelName: state.modelName,
|
|
197
|
-
messages: state.messages,
|
|
198
|
-
credits: state.sessionCredits,
|
|
199
|
-
tokensIn: state.sessionIn,
|
|
200
|
-
tokensOut: state.sessionOut,
|
|
201
|
-
turnCount: state.turnCount,
|
|
202
|
-
summary: extractSummary(state.messages),
|
|
218
|
+
const stats = turnStats({
|
|
219
|
+
elapsed,
|
|
220
|
+
tokensIn: 0,
|
|
221
|
+
tokensOut: 0,
|
|
222
|
+
tools: 0,
|
|
223
|
+
credits: result.totalCredits ?? 0,
|
|
203
224
|
});
|
|
204
|
-
|
|
225
|
+
if (stats) console.log(stats);
|
|
226
|
+
console.log("");
|
|
227
|
+
|
|
228
|
+
// Position cursor at prompt area for the interactive prompt
|
|
229
|
+
process.stdout.write(`\x1b[${promptRow};1H\x1b[J`);
|
|
230
|
+
}
|
|
205
231
|
}
|
|
206
232
|
|
|
207
233
|
/* ───────── slash commands ───────── */
|
|
@@ -232,10 +258,10 @@ async function handleSlash(line, state) {
|
|
|
232
258
|
try {
|
|
233
259
|
const me = await fetchBalance();
|
|
234
260
|
state.balance = me.balance;
|
|
235
|
-
state.plan = me.plan;
|
|
261
|
+
state.plan = me.plan || "FREE";
|
|
236
262
|
console.log(
|
|
237
263
|
c.cyan(`balance: ${me.balance.toLocaleString()} credits`) +
|
|
238
|
-
c.gray(`
|
|
264
|
+
c.gray(` \xB7 plan: ${me.plan || "FREE"} (${me.planCredits ?? 0} plan + ${me.topupCredits ?? 0} topup)`),
|
|
239
265
|
);
|
|
240
266
|
} catch (err) {
|
|
241
267
|
console.log(errorLine(err.message || String(err)));
|
|
@@ -248,7 +274,7 @@ async function handleSlash(line, state) {
|
|
|
248
274
|
try {
|
|
249
275
|
process.chdir(next);
|
|
250
276
|
state.cwd = next;
|
|
251
|
-
console.log(c.gray(`cwd
|
|
277
|
+
console.log(c.gray(`cwd -> ${state.cwd}`));
|
|
252
278
|
} catch (err) {
|
|
253
279
|
console.log(errorLine(err.message));
|
|
254
280
|
}
|
|
@@ -280,7 +306,7 @@ async function handleSlash(line, state) {
|
|
|
280
306
|
state.modelName = MODEL_ALIASES[key].name;
|
|
281
307
|
const premium = state.model !== null;
|
|
282
308
|
console.log(
|
|
283
|
-
c.gray(`model
|
|
309
|
+
c.gray(`model -> ${c.cyan(state.modelName)}`) +
|
|
284
310
|
(premium ? c.gray(" (premium — needs purchased credits, else falls back to Core)") : ""),
|
|
285
311
|
);
|
|
286
312
|
} else {
|
|
@@ -288,157 +314,95 @@ async function handleSlash(line, state) {
|
|
|
288
314
|
}
|
|
289
315
|
break;
|
|
290
316
|
}
|
|
291
|
-
case "init": {
|
|
292
|
-
console.log(c.gray("Scanning project..."));
|
|
293
|
-
try {
|
|
294
|
-
const content = generateProjectIndex(state.cwd);
|
|
295
|
-
const dir = path.join(state.cwd, ".aether");
|
|
296
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
297
|
-
const filePath = path.join(dir, "context.md");
|
|
298
|
-
fs.writeFileSync(filePath, content, "utf8");
|
|
299
|
-
const lineCount = content.split("\n").length;
|
|
300
|
-
console.log(c.green("●") + " " + c.bold(`.aether/context.md`) + c.gray(` generated (${lineCount} lines)`));
|
|
301
|
-
console.log(c.gray(" The agent now reads this on every turn. Edit it freely to add project-specific notes."));
|
|
302
|
-
// Add to .gitignore if not already there
|
|
303
|
-
const gitignorePath = path.join(state.cwd, ".gitignore");
|
|
304
|
-
try {
|
|
305
|
-
const existing = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, "utf8") : "";
|
|
306
|
-
if (!existing.includes(".aether/")) {
|
|
307
|
-
fs.appendFileSync(gitignorePath, `\n# Aether agent context\n.aether/\n`, "utf8");
|
|
308
|
-
console.log(c.gray(" Added .aether/ to .gitignore"));
|
|
309
|
-
}
|
|
310
|
-
} catch { /* non-fatal */ }
|
|
311
|
-
} catch (err) {
|
|
312
|
-
console.log(errorLine(`/init failed: ${err.message}`));
|
|
313
|
-
}
|
|
314
|
-
break;
|
|
315
|
-
}
|
|
316
|
-
case "sessions": {
|
|
317
|
-
const sessions = listSessions(state.cwd);
|
|
318
|
-
if (sessions.length === 0) {
|
|
319
|
-
console.log(c.gray("No saved sessions for this directory."));
|
|
320
|
-
} else {
|
|
321
|
-
console.log(c.bold("Saved sessions") + c.gray(` (${state.cwd})`));
|
|
322
|
-
for (const s of sessions.slice(0, 10)) {
|
|
323
|
-
const active = s.id === state.sessionId ? c.cyan(" ← current") : "";
|
|
324
|
-
const summary = s.summary || "(no summary)";
|
|
325
|
-
const truncated = summary.length > 60 ? summary.slice(0, 57) + "..." : summary;
|
|
326
|
-
console.log(
|
|
327
|
-
` ${c.gray(formatAge(s.updatedAt).padEnd(8))} ` +
|
|
328
|
-
`${c.gray(s.id.slice(0, 16))} ` +
|
|
329
|
-
`${truncated}` +
|
|
330
|
-
`${c.gray(` · ${s.turnCount} turns`)}` +
|
|
331
|
-
active,
|
|
332
|
-
);
|
|
333
|
-
}
|
|
334
|
-
if (sessions.length > 10) {
|
|
335
|
-
console.log(c.gray(` ... and ${sessions.length - 10} more`));
|
|
336
|
-
}
|
|
337
|
-
console.log(c.gray("\nUse /resume <id-prefix> to load one."));
|
|
338
|
-
}
|
|
339
|
-
break;
|
|
340
|
-
}
|
|
341
|
-
case "resume": {
|
|
342
|
-
const target = (arg || "").trim();
|
|
343
|
-
let session = null;
|
|
344
|
-
if (target) {
|
|
345
|
-
const sessions = listSessions(state.cwd);
|
|
346
|
-
const match = sessions.find((s) => s.id.startsWith(target));
|
|
347
|
-
if (match) session = loadSession(match.id);
|
|
348
|
-
if (!session) {
|
|
349
|
-
console.log(errorLine(`No session matching "${target}" found.`));
|
|
350
|
-
break;
|
|
351
|
-
}
|
|
352
|
-
} else {
|
|
353
|
-
const recent = findRecentSession(state.cwd);
|
|
354
|
-
if (recent) session = loadSession(recent.id);
|
|
355
|
-
if (!session) {
|
|
356
|
-
console.log(errorLine("No recent session to resume. Use /sessions to list."));
|
|
357
|
-
break;
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
autoSave(state);
|
|
361
|
-
state.sessionId = session.id;
|
|
362
|
-
state.messages = session.messages ?? [];
|
|
363
|
-
state.sessionCredits = session.credits ?? 0;
|
|
364
|
-
state.sessionIn = session.tokensIn ?? 0;
|
|
365
|
-
state.sessionOut = session.tokensOut ?? 0;
|
|
366
|
-
state.turnCount = session.turnCount ?? 0;
|
|
367
|
-
if (session.model !== undefined) state.model = session.model;
|
|
368
|
-
if (session.modelName) state.modelName = session.modelName;
|
|
369
|
-
const summary = extractSummary(session.messages) || "(no summary)";
|
|
370
|
-
console.log(c.gray(`Resumed: `) + c.cyan(summary));
|
|
371
|
-
console.log(c.gray(` ${state.messages.length} messages · ${state.turnCount} turns · ${formatAge(session.updatedAt)}`));
|
|
372
|
-
break;
|
|
373
|
-
}
|
|
374
|
-
case "new": {
|
|
375
|
-
autoSave(state);
|
|
376
|
-
state.sessionId = generateSessionId(state.cwd);
|
|
377
|
-
state.messages = [];
|
|
378
|
-
state.sessionCredits = 0;
|
|
379
|
-
state.sessionIn = 0;
|
|
380
|
-
state.sessionOut = 0;
|
|
381
|
-
state.turnCount = 0;
|
|
382
|
-
console.log(c.gray("New session started. Previous session saved."));
|
|
383
|
-
break;
|
|
384
|
-
}
|
|
385
317
|
default:
|
|
386
318
|
console.log(errorLine(`Unknown command: /${cmd}. Type ${c.cyan("/help")} for shortcuts.`));
|
|
387
319
|
}
|
|
388
320
|
return null;
|
|
389
321
|
}
|
|
390
322
|
|
|
391
|
-
/* ───────── banner
|
|
323
|
+
/* ───────── banner (two-column like Claude Code) ───────── */
|
|
392
324
|
|
|
393
|
-
const visLen = (s) => s.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
394
325
|
const shortenPath = (p, max) => (p.length <= max ? p : "..." + p.slice(p.length - max + 3));
|
|
395
326
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
const
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
"Live thinking spinner + loop guard",
|
|
402
|
-
];
|
|
327
|
+
function padRight(str, width) {
|
|
328
|
+
const visible = stripAnsi(str).length;
|
|
329
|
+
const need = Math.max(0, width - visible);
|
|
330
|
+
return str + " ".repeat(need);
|
|
331
|
+
}
|
|
403
332
|
|
|
404
|
-
function
|
|
405
|
-
const
|
|
406
|
-
|
|
407
|
-
const
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
c.gray((credits ? credits + " · " : "") + mode),
|
|
411
|
-
c.gray(shortenPath(state.cwd, 52)),
|
|
412
|
-
];
|
|
413
|
-
if (projectLine) {
|
|
414
|
-
leftLines.push(c.gray("project: " + (projectLine.length > 48 ? projectLine.slice(0, 45) + "..." : projectLine)));
|
|
415
|
-
}
|
|
416
|
-
if (resumed) {
|
|
417
|
-
leftLines.push(c.cyan("↻ resumed session") + c.gray(` · ${state.turnCount} turns · ${state.messages.length} msgs`));
|
|
418
|
-
}
|
|
419
|
-
const left = boxLines(leftLines, { color: c.magenta, padX: 2 });
|
|
420
|
-
const right = boxLines(
|
|
421
|
-
[c.bold(c.cyan("What's new")), ...WHATS_NEW.map((s) => c.gray("• " + s))],
|
|
422
|
-
{ color: c.cyan, padX: 2 },
|
|
423
|
-
);
|
|
333
|
+
function centerIn(str, width) {
|
|
334
|
+
const visible = stripAnsi(str).length;
|
|
335
|
+
if (visible >= width) return str;
|
|
336
|
+
const left = Math.floor((width - visible) / 2);
|
|
337
|
+
return " ".repeat(left) + str;
|
|
338
|
+
}
|
|
424
339
|
|
|
425
|
-
|
|
340
|
+
function buildBannerLines(state) {
|
|
426
341
|
const cols = process.stdout.columns || 80;
|
|
427
|
-
|
|
428
|
-
|
|
342
|
+
const credits = state.balance != null ? `${state.balance.toLocaleString()} credits` : "";
|
|
343
|
+
const plan = state.plan && state.plan !== "FREE" ? state.plan : "";
|
|
344
|
+
|
|
345
|
+
const blk = (code) => `\x1b[${code}m \x1b[0m`;
|
|
346
|
+
const O = blk("48;2;209;107;52");
|
|
347
|
+
const W = blk("48;2;255;255;255");
|
|
348
|
+
const T = blk("48;2;0;178;169");
|
|
349
|
+
|
|
350
|
+
const modelParts = [state.modelName];
|
|
351
|
+
if (credits) modelParts.push(credits);
|
|
352
|
+
if (plan) modelParts.push(c.cyan(plan));
|
|
353
|
+
const modelLine = modelParts.join(c.dim(" \xB7 "));
|
|
354
|
+
|
|
355
|
+
const ruleW = cols - 2;
|
|
356
|
+
const leftW = Math.min(Math.floor(cols * 0.5), 45);
|
|
357
|
+
const twoCol = cols >= 70;
|
|
358
|
+
|
|
359
|
+
const titleText = " Aether Code v" + VERSION + " ";
|
|
360
|
+
const titleRuleRight = Math.max(0, ruleW - 3 - titleText.length);
|
|
361
|
+
const topRule = c.cyan("╭─" + titleText + "─".repeat(titleRuleRight) + "╮");
|
|
362
|
+
|
|
363
|
+
const VL = c.cyan("│");
|
|
364
|
+
const lines = [];
|
|
365
|
+
|
|
366
|
+
lines.push("");
|
|
367
|
+
lines.push(" " + topRule);
|
|
368
|
+
|
|
369
|
+
const bottomRule = c.cyan(" ╰" + "─".repeat(ruleW) + "╯");
|
|
370
|
+
|
|
371
|
+
if (twoCol) {
|
|
372
|
+
const row = (left, right) => {
|
|
373
|
+
lines.push(" " + VL + padRight(left, leftW) + " " + VL + " " + padRight(right, Math.max(0, ruleW - leftW - 4)) + VL);
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
row("", "");
|
|
377
|
+
row(centerIn(`${O}${T}${O}`, leftW), c.bold("Tips"));
|
|
378
|
+
row(centerIn(`${T}${O}${T}`, leftW), c.dim("Ask Aether to build, debug, or refactor code"));
|
|
379
|
+
row(centerIn(`${O}${T}${O}`, leftW), c.dim("Use /help for shortcuts and /balance for credits"));
|
|
380
|
+
row("", c.dim("Type @ to include file paths or image paths"));
|
|
381
|
+
row(centerIn(c.bold("/model") + c.dim(":") + c.cyan(state.modelName.toLowerCase().replace("aether ", "")), leftW), "");
|
|
382
|
+
row(centerIn(modelLine, leftW), "");
|
|
383
|
+
row("", "");
|
|
384
|
+
row(centerIn(c.dim("cwd: " + shortenPath(state.cwd, 40)), leftW), "");
|
|
385
|
+
row("", "");
|
|
429
386
|
} else {
|
|
430
|
-
|
|
387
|
+
const srow = (content) => {
|
|
388
|
+
lines.push(" " + VL + " " + padRight(content, Math.max(0, ruleW - 2)) + VL);
|
|
389
|
+
};
|
|
390
|
+
srow("");
|
|
391
|
+
srow(centerIn(`${O}${T}${O}`, ruleW - 2));
|
|
392
|
+
srow(centerIn(`${T}${O}${T}`, ruleW - 2));
|
|
393
|
+
srow(centerIn(`${O}${T}${O}`, ruleW - 2));
|
|
394
|
+
srow("");
|
|
395
|
+
srow(centerIn(modelLine, ruleW - 2));
|
|
396
|
+
srow(centerIn(c.dim(shortenPath(state.cwd, 50)), ruleW - 2));
|
|
397
|
+
srow("");
|
|
431
398
|
}
|
|
432
|
-
|
|
399
|
+
|
|
400
|
+
lines.push(bottomRule);
|
|
401
|
+
return lines;
|
|
433
402
|
}
|
|
434
403
|
|
|
435
|
-
function
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
if (state.messages.length > 0) {
|
|
440
|
-
parts.push(c.gray(`history: ${state.messages.length} msg${state.messages.length === 1 ? "" : "s"}`));
|
|
441
|
-
}
|
|
442
|
-
parts.push(c.cyan(state.autoYes ? "auto-yes" : "review"));
|
|
443
|
-
console.log(c.dim(parts.join(" · ")));
|
|
404
|
+
function printBanner(state) {
|
|
405
|
+
const lines = buildBannerLines(state);
|
|
406
|
+
for (const l of lines) console.log(l);
|
|
407
|
+
return lines.length;
|
|
444
408
|
}
|