aether-code 0.6.1 → 0.8.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/src/plan.js DELETED
@@ -1,133 +0,0 @@
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/sessions.js DELETED
@@ -1,145 +0,0 @@
1
- // Session persistence — save conversation history to disk so users can
2
- // resume across invocations.
3
- //
4
- // Storage: `~/.aether/sessions/<id>.json`, one file per session.
5
- // Index: `~/.aether/sessions/index.json` — a sorted list of recent sessions
6
- // with metadata for `aether sessions` and `aether --resume`.
7
- //
8
- // Garbage collection: keep at most MAX_SESSIONS (50). Older files removed
9
- // on each save. The index reflects only live files.
10
- //
11
- // File mode 0600 — same convention as ~/.aetherrc since sessions can contain
12
- // pasted code, internal paths, etc.
13
-
14
- import fs from "node:fs";
15
- import path from "node:path";
16
- import os from "node:os";
17
-
18
- const SESSIONS_DIR = path.join(os.homedir(), ".aether", "sessions");
19
- const INDEX_FILE = path.join(SESSIONS_DIR, "index.json");
20
- const MAX_SESSIONS = 50;
21
-
22
- export function sessionsDir() {
23
- return SESSIONS_DIR;
24
- }
25
-
26
- function ensureDir() {
27
- fs.mkdirSync(SESSIONS_DIR, { recursive: true });
28
- }
29
-
30
- /** Generate a fresh session id — local date+time, sortable. */
31
- export function newSessionId() {
32
- const now = new Date();
33
- const pad = (n) => String(n).padStart(2, "0");
34
- return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
35
- }
36
-
37
- /**
38
- * Persist a session. Called incrementally as the agent runs.
39
- *
40
- * @param {object} session — { id, createdAt, lastUsedAt, cwd, model, totalCredits,
41
- * totalIn, totalOut, firstUserMessage, messages }
42
- */
43
- export function saveSession(session) {
44
- ensureDir();
45
- const file = path.join(SESSIONS_DIR, `${session.id}.json`);
46
- const payload = {
47
- ...session,
48
- lastUsedAt: new Date().toISOString(),
49
- };
50
- fs.writeFileSync(file, JSON.stringify(payload, null, 2), { mode: 0o600 });
51
- try { fs.chmodSync(file, 0o600); } catch { /* windows */ }
52
- rebuildIndex();
53
- }
54
-
55
- /** Load a session by id. Returns null if not found. */
56
- export function loadSession(id) {
57
- const file = path.join(SESSIONS_DIR, `${id}.json`);
58
- try {
59
- return JSON.parse(fs.readFileSync(file, "utf8"));
60
- } catch {
61
- return null;
62
- }
63
- }
64
-
65
- /** Load the most recent session (by lastUsedAt). Null if none. */
66
- export function loadMostRecent() {
67
- const list = listSessions(1);
68
- if (list.length === 0) return null;
69
- return loadSession(list[0].id);
70
- }
71
-
72
- /** Delete a session by id. Returns true if it existed. */
73
- export function deleteSession(id) {
74
- const file = path.join(SESSIONS_DIR, `${id}.json`);
75
- try {
76
- fs.unlinkSync(file);
77
- rebuildIndex();
78
- return true;
79
- } catch {
80
- return false;
81
- }
82
- }
83
-
84
- /**
85
- * Return up to `limit` sessions sorted by lastUsedAt descending.
86
- * Each entry is the LIGHTWEIGHT summary, not the full conversation.
87
- */
88
- export function listSessions(limit = 20) {
89
- ensureDir();
90
- try {
91
- const idx = JSON.parse(fs.readFileSync(INDEX_FILE, "utf8"));
92
- return Array.isArray(idx) ? idx.slice(0, limit) : [];
93
- } catch {
94
- return [];
95
- }
96
- }
97
-
98
- /** Rebuild the index by reading every session file's metadata. */
99
- function rebuildIndex() {
100
- ensureDir();
101
- let files;
102
- try {
103
- files = fs.readdirSync(SESSIONS_DIR).filter((f) => f.endsWith(".json") && f !== "index.json");
104
- } catch {
105
- return;
106
- }
107
- const entries = [];
108
- for (const f of files) {
109
- try {
110
- const s = JSON.parse(fs.readFileSync(path.join(SESSIONS_DIR, f), "utf8"));
111
- entries.push({
112
- id: s.id,
113
- createdAt: s.createdAt,
114
- lastUsedAt: s.lastUsedAt ?? s.createdAt,
115
- cwd: s.cwd,
116
- firstUserMessage: s.firstUserMessage ?? "(no message)",
117
- totalCredits: s.totalCredits ?? 0,
118
- messageCount: Array.isArray(s.messages) ? s.messages.length : 0,
119
- });
120
- } catch {
121
- // Corrupt file — skip
122
- }
123
- }
124
- // Sort by last used, descending
125
- entries.sort((a, b) => (b.lastUsedAt || "").localeCompare(a.lastUsedAt || ""));
126
-
127
- // Garbage-collect oldest sessions beyond MAX_SESSIONS
128
- if (entries.length > MAX_SESSIONS) {
129
- const toDelete = entries.slice(MAX_SESSIONS);
130
- for (const old of toDelete) {
131
- try { fs.unlinkSync(path.join(SESSIONS_DIR, `${old.id}.json`)); } catch { /* tolerate */ }
132
- }
133
- entries.length = MAX_SESSIONS;
134
- }
135
-
136
- fs.writeFileSync(INDEX_FILE, JSON.stringify(entries, null, 2), { mode: 0o600 });
137
- try { fs.chmodSync(INDEX_FILE, 0o600); } catch { /* windows */ }
138
- }
139
-
140
- /** Truncate a long user message for display in the listing. */
141
- export function truncatePreview(text, max = 60) {
142
- if (!text) return "";
143
- const oneLine = text.replace(/\s+/g, " ").trim();
144
- return oneLine.length > max ? oneLine.slice(0, max - 1) + "…" : oneLine;
145
- }