aether-code 0.43.1 → 0.43.2
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 +4 -6
- package/package.json +2 -4
- 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 +305 -170
- package/src/api.js +32 -22
- package/src/box-input.js +36 -141
- 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 +712 -0
- package/src/render.js +254 -288
- package/src/repl.js +444 -408
- package/src/sessions.js +167 -0
- package/src/setup.js +8 -6
- package/src/tools.js +393 -16
- 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,408 +1,444 @@
|
|
|
1
|
-
// Interactive REPL — Claude-CLI-style. Launched when `aether-code` is run
|
|
2
|
-
// without a task argument.
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
case "
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
console.log(
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
function printBanner(state) {
|
|
405
|
-
const
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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 fs from "node:fs";
|
|
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, boxLines, sideBySide } from "./render.js";
|
|
17
|
+
import { checkForUpdate } from "./update-check.js";
|
|
18
|
+
import { promptBox, EXIT_SIGNAL } from "./box-input.js";
|
|
19
|
+
import { getVersion } from "./version.js";
|
|
20
|
+
import {
|
|
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();
|
|
27
|
+
const MODEL_NAME = "Aether Core";
|
|
28
|
+
|
|
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
|
+
const MODEL_ALIASES = {
|
|
33
|
+
core: { id: null, name: "Aether Core" },
|
|
34
|
+
gemma: { id: null, name: "Aether Core" },
|
|
35
|
+
ultra: { id: "claude-opus-4-6", name: "Aether Ultra" },
|
|
36
|
+
opus: { id: "claude-opus-4-6", name: "Aether Ultra" },
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const SHORTCUTS = `
|
|
40
|
+
${c.cyan("/help")} Show this help
|
|
41
|
+
${c.cyan("/exit")} Exit (or Ctrl+C twice)
|
|
42
|
+
${c.cyan("/clear")} Clear conversation history (start fresh)
|
|
43
|
+
${c.cyan("/balance")} Refresh and show credit balance
|
|
44
|
+
${c.cyan("/cwd")} ${c.gray("[path]")} Show or change working directory
|
|
45
|
+
${c.cyan("/yes")} Toggle auto-approve mode (skip y/N prompts)
|
|
46
|
+
${c.cyan("/turns")} ${c.gray("<n>")} Set max turns per prompt (default 25)
|
|
47
|
+
${c.cyan("/model")} ${c.gray("[core|ultra]")} Show or switch model (ultra=Opus — premium)
|
|
48
|
+
${c.cyan("/init")} Deep-scan project → generate .aether/context.md
|
|
49
|
+
${c.cyan("/sessions")} List saved sessions for this directory
|
|
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.")}
|
|
55
|
+
`;
|
|
56
|
+
|
|
57
|
+
export async function runRepl({ cwd: initialCwd, autoYes: initialAutoYes, unsafePaths: initialUnsafePaths, maxTurns: initialMaxTurns, model: initialModel = null, mcpManager = null }) {
|
|
58
|
+
const initialAlias = Object.values(MODEL_ALIASES).find((a) => a.id === initialModel);
|
|
59
|
+
const state = {
|
|
60
|
+
cwd: initialCwd,
|
|
61
|
+
autoYes: !!initialAutoYes,
|
|
62
|
+
unsafePaths: !!initialUnsafePaths,
|
|
63
|
+
maxTurns: initialMaxTurns ?? 25,
|
|
64
|
+
model: initialModel ?? null,
|
|
65
|
+
modelName: initialAlias?.name ?? MODEL_NAME,
|
|
66
|
+
messages: [],
|
|
67
|
+
balance: null,
|
|
68
|
+
sessionCredits: 0,
|
|
69
|
+
sessionIn: 0,
|
|
70
|
+
sessionOut: 0,
|
|
71
|
+
sessionId: generateSessionId(initialCwd),
|
|
72
|
+
turnCount: 0,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const updatePromise = checkForUpdate().catch(() => null);
|
|
76
|
+
|
|
77
|
+
let needsSetup = false;
|
|
78
|
+
try {
|
|
79
|
+
const me = await fetchBalance();
|
|
80
|
+
state.balance = me.balance;
|
|
81
|
+
state.plan = me.plan;
|
|
82
|
+
} catch (err) {
|
|
83
|
+
if (err instanceof AetherError && (err.code === "NO_API_KEY" || err.status === 401)) {
|
|
84
|
+
needsSetup = true;
|
|
85
|
+
} else {
|
|
86
|
+
console.log(c.gray(`(could not fetch balance: ${err.message})`));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (needsSetup) {
|
|
91
|
+
const ok = await runSetup();
|
|
92
|
+
if (!ok) {
|
|
93
|
+
console.log(c.gray("Aborting — no valid API key."));
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
const me = await fetchBalance();
|
|
98
|
+
state.balance = me.balance;
|
|
99
|
+
state.plan = me.plan;
|
|
100
|
+
} catch { /* tolerate */ }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Check for a resumable session before printing the banner
|
|
104
|
+
let resumed = false;
|
|
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 */ }
|
|
127
|
+
|
|
128
|
+
// Project quick-scan for banner
|
|
129
|
+
let projectLine = "";
|
|
130
|
+
try {
|
|
131
|
+
projectLine = scanProjectQuick(state.cwd);
|
|
132
|
+
} catch { /* non-fatal */ }
|
|
133
|
+
|
|
134
|
+
printBanner(state, projectLine, resumed);
|
|
135
|
+
const updateNudge = await updatePromise;
|
|
136
|
+
if (updateNudge) console.log(updateNudge + "\n");
|
|
137
|
+
|
|
138
|
+
const inputHistory = [];
|
|
139
|
+
|
|
140
|
+
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
|
+
}
|
|
147
|
+
const line = (raw ?? "").trim();
|
|
148
|
+
if (!line) continue;
|
|
149
|
+
|
|
150
|
+
inputHistory.push(line);
|
|
151
|
+
|
|
152
|
+
if (line.startsWith("/") || line === "?") {
|
|
153
|
+
const handled = await handleSlash(line, state);
|
|
154
|
+
if (handled === "exit") {
|
|
155
|
+
autoSave(state);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
printStatusLine(state);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
state.turnCount++;
|
|
163
|
+
const result = await runAgent({
|
|
164
|
+
initialPrompt: line,
|
|
165
|
+
priorMessages: state.messages.length > 0 ? state.messages : undefined,
|
|
166
|
+
cwd: state.cwd,
|
|
167
|
+
autoYes: state.autoYes,
|
|
168
|
+
unsafePaths: state.unsafePaths,
|
|
169
|
+
model: state.model,
|
|
170
|
+
maxTurns: state.maxTurns,
|
|
171
|
+
mcpManager,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
state.sessionCredits += result.totalCredits ?? 0;
|
|
175
|
+
state.sessionIn += result.totalIn ?? 0;
|
|
176
|
+
state.sessionOut += result.totalOut ?? 0;
|
|
177
|
+
if (typeof result.balance === "number") state.balance = result.balance;
|
|
178
|
+
if (result.messages) state.messages = result.messages;
|
|
179
|
+
|
|
180
|
+
if (!result.ok && result.error) {
|
|
181
|
+
console.log("\n" + errorLine(result.error.message || String(result.error)));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
autoSave(state);
|
|
185
|
+
printStatusLine(state);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function autoSave(state) {
|
|
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),
|
|
203
|
+
});
|
|
204
|
+
} catch { /* non-fatal — don't kill the session over a save failure */ }
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/* ───────── slash commands ───────── */
|
|
208
|
+
|
|
209
|
+
async function handleSlash(line, state) {
|
|
210
|
+
const [cmd, ...rest] = line.replace(/^\//, "").split(/\s+/);
|
|
211
|
+
const arg = rest.join(" ").trim();
|
|
212
|
+
|
|
213
|
+
switch ((cmd || "").toLowerCase()) {
|
|
214
|
+
case "":
|
|
215
|
+
case "help":
|
|
216
|
+
case "?":
|
|
217
|
+
console.log(SHORTCUTS);
|
|
218
|
+
break;
|
|
219
|
+
case "exit":
|
|
220
|
+
case "quit":
|
|
221
|
+
case "q":
|
|
222
|
+
console.log(c.gray("bye."));
|
|
223
|
+
return "exit";
|
|
224
|
+
case "clear":
|
|
225
|
+
state.messages = [];
|
|
226
|
+
state.sessionCredits = 0;
|
|
227
|
+
state.sessionIn = 0;
|
|
228
|
+
state.sessionOut = 0;
|
|
229
|
+
console.log(c.gray("Conversation cleared."));
|
|
230
|
+
break;
|
|
231
|
+
case "balance": {
|
|
232
|
+
try {
|
|
233
|
+
const me = await fetchBalance();
|
|
234
|
+
state.balance = me.balance;
|
|
235
|
+
state.plan = me.plan;
|
|
236
|
+
console.log(
|
|
237
|
+
c.cyan(`balance: ${me.balance.toLocaleString()} credits`) +
|
|
238
|
+
c.gray(` · plan: ${me.plan} (${me.planCredits} plan + ${me.topupCredits} topup)`),
|
|
239
|
+
);
|
|
240
|
+
} catch (err) {
|
|
241
|
+
console.log(errorLine(err.message || String(err)));
|
|
242
|
+
}
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
case "cwd":
|
|
246
|
+
if (arg) {
|
|
247
|
+
const next = path.resolve(arg);
|
|
248
|
+
try {
|
|
249
|
+
process.chdir(next);
|
|
250
|
+
state.cwd = next;
|
|
251
|
+
console.log(c.gray(`cwd → ${state.cwd}`));
|
|
252
|
+
} catch (err) {
|
|
253
|
+
console.log(errorLine(err.message));
|
|
254
|
+
}
|
|
255
|
+
} else {
|
|
256
|
+
console.log(c.gray(`cwd: ${state.cwd}`));
|
|
257
|
+
}
|
|
258
|
+
break;
|
|
259
|
+
case "yes":
|
|
260
|
+
state.autoYes = !state.autoYes;
|
|
261
|
+
console.log(c.gray(`auto-yes: ${state.autoYes ? "on (writes/shells will skip y/N)" : "off"}`));
|
|
262
|
+
break;
|
|
263
|
+
case "turns": {
|
|
264
|
+
const n = parseInt(arg, 10);
|
|
265
|
+
if (!Number.isFinite(n) || n < 1 || n > 200) {
|
|
266
|
+
console.log(errorLine(`/turns expects 1..200, got "${arg}"`));
|
|
267
|
+
} else {
|
|
268
|
+
state.maxTurns = n;
|
|
269
|
+
console.log(c.gray(`max turns: ${state.maxTurns}`));
|
|
270
|
+
}
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
case "model": {
|
|
274
|
+
const key = (arg || "").trim().toLowerCase();
|
|
275
|
+
if (!key) {
|
|
276
|
+
console.log(c.gray(`model: ${state.modelName}`));
|
|
277
|
+
console.log(c.gray("switch with /model core | ultra (ultra=Opus — premium, need credits)"));
|
|
278
|
+
} else if (MODEL_ALIASES[key]) {
|
|
279
|
+
state.model = MODEL_ALIASES[key].id;
|
|
280
|
+
state.modelName = MODEL_ALIASES[key].name;
|
|
281
|
+
const premium = state.model !== null;
|
|
282
|
+
console.log(
|
|
283
|
+
c.gray(`model → ${c.cyan(state.modelName)}`) +
|
|
284
|
+
(premium ? c.gray(" (premium — needs purchased credits, else falls back to Core)") : ""),
|
|
285
|
+
);
|
|
286
|
+
} else {
|
|
287
|
+
console.log(errorLine(`Unknown model "${key}". Try: core, ultra`));
|
|
288
|
+
}
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
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
|
+
default:
|
|
386
|
+
console.log(errorLine(`Unknown command: /${cmd}. Type ${c.cyan("/help")} for shortcuts.`));
|
|
387
|
+
}
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/* ───────── banner + status ───────── */
|
|
392
|
+
|
|
393
|
+
const visLen = (s) => s.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
394
|
+
const shortenPath = (p, max) => (p.length <= max ? p : "..." + p.slice(p.length - max + 3));
|
|
395
|
+
|
|
396
|
+
// Short, hand-curated highlights shown in the "What's new" box. Update with
|
|
397
|
+
// notable releases — keep it to ~3 lines so it balances the banner height.
|
|
398
|
+
const WHATS_NEW = [
|
|
399
|
+
"Session persistence — close & resume",
|
|
400
|
+
"Project auto-understanding on startup",
|
|
401
|
+
"Live thinking spinner + loop guard",
|
|
402
|
+
];
|
|
403
|
+
|
|
404
|
+
function printBanner(state, projectLine = "", resumed = false) {
|
|
405
|
+
const mode = state.autoYes ? (state.unsafePaths ? "skip-permissions" : "auto-yes") : "review mode";
|
|
406
|
+
const credits = state.balance != null ? `${state.balance.toLocaleString()} credits` : "";
|
|
407
|
+
const leftLines = [
|
|
408
|
+
c.bold(c.magenta("aether")) + c.gray(" · ") + c.bold(state.modelName) + c.gray(" v" + VERSION),
|
|
409
|
+
c.gray("uncensored AI coding agent"),
|
|
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
|
+
);
|
|
424
|
+
|
|
425
|
+
console.log("");
|
|
426
|
+
const cols = process.stdout.columns || 80;
|
|
427
|
+
if (cols >= visLen(left[0]) + visLen(right[0]) + 4) {
|
|
428
|
+
console.log(sideBySide(left, right, 3));
|
|
429
|
+
} else {
|
|
430
|
+
console.log(left.join("\n"));
|
|
431
|
+
}
|
|
432
|
+
console.log("");
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function printStatusLine(state) {
|
|
436
|
+
const parts = [];
|
|
437
|
+
parts.push(c.gray(`session: ${state.sessionCredits} cr · ${state.sessionIn}→${state.sessionOut} tokens`));
|
|
438
|
+
if (state.balance != null) parts.push(c.gray(`balance: ${state.balance.toLocaleString()}`));
|
|
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(" · ")));
|
|
444
|
+
}
|