aether-code 0.4.0 → 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.
@@ -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.4.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.4.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/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/repl.js CHANGED
@@ -15,7 +15,7 @@ import { fetchBalance, AetherError } from "./api.js";
15
15
  import { runSetup } from "./setup.js";
16
16
  import { c, errorLine, banner, statusLine } from "./render.js";
17
17
 
18
- const VERSION = "0.4.0";
18
+ const VERSION = "0.5.0";
19
19
  const MODEL_NAME = "Aether Core";
20
20
 
21
21
  const SHORTCUTS = `