aether-code 0.36.7 → 0.37.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/README.md +0 -2
- package/bin/aether-code.js +514 -516
- package/package.json +75 -77
- 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 +38 -170
- package/src/api.js +8 -18
- package/src/box-input.js +36 -140
- 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 +260 -259
- package/src/menu.js +83 -83
- package/src/project-context.js +279 -0
- package/src/render.js +69 -136
- package/src/repl.js +219 -207
- package/src/sessions.js +167 -0
- package/src/setup.js +141 -139
- package/src/tools.js +942 -911
- package/src/update-check.js +7 -64
- package/src/version.js +15 -0
- package/scripts/postinstall.js +0 -182
package/src/repl.js
CHANGED
|
@@ -1,17 +1,33 @@
|
|
|
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
|
|
3
10
|
|
|
4
11
|
import path from "node:path";
|
|
5
12
|
import { runAgent } from "./agent.js";
|
|
6
13
|
import { fetchBalance, AetherError } from "./api.js";
|
|
7
14
|
import { runSetup } from "./setup.js";
|
|
8
|
-
import { c, errorLine,
|
|
9
|
-
import {
|
|
15
|
+
import { c, errorLine, boxLines, sideBySide } from "./render.js";
|
|
16
|
+
import { checkForUpdate } from "./update-check.js";
|
|
10
17
|
import { promptBox, EXIT_SIGNAL } from "./box-input.js";
|
|
11
|
-
|
|
12
|
-
|
|
18
|
+
import { getVersion } from "./version.js";
|
|
19
|
+
import {
|
|
20
|
+
generateSessionId, saveSession, loadSession, listSessions,
|
|
21
|
+
findRecentSession, deleteSession, extractSummary, formatAge,
|
|
22
|
+
} from "./sessions.js";
|
|
23
|
+
import { scanProjectQuick } from "./project-context.js";
|
|
24
|
+
|
|
25
|
+
const VERSION = getVersion();
|
|
13
26
|
const MODEL_NAME = "Aether Core";
|
|
14
27
|
|
|
28
|
+
// /model aliases → { id, name }. id=null = server default (gemma / Aether Core).
|
|
29
|
+
// Premium models are gated SERVER-SIDE: if you haven't purchased credits, the
|
|
30
|
+
// server transparently uses Core instead — selecting one here can't bypass that.
|
|
15
31
|
const MODEL_ALIASES = {
|
|
16
32
|
core: { id: null, name: "Aether Core" },
|
|
17
33
|
gemma: { id: null, name: "Aether Core" },
|
|
@@ -28,44 +44,14 @@ const SHORTCUTS = `
|
|
|
28
44
|
${c.cyan("/yes")} Toggle auto-approve mode (skip y/N prompts)
|
|
29
45
|
${c.cyan("/turns")} ${c.gray("<n>")} Set max turns per prompt (default 25)
|
|
30
46
|
${c.cyan("/model")} ${c.gray("[core|ultra]")} Show or switch model (ultra=Opus — premium)
|
|
47
|
+
${c.cyan("/sessions")} List saved sessions for this directory
|
|
48
|
+
${c.cyan("/resume")} ${c.gray("[id]")} Resume a saved session (default: most recent)
|
|
49
|
+
${c.cyan("/new")} Start a fresh session (save current first)
|
|
31
50
|
|
|
32
51
|
${c.gray("Anything else is sent to the agent as your next message.")}
|
|
33
|
-
${c.gray("
|
|
52
|
+
${c.gray("Sessions auto-save — close the terminal and come back later.")}
|
|
34
53
|
`;
|
|
35
54
|
|
|
36
|
-
const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
37
|
-
|
|
38
|
-
function getVisibleRows() {
|
|
39
|
-
let rows = process.stdout.rows || 24;
|
|
40
|
-
if (process.platform === "win32" && !process.env.WT_SESSION && rows > 60) {
|
|
41
|
-
rows = 24;
|
|
42
|
-
}
|
|
43
|
-
return rows;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function pushPromptToBottom(contentLines) {
|
|
47
|
-
const visibleRows = getVisibleRows();
|
|
48
|
-
const promptHeight = 4;
|
|
49
|
-
const gap = Math.max(0, visibleRows - contentLines - promptHeight);
|
|
50
|
-
if (gap > 0) process.stdout.write("\n".repeat(gap));
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function drawStaticPrompt(startRow, state) {
|
|
54
|
-
const termCols = process.stdout.columns || 80;
|
|
55
|
-
const ruleStr = `\x1b[36m ${"─".repeat(Math.max(0, termCols - 2))}\x1b[0m`;
|
|
56
|
-
const modeStr = state.autoYes ? "auto-yes" : "review";
|
|
57
|
-
const blkFn = (code) => `\x1b[${code}m \x1b[0m`;
|
|
58
|
-
const statusBar = blkFn(46) + blkFn(44) +
|
|
59
|
-
`\x1b[2m ${modeStr} mode \xB7 ? for shortcuts \xB7 @file for images\x1b[0m`;
|
|
60
|
-
process.stdout.write(
|
|
61
|
-
`\x1b[${startRow};1H` +
|
|
62
|
-
ruleStr + "\n" +
|
|
63
|
-
`\x1b[2m> \x1b[0m` + "\n" +
|
|
64
|
-
ruleStr + "\n" +
|
|
65
|
-
statusBar,
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
55
|
export async function runRepl({ cwd: initialCwd, autoYes: initialAutoYes, unsafePaths: initialUnsafePaths, maxTurns: initialMaxTurns, model: initialModel = null, mcpManager = null }) {
|
|
70
56
|
const initialAlias = Object.values(MODEL_ALIASES).find((a) => a.id === initialModel);
|
|
71
57
|
const state = {
|
|
@@ -80,15 +66,17 @@ export async function runRepl({ cwd: initialCwd, autoYes: initialAutoYes, unsafe
|
|
|
80
66
|
sessionCredits: 0,
|
|
81
67
|
sessionIn: 0,
|
|
82
68
|
sessionOut: 0,
|
|
69
|
+
sessionId: generateSessionId(initialCwd),
|
|
70
|
+
turnCount: 0,
|
|
83
71
|
};
|
|
84
72
|
|
|
85
|
-
const updatePromise =
|
|
73
|
+
const updatePromise = checkForUpdate().catch(() => null);
|
|
86
74
|
|
|
87
75
|
let needsSetup = false;
|
|
88
76
|
try {
|
|
89
77
|
const me = await fetchBalance();
|
|
90
78
|
state.balance = me.balance;
|
|
91
|
-
state.plan = me.plan
|
|
79
|
+
state.plan = me.plan;
|
|
92
80
|
} catch (err) {
|
|
93
81
|
if (err instanceof AetherError && (err.code === "NO_API_KEY" || err.status === 401)) {
|
|
94
82
|
needsSetup = true;
|
|
@@ -106,33 +94,54 @@ export async function runRepl({ cwd: initialCwd, autoYes: initialAutoYes, unsafe
|
|
|
106
94
|
try {
|
|
107
95
|
const me = await fetchBalance();
|
|
108
96
|
state.balance = me.balance;
|
|
109
|
-
state.plan = me.plan
|
|
97
|
+
state.plan = me.plan;
|
|
110
98
|
} catch { /* tolerate */ }
|
|
111
99
|
}
|
|
112
100
|
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
101
|
+
// Check for a resumable session before printing the banner
|
|
102
|
+
let resumed = false;
|
|
103
|
+
try {
|
|
104
|
+
const recent = findRecentSession(state.cwd);
|
|
105
|
+
if (recent && recent.messageCount >= 2) {
|
|
106
|
+
const full = loadSession(recent.id);
|
|
107
|
+
if (full && full.messages?.length >= 2) {
|
|
108
|
+
console.log(
|
|
109
|
+
c.gray(`\n Found session from ${formatAge(recent.updatedAt)} — `) +
|
|
110
|
+
c.cyan(extractSummary(full.messages) || "(no summary)"),
|
|
111
|
+
);
|
|
112
|
+
console.log(c.gray(" Resuming automatically. Use /new to start fresh.\n"));
|
|
113
|
+
state.sessionId = full.id;
|
|
114
|
+
state.messages = full.messages;
|
|
115
|
+
state.sessionCredits = full.credits ?? 0;
|
|
116
|
+
state.sessionIn = full.tokensIn ?? 0;
|
|
117
|
+
state.sessionOut = full.tokensOut ?? 0;
|
|
118
|
+
state.turnCount = full.turnCount ?? 0;
|
|
119
|
+
if (full.model !== undefined) state.model = full.model;
|
|
120
|
+
if (full.modelName) state.modelName = full.modelName;
|
|
121
|
+
resumed = true;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
} catch { /* non-fatal */ }
|
|
117
125
|
|
|
118
|
-
|
|
119
|
-
let
|
|
120
|
-
|
|
126
|
+
// Project quick-scan for banner
|
|
127
|
+
let projectLine = "";
|
|
128
|
+
try {
|
|
129
|
+
projectLine = scanProjectQuick(state.cwd);
|
|
130
|
+
} catch { /* non-fatal */ }
|
|
121
131
|
|
|
122
|
-
|
|
132
|
+
printBanner(state, projectLine, resumed);
|
|
133
|
+
const updateNudge = await updatePromise;
|
|
134
|
+
if (updateNudge) console.log(updateNudge + "\n");
|
|
123
135
|
|
|
124
136
|
const inputHistory = [];
|
|
125
137
|
|
|
126
|
-
const handlePromptResize = () => {
|
|
127
|
-
const vr = getVisibleRows();
|
|
128
|
-
process.stdout.write("\x1b[2J\x1b[H");
|
|
129
|
-
printBanner(state);
|
|
130
|
-
process.stdout.write(`\x1b[${Math.max(1, vr - 3)};1H`);
|
|
131
|
-
};
|
|
132
|
-
|
|
133
138
|
while (true) {
|
|
134
|
-
const raw = await promptBox({ history: inputHistory
|
|
135
|
-
if (raw === EXIT_SIGNAL) {
|
|
139
|
+
const raw = await promptBox({ history: inputHistory });
|
|
140
|
+
if (raw === EXIT_SIGNAL) {
|
|
141
|
+
autoSave(state);
|
|
142
|
+
console.log(c.gray("bye."));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
136
145
|
const line = (raw ?? "").trim();
|
|
137
146
|
if (!line) continue;
|
|
138
147
|
|
|
@@ -140,71 +149,25 @@ export async function runRepl({ cwd: initialCwd, autoYes: initialAutoYes, unsafe
|
|
|
140
149
|
|
|
141
150
|
if (line.startsWith("/") || line === "?") {
|
|
142
151
|
const handled = await handleSlash(line, state);
|
|
143
|
-
if (handled === "exit")
|
|
152
|
+
if (handled === "exit") {
|
|
153
|
+
autoSave(state);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
printStatusLine(state);
|
|
144
157
|
continue;
|
|
145
158
|
}
|
|
146
159
|
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
// Clear screen, print banner + echo, draw static prompt at bottom,
|
|
160
|
-
// then set scroll region so agent output stays above the prompt.
|
|
161
|
-
process.stdout.write(
|
|
162
|
-
"\x1b[r" +
|
|
163
|
-
"\x1b[2J\x1b[H" +
|
|
164
|
-
bLines.join("\n") + "\n" + echoBar + "\n",
|
|
165
|
-
);
|
|
166
|
-
drawStaticPrompt(promptRow, state);
|
|
167
|
-
|
|
168
|
-
const agentRow = Math.min(bLines.length + 2, scrollEnd);
|
|
169
|
-
process.stdout.write(`\x1b[1;${scrollEnd}r\x1b[${agentRow};1H`);
|
|
170
|
-
|
|
171
|
-
const onResize = () => {
|
|
172
|
-
const newVr = getVisibleRows();
|
|
173
|
-
const newScrollEnd = Math.max(4, newVr - 4);
|
|
174
|
-
const newPromptRow = newScrollEnd + 1;
|
|
175
|
-
process.stdout.write(
|
|
176
|
-
"\x1b7" +
|
|
177
|
-
"\x1b[r" +
|
|
178
|
-
`\x1b[${promptRow};1H\x1b[J`,
|
|
179
|
-
);
|
|
180
|
-
drawStaticPrompt(newPromptRow, state);
|
|
181
|
-
process.stdout.write(
|
|
182
|
-
`\x1b[1;${newScrollEnd}r` +
|
|
183
|
-
"\x1b8",
|
|
184
|
-
);
|
|
185
|
-
scrollEnd = newScrollEnd;
|
|
186
|
-
promptRow = newPromptRow;
|
|
187
|
-
};
|
|
188
|
-
process.stdout.on("resize", onResize);
|
|
189
|
-
|
|
190
|
-
const t0 = Date.now();
|
|
191
|
-
let result;
|
|
192
|
-
try {
|
|
193
|
-
result = await runAgent({
|
|
194
|
-
initialPrompt: line,
|
|
195
|
-
priorMessages: state.messages.length > 0 ? state.messages : undefined,
|
|
196
|
-
cwd: state.cwd,
|
|
197
|
-
autoYes: state.autoYes,
|
|
198
|
-
unsafePaths: state.unsafePaths,
|
|
199
|
-
model: state.model,
|
|
200
|
-
maxTurns: state.maxTurns,
|
|
201
|
-
mcpManager,
|
|
202
|
-
});
|
|
203
|
-
} finally {
|
|
204
|
-
process.stdout.removeListener("resize", onResize);
|
|
205
|
-
process.stdout.write("\x1b[r");
|
|
206
|
-
}
|
|
207
|
-
const elapsed = Date.now() - t0;
|
|
160
|
+
state.turnCount++;
|
|
161
|
+
const result = await runAgent({
|
|
162
|
+
initialPrompt: line,
|
|
163
|
+
priorMessages: state.messages.length > 0 ? state.messages : undefined,
|
|
164
|
+
cwd: state.cwd,
|
|
165
|
+
autoYes: state.autoYes,
|
|
166
|
+
unsafePaths: state.unsafePaths,
|
|
167
|
+
model: state.model,
|
|
168
|
+
maxTurns: state.maxTurns,
|
|
169
|
+
mcpManager,
|
|
170
|
+
});
|
|
208
171
|
|
|
209
172
|
state.sessionCredits += result.totalCredits ?? 0;
|
|
210
173
|
state.sessionIn += result.totalIn ?? 0;
|
|
@@ -216,21 +179,29 @@ export async function runRepl({ cwd: initialCwd, autoYes: initialAutoYes, unsafe
|
|
|
216
179
|
console.log("\n" + errorLine(result.error.message || String(result.error)));
|
|
217
180
|
}
|
|
218
181
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
tokensIn: 0,
|
|
222
|
-
tokensOut: 0,
|
|
223
|
-
tools: 0,
|
|
224
|
-
credits: result.totalCredits ?? 0,
|
|
225
|
-
});
|
|
226
|
-
if (stats) console.log(stats);
|
|
227
|
-
console.log("");
|
|
228
|
-
|
|
229
|
-
// Position cursor at prompt area for the interactive prompt
|
|
230
|
-
process.stdout.write(`\x1b[${promptRow};1H\x1b[J`);
|
|
182
|
+
autoSave(state);
|
|
183
|
+
printStatusLine(state);
|
|
231
184
|
}
|
|
232
185
|
}
|
|
233
186
|
|
|
187
|
+
function autoSave(state) {
|
|
188
|
+
if (state.messages.length < 2) return;
|
|
189
|
+
try {
|
|
190
|
+
saveSession({
|
|
191
|
+
id: state.sessionId,
|
|
192
|
+
cwd: state.cwd,
|
|
193
|
+
model: state.model,
|
|
194
|
+
modelName: state.modelName,
|
|
195
|
+
messages: state.messages,
|
|
196
|
+
credits: state.sessionCredits,
|
|
197
|
+
tokensIn: state.sessionIn,
|
|
198
|
+
tokensOut: state.sessionOut,
|
|
199
|
+
turnCount: state.turnCount,
|
|
200
|
+
summary: extractSummary(state.messages),
|
|
201
|
+
});
|
|
202
|
+
} catch { /* non-fatal — don't kill the session over a save failure */ }
|
|
203
|
+
}
|
|
204
|
+
|
|
234
205
|
/* ───────── slash commands ───────── */
|
|
235
206
|
|
|
236
207
|
async function handleSlash(line, state) {
|
|
@@ -259,10 +230,10 @@ async function handleSlash(line, state) {
|
|
|
259
230
|
try {
|
|
260
231
|
const me = await fetchBalance();
|
|
261
232
|
state.balance = me.balance;
|
|
262
|
-
state.plan = me.plan
|
|
233
|
+
state.plan = me.plan;
|
|
263
234
|
console.log(
|
|
264
235
|
c.cyan(`balance: ${me.balance.toLocaleString()} credits`) +
|
|
265
|
-
c.gray(`
|
|
236
|
+
c.gray(` · plan: ${me.plan} (${me.planCredits} plan + ${me.topupCredits} topup)`),
|
|
266
237
|
);
|
|
267
238
|
} catch (err) {
|
|
268
239
|
console.log(errorLine(err.message || String(err)));
|
|
@@ -275,7 +246,7 @@ async function handleSlash(line, state) {
|
|
|
275
246
|
try {
|
|
276
247
|
process.chdir(next);
|
|
277
248
|
state.cwd = next;
|
|
278
|
-
console.log(c.gray(`cwd
|
|
249
|
+
console.log(c.gray(`cwd → ${state.cwd}`));
|
|
279
250
|
} catch (err) {
|
|
280
251
|
console.log(errorLine(err.message));
|
|
281
252
|
}
|
|
@@ -307,7 +278,7 @@ async function handleSlash(line, state) {
|
|
|
307
278
|
state.modelName = MODEL_ALIASES[key].name;
|
|
308
279
|
const premium = state.model !== null;
|
|
309
280
|
console.log(
|
|
310
|
-
c.gray(`model
|
|
281
|
+
c.gray(`model → ${c.cyan(state.modelName)}`) +
|
|
311
282
|
(premium ? c.gray(" (premium — needs purchased credits, else falls back to Core)") : ""),
|
|
312
283
|
);
|
|
313
284
|
} else {
|
|
@@ -315,91 +286,132 @@ async function handleSlash(line, state) {
|
|
|
315
286
|
}
|
|
316
287
|
break;
|
|
317
288
|
}
|
|
289
|
+
case "sessions": {
|
|
290
|
+
const sessions = listSessions(state.cwd);
|
|
291
|
+
if (sessions.length === 0) {
|
|
292
|
+
console.log(c.gray("No saved sessions for this directory."));
|
|
293
|
+
} else {
|
|
294
|
+
console.log(c.bold("Saved sessions") + c.gray(` (${state.cwd})`));
|
|
295
|
+
for (const s of sessions.slice(0, 10)) {
|
|
296
|
+
const active = s.id === state.sessionId ? c.cyan(" ← current") : "";
|
|
297
|
+
const summary = s.summary || "(no summary)";
|
|
298
|
+
const truncated = summary.length > 60 ? summary.slice(0, 57) + "..." : summary;
|
|
299
|
+
console.log(
|
|
300
|
+
` ${c.gray(formatAge(s.updatedAt).padEnd(8))} ` +
|
|
301
|
+
`${c.gray(s.id.slice(0, 16))} ` +
|
|
302
|
+
`${truncated}` +
|
|
303
|
+
`${c.gray(` · ${s.turnCount} turns`)}` +
|
|
304
|
+
active,
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
if (sessions.length > 10) {
|
|
308
|
+
console.log(c.gray(` ... and ${sessions.length - 10} more`));
|
|
309
|
+
}
|
|
310
|
+
console.log(c.gray("\nUse /resume <id-prefix> to load one."));
|
|
311
|
+
}
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
case "resume": {
|
|
315
|
+
const target = (arg || "").trim();
|
|
316
|
+
let session = null;
|
|
317
|
+
if (target) {
|
|
318
|
+
const sessions = listSessions(state.cwd);
|
|
319
|
+
const match = sessions.find((s) => s.id.startsWith(target));
|
|
320
|
+
if (match) session = loadSession(match.id);
|
|
321
|
+
if (!session) {
|
|
322
|
+
console.log(errorLine(`No session matching "${target}" found.`));
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
} else {
|
|
326
|
+
const recent = findRecentSession(state.cwd);
|
|
327
|
+
if (recent) session = loadSession(recent.id);
|
|
328
|
+
if (!session) {
|
|
329
|
+
console.log(errorLine("No recent session to resume. Use /sessions to list."));
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
autoSave(state);
|
|
334
|
+
state.sessionId = session.id;
|
|
335
|
+
state.messages = session.messages ?? [];
|
|
336
|
+
state.sessionCredits = session.credits ?? 0;
|
|
337
|
+
state.sessionIn = session.tokensIn ?? 0;
|
|
338
|
+
state.sessionOut = session.tokensOut ?? 0;
|
|
339
|
+
state.turnCount = session.turnCount ?? 0;
|
|
340
|
+
if (session.model !== undefined) state.model = session.model;
|
|
341
|
+
if (session.modelName) state.modelName = session.modelName;
|
|
342
|
+
const summary = extractSummary(session.messages) || "(no summary)";
|
|
343
|
+
console.log(c.gray(`Resumed: `) + c.cyan(summary));
|
|
344
|
+
console.log(c.gray(` ${state.messages.length} messages · ${state.turnCount} turns · ${formatAge(session.updatedAt)}`));
|
|
345
|
+
break;
|
|
346
|
+
}
|
|
347
|
+
case "new": {
|
|
348
|
+
autoSave(state);
|
|
349
|
+
state.sessionId = generateSessionId(state.cwd);
|
|
350
|
+
state.messages = [];
|
|
351
|
+
state.sessionCredits = 0;
|
|
352
|
+
state.sessionIn = 0;
|
|
353
|
+
state.sessionOut = 0;
|
|
354
|
+
state.turnCount = 0;
|
|
355
|
+
console.log(c.gray("New session started. Previous session saved."));
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
318
358
|
default:
|
|
319
359
|
console.log(errorLine(`Unknown command: /${cmd}. Type ${c.cyan("/help")} for shortcuts.`));
|
|
320
360
|
}
|
|
321
361
|
return null;
|
|
322
362
|
}
|
|
323
363
|
|
|
324
|
-
/* ───────── banner
|
|
364
|
+
/* ───────── banner + status ───────── */
|
|
325
365
|
|
|
366
|
+
const visLen = (s) => s.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
326
367
|
const shortenPath = (p, max) => (p.length <= max ? p : "..." + p.slice(p.length - max + 3));
|
|
327
368
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
const visible = stripAnsi(str).length;
|
|
336
|
-
if (visible >= width) return str;
|
|
337
|
-
const left = Math.floor((width - visible) / 2);
|
|
338
|
-
return " ".repeat(left) + str;
|
|
339
|
-
}
|
|
369
|
+
// Short, hand-curated highlights shown in the "What's new" box. Update with
|
|
370
|
+
// notable releases — keep it to ~3 lines so it balances the banner height.
|
|
371
|
+
const WHATS_NEW = [
|
|
372
|
+
"Session persistence — close & resume",
|
|
373
|
+
"Project auto-understanding on startup",
|
|
374
|
+
"Live thinking spinner + loop guard",
|
|
375
|
+
];
|
|
340
376
|
|
|
341
|
-
function
|
|
342
|
-
const
|
|
377
|
+
function printBanner(state, projectLine = "", resumed = false) {
|
|
378
|
+
const mode = state.autoYes ? (state.unsafePaths ? "skip-permissions" : "auto-yes") : "review mode";
|
|
343
379
|
const credits = state.balance != null ? `${state.balance.toLocaleString()} credits` : "";
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
const
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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(" " + padRight(left, leftW) + " " + VL + " " + right);
|
|
374
|
-
};
|
|
380
|
+
const leftLines = [
|
|
381
|
+
c.bold(c.magenta("aether")) + c.gray(" · ") + c.bold(state.modelName) + c.gray(" v" + VERSION),
|
|
382
|
+
c.gray("uncensored AI coding agent"),
|
|
383
|
+
c.gray((credits ? credits + " · " : "") + mode),
|
|
384
|
+
c.gray(shortenPath(state.cwd, 52)),
|
|
385
|
+
];
|
|
386
|
+
if (projectLine) {
|
|
387
|
+
leftLines.push(c.gray("project: " + (projectLine.length > 48 ? projectLine.slice(0, 45) + "..." : projectLine)));
|
|
388
|
+
}
|
|
389
|
+
if (resumed) {
|
|
390
|
+
leftLines.push(c.cyan("↻ resumed session") + c.gray(` · ${state.turnCount} turns · ${state.messages.length} msgs`));
|
|
391
|
+
}
|
|
392
|
+
const left = boxLines(leftLines, { color: c.magenta, padX: 2 });
|
|
393
|
+
const right = boxLines(
|
|
394
|
+
[c.bold(c.cyan("What's new")), ...WHATS_NEW.map((s) => c.gray("• " + s))],
|
|
395
|
+
{ color: c.cyan, padX: 2 },
|
|
396
|
+
);
|
|
375
397
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
row(centerIn(`${C}${B} ${C}`, leftW), c.dim("Creative thinking words while processing"));
|
|
381
|
-
row("", c.dim("Prompt pins to bottom, content scrolls up"));
|
|
382
|
-
row("", "");
|
|
383
|
-
row(centerIn(modelLine, leftW), "");
|
|
384
|
-
row(centerIn(c.dim(shortenPath(state.cwd, 50)), leftW), "");
|
|
398
|
+
console.log("");
|
|
399
|
+
const cols = process.stdout.columns || 80;
|
|
400
|
+
if (cols >= visLen(left[0]) + visLen(right[0]) + 4) {
|
|
401
|
+
console.log(sideBySide(left, right, 3));
|
|
385
402
|
} else {
|
|
386
|
-
|
|
387
|
-
lines.push(centerIn(c.bold("Welcome back!"), cols - 4));
|
|
388
|
-
lines.push("");
|
|
389
|
-
lines.push(centerIn(`${C}${B} ${C}`, cols - 4));
|
|
390
|
-
lines.push(centerIn(`${B}${C} ${B}`, cols - 4));
|
|
391
|
-
lines.push(centerIn(`${C}${B} ${C}`, cols - 4));
|
|
392
|
-
lines.push("");
|
|
393
|
-
lines.push(centerIn(modelLine, cols - 4));
|
|
394
|
-
lines.push(centerIn(c.dim(shortenPath(state.cwd, 50)), cols - 4));
|
|
403
|
+
console.log(left.join("\n"));
|
|
395
404
|
}
|
|
396
|
-
|
|
397
|
-
lines.push(bottomRule);
|
|
398
|
-
return lines;
|
|
405
|
+
console.log("");
|
|
399
406
|
}
|
|
400
407
|
|
|
401
|
-
function
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
|
|
408
|
+
function printStatusLine(state) {
|
|
409
|
+
const parts = [];
|
|
410
|
+
parts.push(c.gray(`session: ${state.sessionCredits} cr · ${state.sessionIn}→${state.sessionOut} tokens`));
|
|
411
|
+
if (state.balance != null) parts.push(c.gray(`balance: ${state.balance.toLocaleString()}`));
|
|
412
|
+
if (state.messages.length > 0) {
|
|
413
|
+
parts.push(c.gray(`history: ${state.messages.length} msg${state.messages.length === 1 ? "" : "s"}`));
|
|
414
|
+
}
|
|
415
|
+
parts.push(c.cyan(state.autoYes ? "auto-yes" : "review"));
|
|
416
|
+
console.log(c.dim(parts.join(" · ")));
|
|
405
417
|
}
|