aether-code 0.3.1 → 0.5.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/bin/aether-code.js +38 -2
- package/package.json +2 -2
- package/src/agent.js +29 -31
- package/src/api.js +2 -0
- package/src/plan.js +133 -0
- package/src/render.js +161 -21
- package/src/repl.js +17 -19
- package/src/tools.js +3 -2
package/bin/aether-code.js
CHANGED
|
@@ -14,9 +14,10 @@ import { runRepl } from "../src/repl.js";
|
|
|
14
14
|
import { runSetup } from "../src/setup.js";
|
|
15
15
|
import { fetchBalance, AetherError } from "../src/api.js";
|
|
16
16
|
import { writeConfigFile, getConfig, CONFIG_PATH } from "../src/config.js";
|
|
17
|
+
import { generateAndApprovePlan } from "../src/plan.js";
|
|
17
18
|
import { c, errorLine, divider } from "../src/render.js";
|
|
18
19
|
|
|
19
|
-
const VERSION = "0.
|
|
20
|
+
const VERSION = "0.5.0";
|
|
20
21
|
|
|
21
22
|
const HELP = `${c.bold("aether")} — uncensored AI coding agent
|
|
22
23
|
|
|
@@ -40,6 +41,7 @@ ${c.bold("EXAMPLES")}
|
|
|
40
41
|
aether --cwd ./my-project "fix the failing tests"
|
|
41
42
|
|
|
42
43
|
${c.bold("FLAGS")}
|
|
44
|
+
--plan Show a numbered plan first; approve / refine / cancel before any tools fire.
|
|
43
45
|
--yes Auto-approve all writes and shell commands. Use with care.
|
|
44
46
|
--cwd <path> Working directory for the agent (default: current dir).
|
|
45
47
|
--max-turns <n> Maximum turns before stopping (default: 25).
|
|
@@ -65,6 +67,7 @@ function parseArgs(argv) {
|
|
|
65
67
|
for (let i = 0; i < argv.length; i++) {
|
|
66
68
|
const a = argv[i];
|
|
67
69
|
if (a === "--yes") { args.flags.yes = true; }
|
|
70
|
+
else if (a === "--plan") { args.flags.plan = true; }
|
|
68
71
|
else if (a === "--unsafe-paths") { args.flags.unsafePaths = true; }
|
|
69
72
|
else if (a === "--help" || a === "-h") { args.flags.help = true; }
|
|
70
73
|
else if (a === "--version" || a === "-v") { args.flags.version = true; }
|
|
@@ -140,12 +143,45 @@ async function main() {
|
|
|
140
143
|
}
|
|
141
144
|
|
|
142
145
|
console.log(divider());
|
|
143
|
-
console.log(c.magenta(c.bold("aether-code")) + c.gray(` · cwd ${cwd}${autoYes ? " · auto-yes" : ""}${unsafePaths ? " · unsafe-paths" : ""}`));
|
|
146
|
+
console.log(c.magenta(c.bold("aether-code")) + c.gray(` · cwd ${cwd}${autoYes ? " · auto-yes" : ""}${unsafePaths ? " · unsafe-paths" : ""}${args.flags.plan ? " · plan-mode" : ""}`));
|
|
144
147
|
console.log(c.gray(`task: `) + prompt);
|
|
145
148
|
console.log(divider());
|
|
146
149
|
|
|
150
|
+
// Plan mode: generate plan first, get approval, then execute with the plan
|
|
151
|
+
// injected into the conversation as context.
|
|
152
|
+
let priorMessages = undefined;
|
|
153
|
+
if (args.flags.plan) {
|
|
154
|
+
let currentTask = prompt;
|
|
155
|
+
// Up to 3 plan/refine cycles before bailing
|
|
156
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
157
|
+
const planResult = await generateAndApprovePlan({ initialPrompt: currentTask, cwd });
|
|
158
|
+
if (planResult.cancelled) {
|
|
159
|
+
console.log(c.gray("\nCancelled. No tools were run."));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (planResult.approved) {
|
|
163
|
+
// Inject the plan as prior context so the agent remembers it as it executes
|
|
164
|
+
priorMessages = [
|
|
165
|
+
{ role: "user", content: currentTask },
|
|
166
|
+
{ role: "assistant", content: `Plan:\n${planResult.planText}` },
|
|
167
|
+
];
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
if (planResult.refinement) {
|
|
171
|
+
console.log(c.gray(`Refining: ${planResult.refinement}`));
|
|
172
|
+
currentTask = `${currentTask}\n\nRefinement: ${planResult.refinement}`;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (!priorMessages) {
|
|
176
|
+
console.log(c.yellow("\nGave up after 3 refinement cycles. Try a clearer task."));
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
console.log("");
|
|
180
|
+
}
|
|
181
|
+
|
|
147
182
|
const result = await runAgent({
|
|
148
183
|
initialPrompt: prompt,
|
|
184
|
+
priorMessages,
|
|
149
185
|
cwd,
|
|
150
186
|
autoYes,
|
|
151
187
|
unsafePaths,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aether-code",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Uncensored AI coding agent for your terminal. Type `aether` to launch the interactive REPL — like Claude Code, with no refusal layer.",
|
|
5
5
|
"homepage": "https://trynoguard.com",
|
|
6
6
|
"repository": {
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"node": ">=18"
|
|
23
23
|
},
|
|
24
24
|
"scripts": {
|
|
25
|
-
"lint": "node --check bin/aether-code.js src/agent.js src/api.js src/config.js src/render.js src/tools.js src/diff.js src/repl.js"
|
|
25
|
+
"lint": "node --check bin/aether-code.js src/agent.js src/api.js src/config.js src/render.js src/tools.js src/diff.js src/repl.js src/plan.js"
|
|
26
26
|
},
|
|
27
27
|
"keywords": [
|
|
28
28
|
"aether",
|
package/src/agent.js
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
import { agentTurnStream, AetherError } from "./api.js";
|
|
6
6
|
import { TOOL_DEFINITIONS, executeTool } from "./tools.js";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
c, turnLine, costLine, toolAnnounce, toolResultLine, errorLine, assistantPrefix,
|
|
9
|
+
} from "./render.js";
|
|
8
10
|
|
|
9
11
|
const DEFAULT_MAX_TURNS = 25;
|
|
10
12
|
|
|
@@ -17,8 +19,6 @@ export async function runAgent({
|
|
|
17
19
|
maxTurns = DEFAULT_MAX_TURNS,
|
|
18
20
|
onTokens = () => {},
|
|
19
21
|
}) {
|
|
20
|
-
// Two callers: one-shot (initialPrompt only, fresh conversation) and REPL
|
|
21
|
-
// (priorMessages + initialPrompt to continue an ongoing chat).
|
|
22
22
|
const messages = priorMessages
|
|
23
23
|
? [...priorMessages, { role: "user", content: initialPrompt }]
|
|
24
24
|
: [{ role: "user", content: initialPrompt }];
|
|
@@ -28,13 +28,10 @@ export async function runAgent({
|
|
|
28
28
|
let lastBalance = null;
|
|
29
29
|
|
|
30
30
|
for (let i = 0; i < maxTurns; i++) {
|
|
31
|
-
process.stdout.write("\n" +
|
|
31
|
+
process.stdout.write("\n" + turnLine(i + 1) + "\n");
|
|
32
32
|
|
|
33
|
-
// Stream the assistant's response. Print text deltas as they arrive,
|
|
34
|
-
// along with tool-call announcements as soon as the model commits to
|
|
35
|
-
// calling a particular tool (i.e. the `name` arrives in the stream).
|
|
36
33
|
const announced = new Set();
|
|
37
|
-
let
|
|
34
|
+
let textOpened = false;
|
|
38
35
|
|
|
39
36
|
let res;
|
|
40
37
|
try {
|
|
@@ -42,19 +39,22 @@ export async function runAgent({
|
|
|
42
39
|
messages,
|
|
43
40
|
tools: TOOL_DEFINITIONS,
|
|
44
41
|
onDelta: (text) => {
|
|
45
|
-
if (!
|
|
46
|
-
process.stdout.write("
|
|
47
|
-
|
|
42
|
+
if (!textOpened) {
|
|
43
|
+
process.stdout.write(assistantPrefix() + " ");
|
|
44
|
+
textOpened = true;
|
|
48
45
|
}
|
|
49
46
|
process.stdout.write(text);
|
|
50
47
|
},
|
|
51
48
|
onToolCallDelta: (delta) => {
|
|
52
|
-
// Print
|
|
49
|
+
// Print friendly tool-call header once name + (eventually) args land.
|
|
50
|
+
// We delay the announce until args are mostly assembled — see below.
|
|
53
51
|
if (delta.name && !announced.has(delta.index)) {
|
|
54
52
|
announced.add(delta.index);
|
|
55
|
-
if (
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
if (textOpened) {
|
|
54
|
+
process.stdout.write("\n");
|
|
55
|
+
textOpened = false;
|
|
56
|
+
}
|
|
57
|
+
process.stdout.write(c.cyan(" ⚡ ") + c.bold(c.cyan(delta.name)) + c.dim(" preparing…\n"));
|
|
58
58
|
}
|
|
59
59
|
},
|
|
60
60
|
});
|
|
@@ -65,20 +65,21 @@ export async function runAgent({
|
|
|
65
65
|
throw err;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
// End-of-turn newline + cost meter
|
|
69
|
-
if (
|
|
68
|
+
// End-of-turn: newline + cost meter
|
|
69
|
+
if (textOpened) process.stdout.write("\n");
|
|
70
70
|
totalCredits += res.creditsCharged ?? 0;
|
|
71
71
|
totalIn += res.usage?.prompt_tokens ?? 0;
|
|
72
72
|
totalOut += res.usage?.completion_tokens ?? 0;
|
|
73
73
|
if (typeof res.balanceAfter === "number") lastBalance = res.balanceAfter;
|
|
74
74
|
onTokens({ totalCredits, totalIn, totalOut, balance: lastBalance });
|
|
75
|
-
process.stdout.write(
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
process.stdout.write(costLine({
|
|
76
|
+
creditsCharged: res.creditsCharged ?? 0,
|
|
77
|
+
inputTokens: res.usage?.prompt_tokens ?? 0,
|
|
78
|
+
outputTokens: res.usage?.completion_tokens ?? 0,
|
|
79
|
+
finishReason: res.finish_reason,
|
|
80
|
+
}) + "\n");
|
|
78
81
|
|
|
79
|
-
// Push assistant message into history. Coerce null → ""
|
|
80
|
-
// APIs (Venice in particular) reject assistant messages with null content
|
|
81
|
-
// even when tool_calls is set. Empty string is the safest universal value.
|
|
82
|
+
// Push assistant message into history. Coerce null → "" (Venice hates null).
|
|
82
83
|
messages.push({
|
|
83
84
|
role: "assistant",
|
|
84
85
|
content: res.message.content ?? "",
|
|
@@ -90,19 +91,16 @@ export async function runAgent({
|
|
|
90
91
|
return { ok: true, totalCredits, totalIn, totalOut, turns: i + 1, balance: lastBalance, messages };
|
|
91
92
|
}
|
|
92
93
|
|
|
93
|
-
// Execute each tool call.
|
|
94
|
-
//
|
|
94
|
+
// Execute each tool call. Now we know the full args, render the friendly
|
|
95
|
+
// label + execute + show summarized result.
|
|
95
96
|
for (const call of toolCalls) {
|
|
96
97
|
let args = {};
|
|
97
98
|
try { args = JSON.parse(call.function.arguments || "{}"); } catch { /* leave empty */ }
|
|
98
99
|
console.log("");
|
|
99
|
-
console.log(
|
|
100
|
+
console.log(toolAnnounce(call.function.name, args));
|
|
100
101
|
|
|
101
102
|
const result = await executeTool(call, { cwd, autoYes, unsafePaths });
|
|
102
|
-
|
|
103
|
-
const preview = result.output.length > 800 ? result.output.slice(0, 800) + "\n…(truncated)" : result.output;
|
|
104
|
-
console.log(toolResult(preview, result.ok));
|
|
105
|
-
}
|
|
103
|
+
console.log(toolResultLine(call.function.name, result));
|
|
106
104
|
|
|
107
105
|
messages.push({
|
|
108
106
|
role: "tool",
|
|
@@ -112,6 +110,6 @@ export async function runAgent({
|
|
|
112
110
|
}
|
|
113
111
|
}
|
|
114
112
|
|
|
115
|
-
console.log(c.yellow(`\
|
|
113
|
+
console.log(c.yellow(`\n⚠ Reached max turns (${maxTurns}). Stopping.`));
|
|
116
114
|
return { ok: false, error: new Error("Max turns reached"), totalCredits, totalIn, totalOut, balance: lastBalance, messages };
|
|
117
115
|
}
|
package/src/api.js
CHANGED
|
@@ -66,6 +66,7 @@ export async function agentTurnStream({
|
|
|
66
66
|
tools,
|
|
67
67
|
maxTokens,
|
|
68
68
|
temperature,
|
|
69
|
+
toolChoice,
|
|
69
70
|
onDelta,
|
|
70
71
|
onToolCallDelta,
|
|
71
72
|
onFinish,
|
|
@@ -94,6 +95,7 @@ export async function agentTurnStream({
|
|
|
94
95
|
tools,
|
|
95
96
|
max_tokens: maxTokens,
|
|
96
97
|
temperature,
|
|
98
|
+
...(toolChoice ? { tool_choice: toolChoice } : {}),
|
|
97
99
|
}),
|
|
98
100
|
});
|
|
99
101
|
} catch (e) {
|
package/src/plan.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// Plan mode — get a structured multi-step plan from the model BEFORE any
|
|
2
|
+
// tools fire, show it to the user, let them approve or refine.
|
|
3
|
+
//
|
|
4
|
+
// Flow:
|
|
5
|
+
// 1. Send a preflight request to /api/v1/agent with tool_choice: "none"
|
|
6
|
+
// and a planning system prompt. Model returns a numbered plan as text.
|
|
7
|
+
// 2. Render the plan with borders. Prompt user: approve / refine / cancel.
|
|
8
|
+
// 3. If approved, the plan text is appended as a tool-style "plan" entry
|
|
9
|
+
// in the message history (so the model remembers it as it executes).
|
|
10
|
+
// 4. Normal agent loop continues with tools enabled.
|
|
11
|
+
|
|
12
|
+
import readline from "node:readline";
|
|
13
|
+
import { agentTurnStream, AetherError } from "./api.js";
|
|
14
|
+
import { TOOL_DEFINITIONS } from "./tools.js";
|
|
15
|
+
import { c, errorLine } from "./render.js";
|
|
16
|
+
|
|
17
|
+
const PLAN_SYSTEM = `You are in PLAN MODE. The user wants to see what you'll do BEFORE you do it.
|
|
18
|
+
|
|
19
|
+
Output a NUMBERED PLAN of the concrete steps you'll take. Each step should be:
|
|
20
|
+
- One short line
|
|
21
|
+
- Reference a specific tool you'll call (read_file, write_file, edit_file, run_shell, list_dir, search_files)
|
|
22
|
+
- Mention the file path or command involved
|
|
23
|
+
|
|
24
|
+
DO NOT call any tools. DO NOT include code blocks. Just the numbered plan.
|
|
25
|
+
|
|
26
|
+
End with one line: "Approve this plan? (y / n / refine)"
|
|
27
|
+
|
|
28
|
+
Example output:
|
|
29
|
+
|
|
30
|
+
1. read_file: src/auth.ts — inspect the existing handler
|
|
31
|
+
2. edit_file: src/auth.ts — add input validation before the password check
|
|
32
|
+
3. write_file: src/auth.test.ts — add a test for invalid input
|
|
33
|
+
4. run_shell: npm test — verify the test passes
|
|
34
|
+
|
|
35
|
+
Approve this plan? (y / n / refine)`;
|
|
36
|
+
|
|
37
|
+
function ask(question) {
|
|
38
|
+
return new Promise((resolve) => {
|
|
39
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
40
|
+
rl.question(question, (answer) => {
|
|
41
|
+
rl.close();
|
|
42
|
+
resolve(answer.trim());
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function renderPlanBox(plan) {
|
|
48
|
+
const w = Math.min(process.stdout.columns || 80, 90);
|
|
49
|
+
const top = c.gray("╭" + "─".repeat(w - 2) + "╮");
|
|
50
|
+
const bot = c.gray("╰" + "─".repeat(w - 2) + "╯");
|
|
51
|
+
const blank = c.gray("│") + " ".repeat(w - 2) + c.gray("│");
|
|
52
|
+
const lines = [top, blank, c.gray("│ ") + c.bold(c.cyan("📋 Plan")) + " ".repeat(w - 11) + c.gray("│"), blank];
|
|
53
|
+
for (const line of plan.split("\n")) {
|
|
54
|
+
// Wrap long lines
|
|
55
|
+
const wrapWidth = w - 4;
|
|
56
|
+
let rest = line;
|
|
57
|
+
while (rest.length > wrapWidth) {
|
|
58
|
+
const slice = rest.slice(0, wrapWidth);
|
|
59
|
+
lines.push(c.gray("│ ") + slice + " ".repeat(w - 3 - slice.length) + c.gray("│"));
|
|
60
|
+
rest = rest.slice(wrapWidth);
|
|
61
|
+
}
|
|
62
|
+
lines.push(c.gray("│ ") + rest + " ".repeat(Math.max(0, w - 3 - rest.length)) + c.gray("│"));
|
|
63
|
+
}
|
|
64
|
+
lines.push(blank, bot);
|
|
65
|
+
return lines.join("\n");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Generate a plan, show it, get approval. Returns:
|
|
70
|
+
* { approved: true, planText: "..." } — user said yes
|
|
71
|
+
* { approved: false, cancelled: true } — user said no
|
|
72
|
+
* { approved: false, refinement: "..." } — user wants to refine
|
|
73
|
+
*/
|
|
74
|
+
export async function generateAndApprovePlan({ initialPrompt, cwd, priorMessages }) {
|
|
75
|
+
process.stdout.write(c.dim("Generating plan...\n"));
|
|
76
|
+
|
|
77
|
+
const messages = priorMessages
|
|
78
|
+
? [...priorMessages, { role: "user", content: initialPrompt }]
|
|
79
|
+
: [
|
|
80
|
+
{ role: "system", content: PLAN_SYSTEM },
|
|
81
|
+
{ role: "user", content: initialPrompt },
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
let planText = "";
|
|
85
|
+
let creditsCharged = 0;
|
|
86
|
+
let inputTokens = 0;
|
|
87
|
+
let outputTokens = 0;
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const res = await agentTurnStream({
|
|
91
|
+
messages,
|
|
92
|
+
tools: TOOL_DEFINITIONS,
|
|
93
|
+
// Override server heuristic — we explicitly want NO tools here.
|
|
94
|
+
toolChoice: "none",
|
|
95
|
+
maxTokens: 1200,
|
|
96
|
+
});
|
|
97
|
+
planText = (res.message.content || "").trim();
|
|
98
|
+
creditsCharged = res.creditsCharged ?? 0;
|
|
99
|
+
inputTokens = res.usage?.prompt_tokens ?? 0;
|
|
100
|
+
outputTokens = res.usage?.completion_tokens ?? 0;
|
|
101
|
+
} catch (err) {
|
|
102
|
+
if (err instanceof AetherError) {
|
|
103
|
+
console.log(errorLine(err.message));
|
|
104
|
+
return { approved: false, cancelled: true };
|
|
105
|
+
}
|
|
106
|
+
throw err;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!planText) {
|
|
110
|
+
console.log(errorLine("Got empty plan from model. Try again or run without --plan."));
|
|
111
|
+
return { approved: false, cancelled: true };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Render the plan in a box
|
|
115
|
+
console.log("");
|
|
116
|
+
console.log(renderPlanBox(planText));
|
|
117
|
+
console.log(c.dim(` ${creditsCharged} cr · ${inputTokens}→${outputTokens} tokens (planning)\n`));
|
|
118
|
+
|
|
119
|
+
// Prompt for approval
|
|
120
|
+
const answer = await ask(
|
|
121
|
+
c.yellow(" ? ") + c.bold("Run this plan?") + c.gray(" [y / n / refine]: "),
|
|
122
|
+
);
|
|
123
|
+
const lower = answer.toLowerCase();
|
|
124
|
+
|
|
125
|
+
if (lower === "y" || lower === "yes") {
|
|
126
|
+
return { approved: true, planText, creditsCharged, inputTokens, outputTokens };
|
|
127
|
+
}
|
|
128
|
+
if (lower === "n" || lower === "no" || lower === "" || lower === "exit") {
|
|
129
|
+
return { approved: false, cancelled: true };
|
|
130
|
+
}
|
|
131
|
+
// Anything else is treated as refinement text
|
|
132
|
+
return { approved: false, refinement: answer || lower };
|
|
133
|
+
}
|
package/src/render.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
// ANSI helpers — no chalk dependency.
|
|
1
|
+
// ANSI helpers — no chalk dependency. Designed to feel like Claude Code:
|
|
2
|
+
// bordered banner, indented tool calls, structured tool-result rendering.
|
|
2
3
|
|
|
3
4
|
const isTty = process.stdout.isTTY;
|
|
4
5
|
const noColor = !!process.env.NO_COLOR || !isTty;
|
|
@@ -10,6 +11,7 @@ function wrap(open, close) {
|
|
|
10
11
|
export const c = {
|
|
11
12
|
bold: wrap(1, 22),
|
|
12
13
|
dim: wrap(2, 22),
|
|
14
|
+
italic: wrap(3, 23),
|
|
13
15
|
red: wrap(31, 39),
|
|
14
16
|
green: wrap(32, 39),
|
|
15
17
|
yellow: wrap(33, 39),
|
|
@@ -17,42 +19,180 @@ export const c = {
|
|
|
17
19
|
magenta: wrap(35, 39),
|
|
18
20
|
cyan: wrap(36, 39),
|
|
19
21
|
gray: wrap(90, 39),
|
|
22
|
+
white: wrap(37, 39),
|
|
20
23
|
};
|
|
21
24
|
|
|
25
|
+
const W = () => Math.min(process.stdout.columns || 80, 90);
|
|
26
|
+
|
|
27
|
+
export function hr() {
|
|
28
|
+
return c.gray("─".repeat(W()));
|
|
29
|
+
}
|
|
30
|
+
|
|
22
31
|
export function divider() {
|
|
23
|
-
|
|
24
|
-
|
|
32
|
+
return hr();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/* ─── Banner — Claude-Code-style box ─── */
|
|
36
|
+
|
|
37
|
+
export function banner({ version, model, mode, balance, cwd }) {
|
|
38
|
+
const w = Math.min(W(), 78);
|
|
39
|
+
const top = c.gray("╭" + "─".repeat(w - 2) + "╮");
|
|
40
|
+
const bot = c.gray("╰" + "─".repeat(w - 2) + "╯");
|
|
41
|
+
const pad = (line) => {
|
|
42
|
+
// Strip ANSI for length calc
|
|
43
|
+
const visible = line.replace(/\x1b\[[0-9;]*m/g, "");
|
|
44
|
+
const space = Math.max(0, w - 2 - visible.length - 2);
|
|
45
|
+
return c.gray("│ ") + line + " ".repeat(space) + c.gray(" │");
|
|
46
|
+
};
|
|
47
|
+
const blank = pad("");
|
|
48
|
+
const title = pad(`${c.magenta(c.bold("✻ aether-code"))} ${c.gray("v" + version)}`);
|
|
49
|
+
const sub = pad(`${c.gray(model + " · uncensored · " + mode + (balance != null ? " · " + balance.toLocaleString() + " credits" : ""))}`);
|
|
50
|
+
const cwdLine = pad(c.gray("📁 " + truncatePath(cwd, w - 6)));
|
|
51
|
+
const tip = pad(c.gray("Type ") + c.cyan("/help") + c.gray(" for shortcuts. ") + c.cyan("/exit") + c.gray(" or Ctrl+C twice to quit."));
|
|
52
|
+
return [top, blank, title, sub, cwdLine, blank, tip, bot].join("\n");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function truncatePath(p, max) {
|
|
56
|
+
if (!p) return "";
|
|
57
|
+
if (p.length <= max) return p;
|
|
58
|
+
return "…" + p.slice(-(max - 1));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* ─── Turn / cost line ─── */
|
|
62
|
+
|
|
63
|
+
export function turnLine(n) {
|
|
64
|
+
return c.dim("─── turn " + n + " " + "─".repeat(Math.max(0, W() - 12 - String(n).length)));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function costLine({ creditsCharged, inputTokens, outputTokens, finishReason }) {
|
|
68
|
+
const parts = [
|
|
69
|
+
`${creditsCharged} cr`,
|
|
70
|
+
`${inputTokens}→${outputTokens} tok`,
|
|
71
|
+
finishReason ? `finish: ${finishReason}` : null,
|
|
72
|
+
].filter(Boolean);
|
|
73
|
+
return " " + c.dim(parts.join(" · "));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* ─── Assistant text ─── */
|
|
77
|
+
|
|
78
|
+
export function assistantPrefix() {
|
|
79
|
+
return c.magenta("● ") + c.bold(c.magenta("Aether")) + c.gray(":");
|
|
25
80
|
}
|
|
26
81
|
|
|
27
|
-
|
|
28
|
-
|
|
82
|
+
/* ─── Tool calls — friendly labels instead of raw JSON ─── */
|
|
83
|
+
|
|
84
|
+
export function toolAnnounce(name, args) {
|
|
85
|
+
const label = TOOL_LABELS[name] ? TOOL_LABELS[name](args) : `${name}(${jsonPreview(args)})`;
|
|
86
|
+
return c.cyan(" ⚡ ") + c.bold(c.cyan(name)) + c.gray(" ") + c.gray(label);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const TOOL_LABELS = {
|
|
90
|
+
read_file: (a) => `→ ${a.path || "?"}`,
|
|
91
|
+
list_dir: (a) => `→ ${a.path || "."}`,
|
|
92
|
+
search_files: (a) => `→ ${a.pattern || "?"} in ${a.path || "."}`,
|
|
93
|
+
write_file: (a) => `→ ${a.path || "?"} ${c.gray("(" + (a.content?.length || 0) + " bytes)")}`,
|
|
94
|
+
edit_file: (a) => `→ ${a.path || "?"}`,
|
|
95
|
+
run_shell: (a) => `${c.yellow("$")} ${a.command || "?"}` + (a.cwd && a.cwd !== "." ? c.gray(` (cwd: ${a.cwd})`) : ""),
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
function jsonPreview(obj) {
|
|
99
|
+
const s = JSON.stringify(obj);
|
|
100
|
+
return s.length > 80 ? s.slice(0, 77) + "..." : s;
|
|
29
101
|
}
|
|
30
102
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
103
|
+
/* ─── Tool results — structured render per tool ─── */
|
|
104
|
+
|
|
105
|
+
export function toolResultLine(name, result) {
|
|
106
|
+
const icon = result.ok ? c.green(" ✓") : c.red(" ✗");
|
|
107
|
+
const summary = summarizeResult(name, result);
|
|
108
|
+
return `${icon} ${c.dim(summary)}`;
|
|
36
109
|
}
|
|
37
110
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
111
|
+
function summarizeResult(name, result) {
|
|
112
|
+
if (!result.ok) {
|
|
113
|
+
// Try to extract a useful one-liner from error JSON
|
|
114
|
+
let txt = result.output || "";
|
|
115
|
+
try {
|
|
116
|
+
const j = JSON.parse(txt);
|
|
117
|
+
if (j.exit_code !== undefined) {
|
|
118
|
+
// Shell error
|
|
119
|
+
const stderr = (j.stderr || "").trim().split("\n")[0] || "(no stderr)";
|
|
120
|
+
return `exit ${j.exit_code} — ${stderr}`;
|
|
121
|
+
}
|
|
122
|
+
} catch { /* not JSON */ }
|
|
123
|
+
const firstLine = txt.split("\n")[0];
|
|
124
|
+
return firstLine.length > 100 ? firstLine.slice(0, 97) + "..." : firstLine;
|
|
125
|
+
}
|
|
126
|
+
// Success — short summary by tool type
|
|
127
|
+
const out = result.output || "";
|
|
128
|
+
if (name === "read_file") {
|
|
129
|
+
const lines = out.split("\n").length;
|
|
130
|
+
return `read ${out.length} bytes (${lines} line${lines === 1 ? "" : "s"})`;
|
|
131
|
+
}
|
|
132
|
+
if (name === "list_dir") {
|
|
133
|
+
try {
|
|
134
|
+
const items = JSON.parse(out);
|
|
135
|
+
return `${items.length} entr${items.length === 1 ? "y" : "ies"}`;
|
|
136
|
+
} catch {
|
|
137
|
+
return "ok";
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (name === "search_files") {
|
|
141
|
+
try {
|
|
142
|
+
const matches = JSON.parse(out);
|
|
143
|
+
return `${matches.length} match${matches.length === 1 ? "" : "es"}`;
|
|
144
|
+
} catch {
|
|
145
|
+
return "ok";
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (name === "write_file" || name === "edit_file") {
|
|
149
|
+
return out.split("\n")[0];
|
|
150
|
+
}
|
|
151
|
+
if (name === "run_shell") {
|
|
152
|
+
try {
|
|
153
|
+
const j = JSON.parse(out);
|
|
154
|
+
const stdoutLines = (j.stdout || "").split("\n").filter(Boolean).length;
|
|
155
|
+
return `exit ${j.exit_code} · ${stdoutLines} stdout line${stdoutLines === 1 ? "" : "s"}`;
|
|
156
|
+
} catch {
|
|
157
|
+
return "ok";
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return out.split("\n")[0].slice(0, 80);
|
|
45
161
|
}
|
|
46
162
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
163
|
+
/* ─── Status line at end of turn ─── */
|
|
164
|
+
|
|
165
|
+
export function statusLine({ sessionCredits, sessionIn, sessionOut, balance, history, mode }) {
|
|
166
|
+
const parts = [];
|
|
167
|
+
parts.push(`session: ${sessionCredits} cr · ${sessionIn}→${sessionOut} tok`);
|
|
168
|
+
if (balance != null) parts.push(`balance: ${balance.toLocaleString()}`);
|
|
169
|
+
if (history > 0) parts.push(`history: ${history} msg${history === 1 ? "" : "s"}`);
|
|
170
|
+
parts.push(c.cyan(mode));
|
|
171
|
+
return c.dim(parts.join(" · "));
|
|
50
172
|
}
|
|
51
173
|
|
|
174
|
+
/* ─── Errors ─── */
|
|
175
|
+
|
|
52
176
|
export function errorLine(msg) {
|
|
53
|
-
return `${c.red(c.bold("Error:"))} ${msg}`;
|
|
177
|
+
return `${c.red(c.bold("✗ Error:"))} ${msg}`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function warnLine(msg) {
|
|
181
|
+
return `${c.yellow("⚠ ")} ${msg}`;
|
|
54
182
|
}
|
|
55
183
|
|
|
56
184
|
export function note(msg) {
|
|
57
185
|
return c.dim(msg);
|
|
58
186
|
}
|
|
187
|
+
|
|
188
|
+
/* ─── Confirmation prompt rendering ─── */
|
|
189
|
+
|
|
190
|
+
export function confirmPrompt(label) {
|
|
191
|
+
return c.yellow(" ? ") + c.bold(label) + c.gray(" [y/N] ");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/* ─── Spinner / pending indicator (no animation, just text) ─── */
|
|
195
|
+
|
|
196
|
+
export function pending(msg) {
|
|
197
|
+
return c.gray(" ⋯ " + msg);
|
|
198
|
+
}
|
package/src/repl.js
CHANGED
|
@@ -13,9 +13,9 @@ import path from "node:path";
|
|
|
13
13
|
import { runAgent } from "./agent.js";
|
|
14
14
|
import { fetchBalance, AetherError } from "./api.js";
|
|
15
15
|
import { runSetup } from "./setup.js";
|
|
16
|
-
import { c, errorLine } from "./render.js";
|
|
16
|
+
import { c, errorLine, banner, statusLine } from "./render.js";
|
|
17
17
|
|
|
18
|
-
const VERSION = "0.
|
|
18
|
+
const VERSION = "0.5.0";
|
|
19
19
|
const MODEL_NAME = "Aether Core";
|
|
20
20
|
|
|
21
21
|
const SHORTCUTS = `
|
|
@@ -222,25 +222,23 @@ async function handleSlash(line, state) {
|
|
|
222
222
|
|
|
223
223
|
function printBanner(state) {
|
|
224
224
|
console.log("");
|
|
225
|
-
console.log(
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
console.log("");
|
|
233
|
-
console.log(c.gray(`Type ${c.cyan("/help")} for shortcuts. ${c.cyan("/exit")} or Ctrl+C twice to quit.`));
|
|
225
|
+
console.log(banner({
|
|
226
|
+
version: VERSION,
|
|
227
|
+
model: MODEL_NAME + " (1M context)",
|
|
228
|
+
mode: state.autoYes ? "auto-yes" : "review mode",
|
|
229
|
+
balance: state.balance,
|
|
230
|
+
cwd: state.cwd,
|
|
231
|
+
}));
|
|
234
232
|
console.log("");
|
|
235
233
|
}
|
|
236
234
|
|
|
237
235
|
function printStatusLine(state) {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
236
|
+
console.log(statusLine({
|
|
237
|
+
sessionCredits: state.sessionCredits,
|
|
238
|
+
sessionIn: state.sessionIn,
|
|
239
|
+
sessionOut: state.sessionOut,
|
|
240
|
+
balance: state.balance,
|
|
241
|
+
history: state.messages.length,
|
|
242
|
+
mode: state.autoYes ? "auto-yes" : "review",
|
|
243
|
+
}));
|
|
246
244
|
}
|
package/src/tools.js
CHANGED
|
@@ -147,8 +147,9 @@ function ask(question) {
|
|
|
147
147
|
|
|
148
148
|
async function confirm(question, autoYes) {
|
|
149
149
|
if (autoYes) return true;
|
|
150
|
-
const ans = await ask(
|
|
151
|
-
|
|
150
|
+
const ans = await ask(c.yellow(" ? ") + c.bold(question) + c.gray(" [y/N] "));
|
|
151
|
+
// Accept y, yes, or any string starting with y (handles double-tap "yy")
|
|
152
|
+
return /^y/i.test(ans.trim());
|
|
152
153
|
}
|
|
153
154
|
|
|
154
155
|
/* ─────────────────────── Implementations ─────────────────────── */
|