aether-code 0.12.0 → 0.13.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,68 +1,69 @@
1
- {
2
- "name": "aether-code",
3
- "version": "0.12.0",
4
- "description": "Uncensored AI coding agent for your terminal — Claude Code alternative with MCP support. Reads code, writes files, runs commands. Drives IDA Pro, Roblox Studio, Wireshark, Blender, and any MCP server. 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
- "skills",
19
- "README.md",
20
- "LICENSE"
21
- ],
22
- "engines": {
23
- "node": ">=18"
24
- },
25
- "scripts": {
26
- "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 src/mcp-cli.js src/mcp-registry.js src/skills.js",
27
- "test": "node --test \"test/**/*.test.js\""
28
- },
29
- "keywords": [
30
- "aether",
31
- "ai",
32
- "ai-agent",
33
- "ai-cli",
34
- "ai-coding-assistant",
35
- "ai-coding-agent",
36
- "ai-pair-programmer",
37
- "agent",
38
- "agentic",
39
- "agentic-ai",
40
- "cli",
41
- "claude-code",
42
- "claude-code-alternative",
43
- "cursor-alternative",
44
- "coding-agent",
45
- "code-generation",
46
- "developer-tools",
47
- "llm",
48
- "llm-agent",
49
- "llm-tools",
50
- "mcp",
51
- "mcp-client",
52
- "model-context-protocol",
53
- "modelcontextprotocol",
54
- "ida-pro",
55
- "reverse-engineering",
56
- "roblox",
57
- "uncensored",
58
- "uncensored-ai",
59
- "uncensored-llm",
60
- "terminal",
61
- "tool-use",
62
- "tool-calling",
63
- "trynoguard"
64
- ],
65
- "dependencies": {
66
- "@modelcontextprotocol/sdk": "^1.29.0"
67
- }
68
- }
1
+ {
2
+ "name": "aether-code",
3
+ "version": "0.13.0",
4
+ "description": "Uncensored AI coding agent for your terminal — Claude Code alternative with MCP support. Reads code, writes files, runs commands. Drives IDA Pro, Roblox Studio, Wireshark, Blender, and any MCP server. 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
+ "skills",
19
+ "README.md",
20
+ "LICENSE"
21
+ ],
22
+ "engines": {
23
+ "node": ">=18"
24
+ },
25
+ "scripts": {
26
+ "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 src/mcp-cli.js src/mcp-registry.js src/skills.js",
27
+ "test": "node --test \"test/**/*.test.js\"",
28
+ "prepublishOnly": "npm run lint && npm test"
29
+ },
30
+ "keywords": [
31
+ "aether",
32
+ "ai",
33
+ "ai-agent",
34
+ "ai-cli",
35
+ "ai-coding-assistant",
36
+ "ai-coding-agent",
37
+ "ai-pair-programmer",
38
+ "agent",
39
+ "agentic",
40
+ "agentic-ai",
41
+ "cli",
42
+ "claude-code",
43
+ "claude-code-alternative",
44
+ "cursor-alternative",
45
+ "coding-agent",
46
+ "code-generation",
47
+ "developer-tools",
48
+ "llm",
49
+ "llm-agent",
50
+ "llm-tools",
51
+ "mcp",
52
+ "mcp-client",
53
+ "model-context-protocol",
54
+ "modelcontextprotocol",
55
+ "ida-pro",
56
+ "reverse-engineering",
57
+ "roblox",
58
+ "uncensored",
59
+ "uncensored-ai",
60
+ "uncensored-llm",
61
+ "terminal",
62
+ "tool-use",
63
+ "tool-calling",
64
+ "trynoguard"
65
+ ],
66
+ "dependencies": {
67
+ "@modelcontextprotocol/sdk": "^1.29.0"
68
+ }
69
+ }
package/src/agent.js CHANGED
@@ -1,197 +1,197 @@
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 { loadAllSkills, selectSkills, renderSkillsBlock } from "./skills.js";
9
- import { c, divider, turn, toolHeader, toolResult, errorLine } from "./render.js";
10
-
11
- const DEFAULT_MAX_TURNS = 25;
12
-
13
- export async function runAgent({
14
- initialPrompt,
15
- priorMessages,
16
- cwd,
17
- autoYes = false,
18
- unsafePaths = false,
19
- maxTurns = DEFAULT_MAX_TURNS,
20
- onTokens = () => {},
21
- // Optional MCPManager. When provided, its tools are merged into the agent's
22
- // toolset and tool calls prefixed `mcp__` are routed to it instead of the
23
- // built-in executeTool.
24
- mcpManager = null,
25
- }) {
26
- // Merge built-in tools with MCP-provided tools. MCP tools come second so
27
- // any name collision (unlikely given namespacing, but defense in depth)
28
- // resolves to the built-in.
29
- const tools = mcpManager
30
- ? [...TOOL_DEFINITIONS, ...mcpManager.getToolDefinitions()]
31
- : TOOL_DEFINITIONS;
32
-
33
- // Load skills once per runAgent call (bundled + user-installed). They
34
- // get selected per-turn against the current prompt + any file paths the
35
- // model has read so far. Loading errors are non-fatal — a bad skill file
36
- // shouldn't kill the agent.
37
- let allSkills = [];
38
- try {
39
- allSkills = loadAllSkills();
40
- } catch (e) {
41
- process.stderr.write(c.yellow(`(skill load failed: ${e.message})\n`));
42
- }
43
- const referencedPaths = [];
44
- // Two callers: one-shot (initialPrompt only, fresh conversation) and REPL
45
- // (priorMessages + initialPrompt to continue an ongoing chat).
46
- const messages = priorMessages
47
- ? [...priorMessages, { role: "user", content: initialPrompt }]
48
- : [{ role: "user", content: initialPrompt }];
49
- let totalCredits = 0;
50
- let totalIn = 0;
51
- let totalOut = 0;
52
- let lastBalance = null;
53
-
54
- for (let i = 0; i < maxTurns; i++) {
55
- process.stdout.write("\n" + turn(i + 1) + "\n");
56
-
57
- // Stream the assistant's response. Print text deltas as they arrive,
58
- // along with tool-call announcements as soon as the model commits to
59
- // calling a particular tool (i.e. the `name` arrives in the stream).
60
- const announced = new Set();
61
- let lastWasText = false;
62
-
63
- // Select skills for this turn against the current user prompt + any
64
- // paths the model has read so far. Prepend the matching skills' bodies
65
- // to the last user message of a shallow-cloned messages array — we
66
- // don't want skill text accumulating in the persisted history, only
67
- // being available to the model for the turn where it's relevant.
68
- const turnMessages = buildTurnMessages(messages, allSkills, referencedPaths);
69
-
70
- let res;
71
- try {
72
- res = await agentTurnStream({
73
- messages: turnMessages,
74
- tools,
75
- onDelta: (text) => {
76
- if (!lastWasText) {
77
- process.stdout.write(" ");
78
- lastWasText = true;
79
- }
80
- process.stdout.write(text);
81
- },
82
- onToolCallDelta: (delta) => {
83
- // Print the tool header once we know the name (first chunk for that index)
84
- if (delta.name && !announced.has(delta.index)) {
85
- announced.add(delta.index);
86
- if (lastWasText) process.stdout.write("\n");
87
- lastWasText = false;
88
- process.stdout.write(c.cyan(c.bold(delta.name)) + c.gray("(...)") + c.gray(" preparing args\n"));
89
- }
90
- },
91
- });
92
- } catch (err) {
93
- if (err instanceof AetherError) {
94
- return { ok: false, error: err, totalCredits, totalIn, totalOut, balance: lastBalance, messages };
95
- }
96
- throw err;
97
- }
98
-
99
- // End-of-turn newline + cost meter
100
- if (lastWasText) process.stdout.write("\n");
101
- totalCredits += res.creditsCharged ?? 0;
102
- totalIn += res.usage?.prompt_tokens ?? 0;
103
- totalOut += res.usage?.completion_tokens ?? 0;
104
- if (typeof res.balanceAfter === "number") lastBalance = res.balanceAfter;
105
- onTokens({ totalCredits, totalIn, totalOut, balance: lastBalance });
106
- process.stdout.write(
107
- c.dim(` ${res.creditsCharged ?? 0} cr · ${res.usage?.prompt_tokens ?? 0}→${res.usage?.completion_tokens ?? 0} tokens · finish: ${res.finish_reason}\n`),
108
- );
109
-
110
- // Push assistant message into history
111
- messages.push({
112
- role: "assistant",
113
- content: res.message.content,
114
- tool_calls: res.message.tool_calls,
115
- });
116
-
117
- const toolCalls = res.message.tool_calls ?? [];
118
- if (toolCalls.length === 0) {
119
- return { ok: true, totalCredits, totalIn, totalOut, turns: i + 1, balance: lastBalance, messages };
120
- }
121
-
122
- // Execute each tool call. Show the actual args (now that we have them
123
- // fully assembled) and run.
124
- for (const call of toolCalls) {
125
- let args = {};
126
- try { args = JSON.parse(call.function.arguments || "{}"); } catch { /* leave empty */ }
127
- console.log("");
128
- console.log(toolHeader(call.function.name, args));
129
-
130
- // Route to MCP if the tool name is namespaced (mcp__server__tool);
131
- // otherwise execute the built-in tool. unnamespaceToolName returns
132
- // null for non-MCP names, which is our cheap dispatch test.
133
- let result;
134
- if (mcpManager && unnamespaceToolName(call.function.name)) {
135
- result = await mcpManager.callTool(call.function.name, args);
136
- } else {
137
- result = await executeTool(call, { cwd, autoYes, unsafePaths });
138
- }
139
-
140
- // Track paths the model has touched. Skills with path-pattern triggers
141
- // (e.g. RE skill on `*.exe`) match against this list, so reading a
142
- // binary in turn 3 can activate the RE skill in turn 4.
143
- if (call.function.name === "read_file" || call.function.name === "edit_file" || call.function.name === "write_file") {
144
- if (typeof args.path === "string") referencedPaths.push(args.path);
145
- }
146
- if (result.output) {
147
- const preview = result.output.length > 800 ? result.output.slice(0, 800) + "\n…(truncated)" : result.output;
148
- console.log(toolResult(preview, result.ok));
149
- }
150
-
151
- messages.push({
152
- role: "tool",
153
- tool_call_id: call.id,
154
- content: result.output ?? (result.ok ? "(no output)" : "Failed."),
155
- });
156
- }
157
- }
158
-
159
- console.log(c.yellow(`\nReached max turns (${maxTurns}). Stopping.`));
160
- return { ok: false, error: new Error("Max turns reached"), totalCredits, totalIn, totalOut, balance: lastBalance, messages };
161
- }
162
-
163
- /**
164
- * Per-turn skill injection. Selects skills against the latest user message
165
- * + paths the model has touched, then prepends matching bodies onto the
166
- * final user message of a shallow-cloned messages array. Returns the
167
- * original array unchanged when no skills match — zero overhead on the
168
- * no-skills path.
169
- *
170
- * Why prepend to user message instead of inserting a system message:
171
- * the server's AGENT_SYSTEM check skips its own system prompt when ANY
172
- * system message is present in the request. Adding a skills system
173
- * message would silently delete the server's discipline — which is
174
- * worse than no skills at all. Prepending into the user message keeps
175
- * both layers active.
176
- */
177
- function buildTurnMessages(messages, allSkills, referencedPaths) {
178
- if (allSkills.length === 0) return messages;
179
- // Find the latest user message — that's where the current task lives.
180
- let lastUserIdx = -1;
181
- for (let i = messages.length - 1; i >= 0; i--) {
182
- if (messages[i].role === "user") { lastUserIdx = i; break; }
183
- }
184
- if (lastUserIdx === -1) return messages;
185
- const prompt = typeof messages[lastUserIdx].content === "string"
186
- ? messages[lastUserIdx].content
187
- : "";
188
- const active = selectSkills({ skills: allSkills, prompt, referencedPaths });
189
- if (active.length === 0) return messages;
190
- const block = renderSkillsBlock(active);
191
- const cloned = [...messages];
192
- cloned[lastUserIdx] = {
193
- ...cloned[lastUserIdx],
194
- content: `${block}\n\n---\n\n${prompt}`,
195
- };
196
- return cloned;
197
- }
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 { loadAllSkills, selectSkills, renderSkillsBlock } from "./skills.js";
9
+ import { c, divider, turn, toolHeader, toolResult, errorLine } from "./render.js";
10
+
11
+ const DEFAULT_MAX_TURNS = 25;
12
+
13
+ export async function runAgent({
14
+ initialPrompt,
15
+ priorMessages,
16
+ cwd,
17
+ autoYes = false,
18
+ unsafePaths = false,
19
+ maxTurns = DEFAULT_MAX_TURNS,
20
+ onTokens = () => {},
21
+ // Optional MCPManager. When provided, its tools are merged into the agent's
22
+ // toolset and tool calls prefixed `mcp__` are routed to it instead of the
23
+ // built-in executeTool.
24
+ mcpManager = null,
25
+ }) {
26
+ // Merge built-in tools with MCP-provided tools. MCP tools come second so
27
+ // any name collision (unlikely given namespacing, but defense in depth)
28
+ // resolves to the built-in.
29
+ const tools = mcpManager
30
+ ? [...TOOL_DEFINITIONS, ...mcpManager.getToolDefinitions()]
31
+ : TOOL_DEFINITIONS;
32
+
33
+ // Load skills once per runAgent call (bundled + user-installed). They
34
+ // get selected per-turn against the current prompt + any file paths the
35
+ // model has read so far. Loading errors are non-fatal — a bad skill file
36
+ // shouldn't kill the agent.
37
+ let allSkills = [];
38
+ try {
39
+ allSkills = loadAllSkills();
40
+ } catch (e) {
41
+ process.stderr.write(c.yellow(`(skill load failed: ${e.message})\n`));
42
+ }
43
+ const referencedPaths = [];
44
+ // Two callers: one-shot (initialPrompt only, fresh conversation) and REPL
45
+ // (priorMessages + initialPrompt to continue an ongoing chat).
46
+ const messages = priorMessages
47
+ ? [...priorMessages, { role: "user", content: initialPrompt }]
48
+ : [{ role: "user", content: initialPrompt }];
49
+ let totalCredits = 0;
50
+ let totalIn = 0;
51
+ let totalOut = 0;
52
+ let lastBalance = null;
53
+
54
+ for (let i = 0; i < maxTurns; i++) {
55
+ process.stdout.write("\n" + turn(i + 1) + "\n");
56
+
57
+ // Stream the assistant's response. Print text deltas as they arrive,
58
+ // along with tool-call announcements as soon as the model commits to
59
+ // calling a particular tool (i.e. the `name` arrives in the stream).
60
+ const announced = new Set();
61
+ let lastWasText = false;
62
+
63
+ // Select skills for this turn against the current user prompt + any
64
+ // paths the model has read so far. Prepend the matching skills' bodies
65
+ // to the last user message of a shallow-cloned messages array — we
66
+ // don't want skill text accumulating in the persisted history, only
67
+ // being available to the model for the turn where it's relevant.
68
+ const turnMessages = buildTurnMessages(messages, allSkills, referencedPaths);
69
+
70
+ let res;
71
+ try {
72
+ res = await agentTurnStream({
73
+ messages: turnMessages,
74
+ tools,
75
+ onDelta: (text) => {
76
+ if (!lastWasText) {
77
+ process.stdout.write(" ");
78
+ lastWasText = true;
79
+ }
80
+ process.stdout.write(text);
81
+ },
82
+ onToolCallDelta: (delta) => {
83
+ // Print the tool header once we know the name (first chunk for that index)
84
+ if (delta.name && !announced.has(delta.index)) {
85
+ announced.add(delta.index);
86
+ if (lastWasText) process.stdout.write("\n");
87
+ lastWasText = false;
88
+ process.stdout.write(c.cyan(c.bold(delta.name)) + c.gray("(...)") + c.gray(" preparing args\n"));
89
+ }
90
+ },
91
+ });
92
+ } catch (err) {
93
+ if (err instanceof AetherError) {
94
+ return { ok: false, error: err, totalCredits, totalIn, totalOut, balance: lastBalance, messages };
95
+ }
96
+ throw err;
97
+ }
98
+
99
+ // End-of-turn newline + cost meter
100
+ if (lastWasText) process.stdout.write("\n");
101
+ totalCredits += res.creditsCharged ?? 0;
102
+ totalIn += res.usage?.prompt_tokens ?? 0;
103
+ totalOut += res.usage?.completion_tokens ?? 0;
104
+ if (typeof res.balanceAfter === "number") lastBalance = res.balanceAfter;
105
+ onTokens({ totalCredits, totalIn, totalOut, balance: lastBalance });
106
+ process.stdout.write(
107
+ c.dim(` ${res.creditsCharged ?? 0} cr · ${res.usage?.prompt_tokens ?? 0}→${res.usage?.completion_tokens ?? 0} tokens · finish: ${res.finish_reason}\n`),
108
+ );
109
+
110
+ // Push assistant message into history
111
+ messages.push({
112
+ role: "assistant",
113
+ content: res.message.content,
114
+ tool_calls: res.message.tool_calls,
115
+ });
116
+
117
+ const toolCalls = res.message.tool_calls ?? [];
118
+ if (toolCalls.length === 0) {
119
+ return { ok: true, totalCredits, totalIn, totalOut, turns: i + 1, balance: lastBalance, messages };
120
+ }
121
+
122
+ // Execute each tool call. Show the actual args (now that we have them
123
+ // fully assembled) and run.
124
+ for (const call of toolCalls) {
125
+ let args = {};
126
+ try { args = JSON.parse(call.function.arguments || "{}"); } catch { /* leave empty */ }
127
+ console.log("");
128
+ console.log(toolHeader(call.function.name, args));
129
+
130
+ // Route to MCP if the tool name is namespaced (mcp__server__tool);
131
+ // otherwise execute the built-in tool. unnamespaceToolName returns
132
+ // null for non-MCP names, which is our cheap dispatch test.
133
+ let result;
134
+ if (mcpManager && unnamespaceToolName(call.function.name)) {
135
+ result = await mcpManager.callTool(call.function.name, args);
136
+ } else {
137
+ result = await executeTool(call, { cwd, autoYes, unsafePaths });
138
+ }
139
+
140
+ // Track paths the model has touched. Skills with path-pattern triggers
141
+ // (e.g. RE skill on `*.exe`) match against this list, so reading a
142
+ // binary in turn 3 can activate the RE skill in turn 4.
143
+ if (call.function.name === "read_file" || call.function.name === "edit_file" || call.function.name === "write_file") {
144
+ if (typeof args.path === "string") referencedPaths.push(args.path);
145
+ }
146
+ if (result.output) {
147
+ const preview = result.output.length > 800 ? result.output.slice(0, 800) + "\n…(truncated)" : result.output;
148
+ console.log(toolResult(preview, result.ok));
149
+ }
150
+
151
+ messages.push({
152
+ role: "tool",
153
+ tool_call_id: call.id,
154
+ content: result.output ?? (result.ok ? "(no output)" : "Failed."),
155
+ });
156
+ }
157
+ }
158
+
159
+ console.log(c.yellow(`\nReached max turns (${maxTurns}). Stopping.`));
160
+ return { ok: false, error: new Error("Max turns reached"), totalCredits, totalIn, totalOut, balance: lastBalance, messages };
161
+ }
162
+
163
+ /**
164
+ * Per-turn skill injection. Selects skills against the latest user message
165
+ * + paths the model has touched, then prepends matching bodies onto the
166
+ * final user message of a shallow-cloned messages array. Returns the
167
+ * original array unchanged when no skills match — zero overhead on the
168
+ * no-skills path.
169
+ *
170
+ * Why prepend to user message instead of inserting a system message:
171
+ * the server's AGENT_SYSTEM check skips its own system prompt when ANY
172
+ * system message is present in the request. Adding a skills system
173
+ * message would silently delete the server's discipline — which is
174
+ * worse than no skills at all. Prepending into the user message keeps
175
+ * both layers active.
176
+ */
177
+ function buildTurnMessages(messages, allSkills, referencedPaths) {
178
+ if (allSkills.length === 0) return messages;
179
+ // Find the latest user message — that's where the current task lives.
180
+ let lastUserIdx = -1;
181
+ for (let i = messages.length - 1; i >= 0; i--) {
182
+ if (messages[i].role === "user") { lastUserIdx = i; break; }
183
+ }
184
+ if (lastUserIdx === -1) return messages;
185
+ const prompt = typeof messages[lastUserIdx].content === "string"
186
+ ? messages[lastUserIdx].content
187
+ : "";
188
+ const active = selectSkills({ skills: allSkills, prompt, referencedPaths });
189
+ if (active.length === 0) return messages;
190
+ const block = renderSkillsBlock(active);
191
+ const cloned = [...messages];
192
+ cloned[lastUserIdx] = {
193
+ ...cloned[lastUserIdx],
194
+ content: `${block}\n\n---\n\n${prompt}`,
195
+ };
196
+ return cloned;
197
+ }