aether-code 0.6.2 → 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/LICENSE +21 -21
- package/README.md +140 -140
- package/bin/aether-code.js +228 -379
- package/package.json +39 -38
- package/src/agent.js +115 -201
- package/src/api.js +234 -236
- package/src/config.js +38 -38
- package/src/diff.js +48 -48
- package/src/render.js +58 -198
- package/src/repl.js +246 -292
- package/src/setup.js +139 -139
- package/src/tools.js +621 -358
- package/src/plan.js +0 -133
- package/src/sessions.js +0 -145
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
|
-
}
|