aether-code 0.6.2 → 0.9.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/package.json CHANGED
@@ -1,38 +1,42 @@
1
- {
2
- "name": "aether-code",
3
- "version": "0.6.2",
4
- "description": "Uncensored AI coding agent for your terminal. Type `aether` to launch the interactive REPL — like Claude Code, with no refusal layer.",
5
- "homepage": "https://trynoguard.com",
6
- "repository": {
7
- "type": "git",
8
- "url": "https://github.com/dannyphantomx64/aether-code"
9
- },
10
- "license": "MIT",
11
- "type": "module",
12
- "bin": {
13
- "aether": "bin/aether-code.js"
14
- },
15
- "files": [
16
- "bin",
17
- "src",
18
- "README.md",
19
- "LICENSE"
20
- ],
21
- "engines": {
22
- "node": ">=18"
23
- },
24
- "scripts": {
25
- "lint": "node --check bin/aether-code.js && node --check src/agent.js && node --check src/api.js && node --check src/config.js && node --check src/render.js && node --check src/tools.js && node --check src/diff.js && node --check src/repl.js && node --check src/plan.js && node --check src/sessions.js"
26
- },
27
- "keywords": [
28
- "aether",
29
- "uncensored",
30
- "ai",
31
- "coding-agent",
32
- "claude-code-alternative",
33
- "agent",
34
- "cli",
35
- "tool-use",
36
- "agentic"
37
- ]
38
- }
1
+ {
2
+ "name": "aether-code",
3
+ "version": "0.9.0",
4
+ "description": "Uncensored AI coding agent for your terminal. Type `aether` to launch the interactive REPL — like Claude Code, with no refusal layer.",
5
+ "homepage": "https://trynoguard.com",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/dannyphantomx64/aether-code"
9
+ },
10
+ "license": "MIT",
11
+ "type": "module",
12
+ "bin": {
13
+ "aether": "bin/aether-code.js"
14
+ },
15
+ "files": [
16
+ "bin",
17
+ "src",
18
+ "README.md",
19
+ "LICENSE"
20
+ ],
21
+ "engines": {
22
+ "node": ">=18"
23
+ },
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 src/mcp.js",
26
+ "test": "node --test \"test/**/*.test.js\""
27
+ },
28
+ "keywords": [
29
+ "aether",
30
+ "uncensored",
31
+ "ai",
32
+ "coding-agent",
33
+ "claude-code-alternative",
34
+ "agent",
35
+ "cli",
36
+ "tool-use",
37
+ "agentic"
38
+ ],
39
+ "dependencies": {
40
+ "@modelcontextprotocol/sdk": "^1.29.0"
41
+ }
42
+ }
package/src/agent.js CHANGED
@@ -1,201 +1,134 @@
1
- // Agent loop. Streams each turn from /api/v1/agent/stream, prints text deltas
2
- // in real-time, executes any tool calls, loops until the model returns no
3
- // tool calls (task done) or max-turns is reached.
4
-
5
- import { agentTurnStream, AetherError } from "./api.js";
6
- import { TOOL_DEFINITIONS, executeTool } from "./tools.js";
7
- import { saveSession, newSessionId } from "./sessions.js";
8
- import {
9
- c, turnLine, costLine, toolAnnounce, toolResultLine, errorLine, assistantPrefix,
10
- } from "./render.js";
11
-
12
- const DEFAULT_MAX_TURNS = 25;
13
-
14
- export async function runAgent({
15
- initialPrompt,
16
- priorMessages,
17
- cwd,
18
- autoYes = false,
19
- unsafePaths = false,
20
- maxTurns = DEFAULT_MAX_TURNS,
21
- onTokens = () => {},
22
- // Session persistence options
23
- sessionId, // existing id (resume) or null (new)
24
- saveSessions = true, // disable with --no-save
25
- sessionCreatedAt, // ISO from prior session, or now
26
- // Prior cumulative totals from a resumed session used so the saved
27
- // session reflects lifetime usage, not just this invocation.
28
- priorTotalCredits = 0,
29
- priorTotalIn = 0,
30
- priorTotalOut = 0,
31
- }) {
32
- const messages = priorMessages
33
- ? [...priorMessages, { role: "user", content: initialPrompt }]
34
- : [{ role: "user", content: initialPrompt }];
35
- // Per-invocation counters (used in the result summary).
36
- let totalCredits = 0;
37
- let totalIn = 0;
38
- let totalOut = 0;
39
- let lastBalance = null;
40
-
41
- // Session bookkeeping save after every turn so a crash doesn't lose state.
42
- const activeSessionId = sessionId ?? (saveSessions ? newSessionId() : null);
43
- const createdAt = sessionCreatedAt ?? new Date().toISOString();
44
- // Find the first user message text for the index display
45
- const firstUser = messages.find((m) => m.role === "user");
46
- const firstUserMessage =
47
- firstUser && typeof firstUser.content === "string" ? firstUser.content : initialPrompt;
48
- const persist = () => {
49
- if (!activeSessionId || !saveSessions) return;
50
- try {
51
- saveSession({
52
- id: activeSessionId,
53
- createdAt,
54
- cwd,
55
- // Save cumulative lifetime totals so /sessions show reflects all runs.
56
- totalCredits: priorTotalCredits + totalCredits,
57
- totalIn: priorTotalIn + totalIn,
58
- totalOut: priorTotalOut + totalOut,
59
- firstUserMessage,
60
- messages,
61
- });
62
- } catch (e) {
63
- // Don't kill the agent on save failure; just warn once.
64
- console.log(c.dim(`(session save failed: ${e.message})`));
65
- }
66
- };
67
-
68
- for (let i = 0; i < maxTurns; i++) {
69
- process.stdout.write("\n" + turnLine(i + 1) + "\n");
70
-
71
- const announced = new Set();
72
- let textOpened = false;
73
-
74
- let res;
75
- try {
76
- res = await agentTurnStream({
77
- messages,
78
- tools: TOOL_DEFINITIONS,
79
- onDelta: (text) => {
80
- if (!textOpened) {
81
- process.stdout.write(assistantPrefix() + " ");
82
- textOpened = true;
83
- }
84
- process.stdout.write(text);
85
- },
86
- onToolCallDelta: (delta) => {
87
- // Print friendly tool-call header once name + (eventually) args land.
88
- // We delay the announce until args are mostly assembled — see below.
89
- if (delta.name && !announced.has(delta.index)) {
90
- announced.add(delta.index);
91
- if (textOpened) {
92
- process.stdout.write("\n");
93
- textOpened = false;
94
- }
95
- process.stdout.write(c.cyan(" ⚡ ") + c.bold(c.cyan(delta.name)) + c.dim(" preparing…\n"));
96
- }
97
- },
98
- });
99
- } catch (err) {
100
- if (err instanceof AetherError) {
101
- persist();
102
- return {
103
- ok: false,
104
- error: err,
105
- totalCredits,
106
- totalIn,
107
- totalOut,
108
- balance: lastBalance,
109
- messages,
110
- sessionId: activeSessionId,
111
- };
112
- }
113
- throw err;
114
- }
115
-
116
- // End-of-turn: handle three render cases for the assistant message.
117
- //
118
- // 1. Deltas streamed normally → close the line we opened with onDelta.
119
- // 2. No deltas fired BUT the final accumulated content has text → safety
120
- // net: print it retroactively (defends against any SSE buffering
121
- // race we haven't seen but could exist).
122
- // 3. No deltas, no content, no tool calls → the model emitted only
123
- // internal/whitespace tokens. Show a placeholder so the user isn't
124
- // confused by a silent turn that still consumed credits.
125
- const finalText = res.message.content ?? "";
126
- const hasToolCalls = (res.message.tool_calls?.length ?? 0) > 0;
127
- if (textOpened) {
128
- process.stdout.write("\n");
129
- } else if (finalText.length > 0) {
130
- process.stdout.write(assistantPrefix() + " " + finalText + "\n");
131
- } else if (!hasToolCalls) {
132
- process.stdout.write(assistantPrefix() + " " + c.dim("(empty response)") + "\n");
133
- }
134
- totalCredits += res.creditsCharged ?? 0;
135
- totalIn += res.usage?.prompt_tokens ?? 0;
136
- totalOut += res.usage?.completion_tokens ?? 0;
137
- if (typeof res.balanceAfter === "number") lastBalance = res.balanceAfter;
138
- onTokens({ totalCredits, totalIn, totalOut, balance: lastBalance });
139
- process.stdout.write(costLine({
140
- creditsCharged: res.creditsCharged ?? 0,
141
- inputTokens: res.usage?.prompt_tokens ?? 0,
142
- outputTokens: res.usage?.completion_tokens ?? 0,
143
- finishReason: res.finish_reason,
144
- }) + "\n");
145
-
146
- // Push assistant message into history. Coerce null → "" (Venice hates null).
147
- messages.push({
148
- role: "assistant",
149
- content: res.message.content ?? "",
150
- tool_calls: res.message.tool_calls,
151
- });
152
-
153
- const toolCalls = res.message.tool_calls ?? [];
154
- if (toolCalls.length === 0) {
155
- persist();
156
- return {
157
- ok: true,
158
- totalCredits,
159
- totalIn,
160
- totalOut,
161
- turns: i + 1,
162
- balance: lastBalance,
163
- messages,
164
- sessionId: activeSessionId,
165
- };
166
- }
167
-
168
- // Execute each tool call. Now we know the full args, render the friendly
169
- // label + execute + show summarized result.
170
- for (const call of toolCalls) {
171
- let args = {};
172
- try { args = JSON.parse(call.function.arguments || "{}"); } catch { /* leave empty */ }
173
- console.log("");
174
- console.log(toolAnnounce(call.function.name, args));
175
-
176
- const result = await executeTool(call, { cwd, autoYes, unsafePaths });
177
- console.log(toolResultLine(call.function.name, result));
178
-
179
- messages.push({
180
- role: "tool",
181
- tool_call_id: call.id,
182
- content: result.output ?? (result.ok ? "(no output)" : "Failed."),
183
- });
184
- }
185
- // Save after each tool round so a kill -9 doesn't lose state
186
- persist();
187
- }
188
-
189
- console.log(c.yellow(`\n⚠ Reached max turns (${maxTurns}). Stopping.`));
190
- persist();
191
- return {
192
- ok: false,
193
- error: new Error("Max turns reached"),
194
- totalCredits,
195
- totalIn,
196
- totalOut,
197
- balance: lastBalance,
198
- messages,
199
- sessionId: activeSessionId,
200
- };
201
- }
1
+ // Agent loop. Streams each turn from /api/v1/agent/stream, prints text deltas
2
+ // in real-time, executes any tool calls, loops until the model returns no
3
+ // tool calls (task done) or max-turns is reached.
4
+
5
+ import { agentTurnStream, AetherError } from "./api.js";
6
+ import { TOOL_DEFINITIONS, executeTool } from "./tools.js";
7
+ import { unnamespaceToolName } from "./mcp.js";
8
+ import { c, divider, turn, toolHeader, toolResult, errorLine } from "./render.js";
9
+
10
+ const DEFAULT_MAX_TURNS = 25;
11
+
12
+ export async function runAgent({
13
+ initialPrompt,
14
+ priorMessages,
15
+ cwd,
16
+ autoYes = false,
17
+ unsafePaths = false,
18
+ maxTurns = DEFAULT_MAX_TURNS,
19
+ onTokens = () => {},
20
+ // Optional MCPManager. When provided, its tools are merged into the agent's
21
+ // toolset and tool calls prefixed `mcp__` are routed to it instead of the
22
+ // built-in executeTool.
23
+ mcpManager = null,
24
+ }) {
25
+ // Merge built-in tools with MCP-provided tools. MCP tools come second so
26
+ // any name collision (unlikely given namespacing, but defense in depth)
27
+ // resolves to the built-in.
28
+ const tools = mcpManager
29
+ ? [...TOOL_DEFINITIONS, ...mcpManager.getToolDefinitions()]
30
+ : TOOL_DEFINITIONS;
31
+ // Two callers: one-shot (initialPrompt only, fresh conversation) and REPL
32
+ // (priorMessages + initialPrompt to continue an ongoing chat).
33
+ const messages = priorMessages
34
+ ? [...priorMessages, { role: "user", content: initialPrompt }]
35
+ : [{ role: "user", content: initialPrompt }];
36
+ let totalCredits = 0;
37
+ let totalIn = 0;
38
+ let totalOut = 0;
39
+ let lastBalance = null;
40
+
41
+ for (let i = 0; i < maxTurns; i++) {
42
+ process.stdout.write("\n" + turn(i + 1) + "\n");
43
+
44
+ // Stream the assistant's response. Print text deltas as they arrive,
45
+ // along with tool-call announcements as soon as the model commits to
46
+ // calling a particular tool (i.e. the `name` arrives in the stream).
47
+ const announced = new Set();
48
+ let lastWasText = false;
49
+
50
+ let res;
51
+ try {
52
+ res = await agentTurnStream({
53
+ messages,
54
+ tools,
55
+ onDelta: (text) => {
56
+ if (!lastWasText) {
57
+ process.stdout.write(" ");
58
+ lastWasText = true;
59
+ }
60
+ process.stdout.write(text);
61
+ },
62
+ onToolCallDelta: (delta) => {
63
+ // Print the tool header once we know the name (first chunk for that index)
64
+ if (delta.name && !announced.has(delta.index)) {
65
+ announced.add(delta.index);
66
+ if (lastWasText) process.stdout.write("\n");
67
+ lastWasText = false;
68
+ process.stdout.write(c.cyan(c.bold(delta.name)) + c.gray("(...)") + c.gray(" preparing args\n"));
69
+ }
70
+ },
71
+ });
72
+ } catch (err) {
73
+ if (err instanceof AetherError) {
74
+ return { ok: false, error: err, totalCredits, totalIn, totalOut, balance: lastBalance, messages };
75
+ }
76
+ throw err;
77
+ }
78
+
79
+ // End-of-turn newline + cost meter
80
+ if (lastWasText) process.stdout.write("\n");
81
+ totalCredits += res.creditsCharged ?? 0;
82
+ totalIn += res.usage?.prompt_tokens ?? 0;
83
+ totalOut += res.usage?.completion_tokens ?? 0;
84
+ if (typeof res.balanceAfter === "number") lastBalance = res.balanceAfter;
85
+ onTokens({ totalCredits, totalIn, totalOut, balance: lastBalance });
86
+ process.stdout.write(
87
+ c.dim(` ${res.creditsCharged ?? 0} cr · ${res.usage?.prompt_tokens ?? 0}→${res.usage?.completion_tokens ?? 0} tokens · finish: ${res.finish_reason}\n`),
88
+ );
89
+
90
+ // Push assistant message into history
91
+ messages.push({
92
+ role: "assistant",
93
+ content: res.message.content,
94
+ tool_calls: res.message.tool_calls,
95
+ });
96
+
97
+ const toolCalls = res.message.tool_calls ?? [];
98
+ if (toolCalls.length === 0) {
99
+ return { ok: true, totalCredits, totalIn, totalOut, turns: i + 1, balance: lastBalance, messages };
100
+ }
101
+
102
+ // Execute each tool call. Show the actual args (now that we have them
103
+ // fully assembled) and run.
104
+ for (const call of toolCalls) {
105
+ let args = {};
106
+ try { args = JSON.parse(call.function.arguments || "{}"); } catch { /* leave empty */ }
107
+ console.log("");
108
+ console.log(toolHeader(call.function.name, args));
109
+
110
+ // Route to MCP if the tool name is namespaced (mcp__server__tool);
111
+ // otherwise execute the built-in tool. unnamespaceToolName returns
112
+ // null for non-MCP names, which is our cheap dispatch test.
113
+ let result;
114
+ if (mcpManager && unnamespaceToolName(call.function.name)) {
115
+ result = await mcpManager.callTool(call.function.name, args);
116
+ } else {
117
+ result = await executeTool(call, { cwd, autoYes, unsafePaths });
118
+ }
119
+ if (result.output) {
120
+ const preview = result.output.length > 800 ? result.output.slice(0, 800) + "\n…(truncated)" : result.output;
121
+ console.log(toolResult(preview, result.ok));
122
+ }
123
+
124
+ messages.push({
125
+ role: "tool",
126
+ tool_call_id: call.id,
127
+ content: result.output ?? (result.ok ? "(no output)" : "Failed."),
128
+ });
129
+ }
130
+ }
131
+
132
+ console.log(c.yellow(`\nReached max turns (${maxTurns}). Stopping.`));
133
+ return { ok: false, error: new Error("Max turns reached"), totalCredits, totalIn, totalOut, balance: lastBalance, messages };
134
+ }