aether-code 0.3.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 -0
- package/README.md +140 -0
- package/bin/aether-code.js +228 -0
- package/package.json +38 -0
- package/src/agent.js +115 -0
- package/src/api.js +234 -0
- package/src/config.js +38 -0
- package/src/diff.js +48 -0
- package/src/render.js +58 -0
- package/src/repl.js +246 -0
- package/src/setup.js +139 -0
- package/src/tools.js +357 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Aether (trynoguard.com)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# aether-code
|
|
2
|
+
|
|
3
|
+
> Uncensored AI coding agent for your terminal. Aether reads your codebase, writes code, runs commands — like Claude Code, but with no refusal layer.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx aether-code "build me a TypeScript todo CLI in this folder"
|
|
7
|
+
npx aether-code --yes "add JSDoc to every exported function in src/"
|
|
8
|
+
npx aether-code --cwd ./my-project "fix the failing tests"
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Built on the same [Aether API](https://trynoguard.com) that powers [`aether-cli`](https://www.npmjs.com/package/aether-cli), [`aether-mcp`](https://www.npmjs.com/package/aether-mcp), and the browser DevTools extension. One API key, four surfaces.
|
|
12
|
+
|
|
13
|
+
## What it does
|
|
14
|
+
|
|
15
|
+
`aether-code` is a CLI that runs an AI coding agent locally. It uses tool calling under the hood — the model can read files, list directories, search across your codebase, write or edit files, and run shell commands. After each tool call, it sees the result and decides what to do next, looping until the task is complete or you hit the turn limit.
|
|
16
|
+
|
|
17
|
+
It's the same architecture as Claude Code or Cursor's agent mode, with two differences:
|
|
18
|
+
- **Uncensored** — no refusal layer when you ask it to write security tools, RE scripts, "edgy" content, etc.
|
|
19
|
+
- **Uses your existing Aether credits** — same balance pool as the chat / MCP / DevTools extension.
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# One-off (recommended)
|
|
25
|
+
npx aether-code "your task"
|
|
26
|
+
|
|
27
|
+
# Or install globally
|
|
28
|
+
npm install -g aether-code
|
|
29
|
+
aether-code "your task"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Requires Node 18+. Zero runtime dependencies.
|
|
33
|
+
|
|
34
|
+
## Setup
|
|
35
|
+
|
|
36
|
+
If you've already used `aether-cli`, you're done — same `~/.aetherrc` config.
|
|
37
|
+
|
|
38
|
+
Otherwise:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# Generate a key at https://trynoguard.com/account, then:
|
|
42
|
+
export AETHER_API_KEY=ak_live_your_key_here
|
|
43
|
+
# OR — save to ~/.aetherrc (mode 0600):
|
|
44
|
+
npx aether-cli config set ak_live_your_key_here
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Tools the agent has access to
|
|
48
|
+
|
|
49
|
+
| Tool | What it does | Approval |
|
|
50
|
+
|---|---|---|
|
|
51
|
+
| `read_file` | Read any file as UTF-8 text | auto |
|
|
52
|
+
| `list_dir` | List entries in a directory | auto |
|
|
53
|
+
| `search_files` | Recursive regex search across the codebase | auto |
|
|
54
|
+
| `write_file` | Create or overwrite a file (shows diff) | y/N prompt |
|
|
55
|
+
| `edit_file` | Replace one occurrence of `find` with `replace` (shows diff) | y/N prompt |
|
|
56
|
+
| `run_shell` | Run a shell command, capture stdout/stderr (2-min timeout) | y/N prompt |
|
|
57
|
+
|
|
58
|
+
## Safety
|
|
59
|
+
|
|
60
|
+
By default the agent **will not act without your approval**. Each file write and each shell command shows you exactly what's about to happen and waits for `y/N`.
|
|
61
|
+
|
|
62
|
+
- **`--yes`** — auto-approve all writes and commands. Use only for trusted, scoped tasks.
|
|
63
|
+
- **`--cwd <path>`** — clamp all file operations to a specific directory.
|
|
64
|
+
- **`--unsafe-paths`** — opt out of the cwd-clamping. Required only if the agent legitimately needs to touch files outside the working dir (e.g. global config).
|
|
65
|
+
- **2-minute hard timeout** on each shell command (kills the process if it hangs).
|
|
66
|
+
- **20 KB output truncation** — long stdout/stderr is truncated before being sent back to the model so a runaway test suite can't blow up your context.
|
|
67
|
+
|
|
68
|
+
## Examples
|
|
69
|
+
|
|
70
|
+
### Build a small project from scratch
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
mkdir todo-cli && cd todo-cli
|
|
74
|
+
npx aether-code "build a TypeScript CLI that manages a todo list stored in todos.json. Use commander for arg parsing. Include npm scripts for build and test."
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The agent will: list the empty dir → `npm init -y` → install deps → write `tsconfig.json`, `src/index.ts`, `package.json` updates → run `npm run build` to verify.
|
|
78
|
+
|
|
79
|
+
### Fix failing tests
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
cd existing-project
|
|
83
|
+
npx aether-code "run the tests, see what's failing, and fix them"
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The agent will: `run_shell("npm test")` → read the failing files → make targeted edits → re-run tests → repeat until green.
|
|
87
|
+
|
|
88
|
+
### Refactor
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
npx aether-code --max-turns 40 "convert all CommonJS requires to ES module imports across src/, then update package.json"
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Add documentation across a codebase
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
npx aether-code --yes "add a one-line JSDoc to every exported function in src/ that doesn't have one"
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`--yes` is reasonable here because the operation is bounded and read-mostly with small additive edits.
|
|
101
|
+
|
|
102
|
+
### Reverse engineering
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
npx aether-code "deobfuscate ./bundle.min.js, write the cleaned version to ./bundle.clean.js, then identify what the obfuscation was protecting"
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## What it doesn't do (yet)
|
|
109
|
+
|
|
110
|
+
- **No streaming** — each turn waits for a full model response. Future work.
|
|
111
|
+
- **No interactive Ctrl+C handling** — kill with the OS-level signal.
|
|
112
|
+
- **No multi-step plan preview** — the agent just acts. (Manual `--max-turns 1` to inspect first move.)
|
|
113
|
+
- **No persistent session** — each invocation starts fresh. Workspaces feature on the roadmap.
|
|
114
|
+
|
|
115
|
+
## How it differs from Claude Code
|
|
116
|
+
|
|
117
|
+
| | Claude Code | aether-code |
|
|
118
|
+
|---|---|---|
|
|
119
|
+
| Refusal layer | Yes (Anthropic policy) | No |
|
|
120
|
+
| Cost model | Per Anthropic API token | Aether credits (pay-per-use, crypto top-up) |
|
|
121
|
+
| Streaming | Yes | Not yet |
|
|
122
|
+
| Plan mode | Yes | Not yet |
|
|
123
|
+
| MCP support | Yes (Anthropic's MCP) | Not yet (independent of `aether-mcp`) |
|
|
124
|
+
| Open source | No | Yes (MIT) |
|
|
125
|
+
|
|
126
|
+
## Privacy
|
|
127
|
+
|
|
128
|
+
- Your prompts, file reads, and shell outputs go to `trynoguard.com/api/v1/agent` only.
|
|
129
|
+
- Conversations are not stored server-side (no `Conversation` row, no `Message` rows). The agent endpoint is stateless beyond credit accounting.
|
|
130
|
+
- Source code is plain ES modules. Read it before you trust it.
|
|
131
|
+
|
|
132
|
+
## License
|
|
133
|
+
|
|
134
|
+
MIT — see [LICENSE](LICENSE).
|
|
135
|
+
|
|
136
|
+
## Related
|
|
137
|
+
|
|
138
|
+
- **[aether-cli](https://www.npmjs.com/package/aether-cli)** — non-agentic CLI for one-off prompts (`aether ask`, `deobf`, `explain`, etc.).
|
|
139
|
+
- **[aether-mcp](https://www.npmjs.com/package/aether-mcp)** — MCP server. Use Aether inside Claude Desktop / Cursor / Cline / Zed.
|
|
140
|
+
- **[aether-devtools](https://github.com/dannyphantomx64/aether-devtools)** — browser DevTools extension.
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// aether-code — uncensored AI coding agent.
|
|
3
|
+
//
|
|
4
|
+
// Examples:
|
|
5
|
+
// aether-code "build me a TypeScript todo CLI in this folder"
|
|
6
|
+
// aether-code --yes "add JSDoc to every exported function in src/"
|
|
7
|
+
// aether-code --cwd ./my-project "fix the failing tests"
|
|
8
|
+
// aether-code --max-turns 40 "refactor the auth module to use bcrypt"
|
|
9
|
+
|
|
10
|
+
import process from "node:process";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { runAgent } from "../src/agent.js";
|
|
13
|
+
import { runRepl } from "../src/repl.js";
|
|
14
|
+
import { runSetup } from "../src/setup.js";
|
|
15
|
+
import { fetchBalance, AetherError } from "../src/api.js";
|
|
16
|
+
import { writeConfigFile, getConfig, CONFIG_PATH } from "../src/config.js";
|
|
17
|
+
import { c, errorLine, divider } from "../src/render.js";
|
|
18
|
+
|
|
19
|
+
const VERSION = "0.3.0";
|
|
20
|
+
|
|
21
|
+
const HELP = `${c.bold("aether")} — uncensored AI coding agent
|
|
22
|
+
|
|
23
|
+
${c.bold("USAGE")}
|
|
24
|
+
aether Launch interactive REPL (Claude-CLI-style)
|
|
25
|
+
aether [flags] "<task>" Run agent once on a single task
|
|
26
|
+
aether <subcommand> [args] Run a utility subcommand
|
|
27
|
+
|
|
28
|
+
${c.bold("SUBCOMMANDS")}
|
|
29
|
+
${c.cyan("login")} Open browser, paste API key, save
|
|
30
|
+
${c.cyan("logout")} Clear saved API key
|
|
31
|
+
${c.cyan("balance")} Show plan + credit balance
|
|
32
|
+
${c.cyan("config")} show|set|set-base|path Manage config file
|
|
33
|
+
|
|
34
|
+
${c.bold("EXAMPLES")}
|
|
35
|
+
aether # interactive REPL
|
|
36
|
+
aether login # first-time setup
|
|
37
|
+
aether balance # quick credit check
|
|
38
|
+
aether "build a TypeScript todo CLI in this folder"
|
|
39
|
+
aether --yes "add JSDoc to every exported function"
|
|
40
|
+
aether --cwd ./my-project "fix the failing tests"
|
|
41
|
+
|
|
42
|
+
${c.bold("FLAGS")}
|
|
43
|
+
--yes Auto-approve all writes and shell commands. Use with care.
|
|
44
|
+
--cwd <path> Working directory for the agent (default: current dir).
|
|
45
|
+
--max-turns <n> Maximum turns before stopping (default: 25).
|
|
46
|
+
--unsafe-paths Allow the agent to read/write outside cwd.
|
|
47
|
+
--help, -h Show this help.
|
|
48
|
+
--version, -v Print version.
|
|
49
|
+
|
|
50
|
+
${c.bold("CONFIG")}
|
|
51
|
+
Same config as aether-cli — uses ${c.cyan("AETHER_API_KEY")} or ${c.cyan("~/.aetherrc")}.
|
|
52
|
+
Get a key at ${c.blue("https://trynoguard.com/account")}.
|
|
53
|
+
|
|
54
|
+
${c.bold("SAFETY")}
|
|
55
|
+
- File writes show a unified diff and require y/N confirmation by default.
|
|
56
|
+
- Shell commands show what's about to run and require y/N confirmation.
|
|
57
|
+
- Paths are clamped to ${c.cyan("--cwd")} (override with ${c.cyan("--unsafe-paths")}).
|
|
58
|
+
- Each shell command has a 2-minute hard timeout.
|
|
59
|
+
|
|
60
|
+
${c.gray(`v${VERSION}`)}
|
|
61
|
+
`;
|
|
62
|
+
|
|
63
|
+
function parseArgs(argv) {
|
|
64
|
+
const args = { _: [], flags: {} };
|
|
65
|
+
for (let i = 0; i < argv.length; i++) {
|
|
66
|
+
const a = argv[i];
|
|
67
|
+
if (a === "--yes") { args.flags.yes = true; }
|
|
68
|
+
else if (a === "--unsafe-paths") { args.flags.unsafePaths = true; }
|
|
69
|
+
else if (a === "--help" || a === "-h") { args.flags.help = true; }
|
|
70
|
+
else if (a === "--version" || a === "-v") { args.flags.version = true; }
|
|
71
|
+
else if (a === "--cwd") { args.flags.cwd = argv[++i]; }
|
|
72
|
+
else if (a === "--max-turns") { args.flags.maxTurns = parseInt(argv[++i], 10); }
|
|
73
|
+
else if (a.startsWith("--")) {
|
|
74
|
+
const eq = a.indexOf("=");
|
|
75
|
+
if (eq >= 0) args.flags[a.slice(2, eq)] = a.slice(eq + 1);
|
|
76
|
+
else args.flags[a.slice(2)] = true;
|
|
77
|
+
} else {
|
|
78
|
+
args._.push(a);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return args;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function die(msg, code = 1) {
|
|
85
|
+
process.stderr.write(errorLine(msg) + "\n");
|
|
86
|
+
process.exit(code);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function main() {
|
|
90
|
+
const args = parseArgs(process.argv.slice(2));
|
|
91
|
+
|
|
92
|
+
if (args.flags.help) {
|
|
93
|
+
process.stdout.write(HELP);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (args.flags.version) {
|
|
97
|
+
process.stdout.write(`aether-code ${VERSION}\n`);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const cwd = args.flags.cwd ? path.resolve(args.flags.cwd) : process.cwd();
|
|
102
|
+
const autoYes = !!args.flags.yes;
|
|
103
|
+
const unsafePaths = !!args.flags.unsafePaths;
|
|
104
|
+
const maxTurns = Number.isInteger(args.flags.maxTurns) ? args.flags.maxTurns : 25;
|
|
105
|
+
|
|
106
|
+
// Subcommand routing — these shadow the "task as positional arg" mode
|
|
107
|
+
const sub = args._[0]?.toLowerCase();
|
|
108
|
+
if (sub === "login" || sub === "auth") {
|
|
109
|
+
const ok = await runSetup();
|
|
110
|
+
process.exit(ok ? 0 : 1);
|
|
111
|
+
}
|
|
112
|
+
if (sub === "logout") {
|
|
113
|
+
writeConfigFile({ apiKey: "" });
|
|
114
|
+
console.log(c.gray(`Cleared API key from ${CONFIG_PATH}.`));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (sub === "config") {
|
|
118
|
+
await handleConfig(args._.slice(1));
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (sub === "balance") {
|
|
122
|
+
await handleBalance();
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const prompt = args._.join(" ").trim();
|
|
127
|
+
|
|
128
|
+
// No task → drop into interactive REPL (Claude-CLI-style)
|
|
129
|
+
if (!prompt) {
|
|
130
|
+
if (cwd !== process.cwd()) process.chdir(cwd);
|
|
131
|
+
await runRepl({ cwd, autoYes, maxTurns });
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// One-shot mode also needs an API key. If missing, run setup before the task.
|
|
136
|
+
const cfg = getConfig();
|
|
137
|
+
if (!cfg.apiKey) {
|
|
138
|
+
const ok = await runSetup();
|
|
139
|
+
if (!ok) process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
console.log(divider());
|
|
143
|
+
console.log(c.magenta(c.bold("aether-code")) + c.gray(` · cwd ${cwd}${autoYes ? " · auto-yes" : ""}${unsafePaths ? " · unsafe-paths" : ""}`));
|
|
144
|
+
console.log(c.gray(`task: `) + prompt);
|
|
145
|
+
console.log(divider());
|
|
146
|
+
|
|
147
|
+
const result = await runAgent({
|
|
148
|
+
initialPrompt: prompt,
|
|
149
|
+
cwd,
|
|
150
|
+
autoYes,
|
|
151
|
+
unsafePaths,
|
|
152
|
+
maxTurns,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
console.log("\n" + divider());
|
|
156
|
+
if (result.ok) {
|
|
157
|
+
console.log(c.green(c.bold("✓ Done")) + c.gray(` ${result.turns} turn${result.turns === 1 ? "" : "s"} · ${result.totalCredits} credits · ${result.totalIn}→${result.totalOut} tokens`));
|
|
158
|
+
if (typeof result.balance === "number") {
|
|
159
|
+
console.log(c.gray(` balance: ${result.balance.toLocaleString()} credits`));
|
|
160
|
+
}
|
|
161
|
+
} else {
|
|
162
|
+
console.log(c.red(c.bold("✗ Stopped")) + c.gray(` ${result.totalCredits} credits used · ${result.totalIn}→${result.totalOut} tokens`));
|
|
163
|
+
if (result.error) console.log(errorLine(result.error.message));
|
|
164
|
+
}
|
|
165
|
+
console.log(divider());
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function handleConfig(rest) {
|
|
169
|
+
const sub = (rest[0] || "").toLowerCase();
|
|
170
|
+
if (sub === "show" || !sub) {
|
|
171
|
+
const cfg = getConfig();
|
|
172
|
+
console.log(`Config file: ${cfg.configPath}`);
|
|
173
|
+
console.log(`API key: ${cfg.apiKey ? cfg.apiKey.slice(0, 12) + "…" + cfg.apiKey.slice(-4) : c.gray("(none)")}`);
|
|
174
|
+
console.log(`Base URL: ${cfg.baseUrl}`);
|
|
175
|
+
console.log(`Source: ${process.env.AETHER_API_KEY ? "AETHER_API_KEY env" : "config file"}`);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (sub === "set") {
|
|
179
|
+
const key = rest[1];
|
|
180
|
+
if (!key) die("config set: missing API key argument.");
|
|
181
|
+
if (!key.startsWith("ak_live_")) {
|
|
182
|
+
process.stderr.write(c.yellow("warning: keys normally start with ak_live_; saving anyway.\n"));
|
|
183
|
+
}
|
|
184
|
+
writeConfigFile({ apiKey: key });
|
|
185
|
+
console.log(`${c.green("✓")} API key saved to ${CONFIG_PATH}`);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (sub === "set-base") {
|
|
189
|
+
const url = rest[1];
|
|
190
|
+
if (!url) die("config set-base: missing URL argument.");
|
|
191
|
+
writeConfigFile({ baseUrl: url });
|
|
192
|
+
console.log(`${c.green("✓")} Base URL saved.`);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (sub === "path") {
|
|
196
|
+
console.log(CONFIG_PATH);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
die(`config: unknown subcommand "${sub}". Try 'config show', 'config set <key>', 'config path'.`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function handleBalance() {
|
|
203
|
+
try {
|
|
204
|
+
const me = await fetchBalance();
|
|
205
|
+
console.log(c.bold(c.magenta("Aether")));
|
|
206
|
+
console.log(c.gray("─".repeat(50)));
|
|
207
|
+
console.log(`Plan ${c.cyan(me.plan)}${me.role !== "USER" ? c.gray(` · ${me.role}`) : ""}`);
|
|
208
|
+
console.log(`Balance ${c.bold(me.balance.toLocaleString())} credits`);
|
|
209
|
+
console.log(` plan ${me.planCredits.toLocaleString()}`);
|
|
210
|
+
console.log(` topup ${me.topupCredits.toLocaleString()}`);
|
|
211
|
+
if (me.rate) {
|
|
212
|
+
console.log(`Rate ${me.rate.used}/${me.rate.limit} this hour${me.rate.resetIn ? ` · resets in ${me.rate.resetIn}s` : ""}`);
|
|
213
|
+
}
|
|
214
|
+
if (me.isSuspended) console.log(c.red("\n⚠ Account is suspended."));
|
|
215
|
+
} catch (err) {
|
|
216
|
+
if (err instanceof AetherError && err.code === "NO_API_KEY") {
|
|
217
|
+
console.log(errorLine("No API key. Run `aether login` first."));
|
|
218
|
+
} else {
|
|
219
|
+
die(err.message || String(err));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
main().catch((err) => {
|
|
225
|
+
console.error(errorLine(err.message || String(err)));
|
|
226
|
+
if (process.env.DEBUG) console.error(err);
|
|
227
|
+
process.exit(1);
|
|
228
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "aether-code",
|
|
3
|
+
"version": "0.3.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"
|
|
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
|
+
}
|
package/src/agent.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
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 { c, divider, turn, toolHeader, toolResult, errorLine } from "./render.js";
|
|
8
|
+
|
|
9
|
+
const DEFAULT_MAX_TURNS = 25;
|
|
10
|
+
|
|
11
|
+
export async function runAgent({
|
|
12
|
+
initialPrompt,
|
|
13
|
+
priorMessages,
|
|
14
|
+
cwd,
|
|
15
|
+
autoYes = false,
|
|
16
|
+
unsafePaths = false,
|
|
17
|
+
maxTurns = DEFAULT_MAX_TURNS,
|
|
18
|
+
onTokens = () => {},
|
|
19
|
+
}) {
|
|
20
|
+
// Two callers: one-shot (initialPrompt only, fresh conversation) and REPL
|
|
21
|
+
// (priorMessages + initialPrompt to continue an ongoing chat).
|
|
22
|
+
const messages = priorMessages
|
|
23
|
+
? [...priorMessages, { role: "user", content: initialPrompt }]
|
|
24
|
+
: [{ role: "user", content: initialPrompt }];
|
|
25
|
+
let totalCredits = 0;
|
|
26
|
+
let totalIn = 0;
|
|
27
|
+
let totalOut = 0;
|
|
28
|
+
let lastBalance = null;
|
|
29
|
+
|
|
30
|
+
for (let i = 0; i < maxTurns; i++) {
|
|
31
|
+
process.stdout.write("\n" + turn(i + 1) + "\n");
|
|
32
|
+
|
|
33
|
+
// Stream the assistant's response. Print text deltas as they arrive,
|
|
34
|
+
// along with tool-call announcements as soon as the model commits to
|
|
35
|
+
// calling a particular tool (i.e. the `name` arrives in the stream).
|
|
36
|
+
const announced = new Set();
|
|
37
|
+
let lastWasText = false;
|
|
38
|
+
|
|
39
|
+
let res;
|
|
40
|
+
try {
|
|
41
|
+
res = await agentTurnStream({
|
|
42
|
+
messages,
|
|
43
|
+
tools: TOOL_DEFINITIONS,
|
|
44
|
+
onDelta: (text) => {
|
|
45
|
+
if (!lastWasText) {
|
|
46
|
+
process.stdout.write(" ");
|
|
47
|
+
lastWasText = true;
|
|
48
|
+
}
|
|
49
|
+
process.stdout.write(text);
|
|
50
|
+
},
|
|
51
|
+
onToolCallDelta: (delta) => {
|
|
52
|
+
// Print the tool header once we know the name (first chunk for that index)
|
|
53
|
+
if (delta.name && !announced.has(delta.index)) {
|
|
54
|
+
announced.add(delta.index);
|
|
55
|
+
if (lastWasText) process.stdout.write("\n");
|
|
56
|
+
lastWasText = false;
|
|
57
|
+
process.stdout.write(c.cyan(c.bold(delta.name)) + c.gray("(...)") + c.gray(" preparing args\n"));
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
} catch (err) {
|
|
62
|
+
if (err instanceof AetherError) {
|
|
63
|
+
return { ok: false, error: err, totalCredits, totalIn, totalOut, balance: lastBalance, messages };
|
|
64
|
+
}
|
|
65
|
+
throw err;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// End-of-turn newline + cost meter
|
|
69
|
+
if (lastWasText) process.stdout.write("\n");
|
|
70
|
+
totalCredits += res.creditsCharged ?? 0;
|
|
71
|
+
totalIn += res.usage?.prompt_tokens ?? 0;
|
|
72
|
+
totalOut += res.usage?.completion_tokens ?? 0;
|
|
73
|
+
if (typeof res.balanceAfter === "number") lastBalance = res.balanceAfter;
|
|
74
|
+
onTokens({ totalCredits, totalIn, totalOut, balance: lastBalance });
|
|
75
|
+
process.stdout.write(
|
|
76
|
+
c.dim(` ${res.creditsCharged ?? 0} cr · ${res.usage?.prompt_tokens ?? 0}→${res.usage?.completion_tokens ?? 0} tokens · finish: ${res.finish_reason}\n`),
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
// Push assistant message into history
|
|
80
|
+
messages.push({
|
|
81
|
+
role: "assistant",
|
|
82
|
+
content: res.message.content,
|
|
83
|
+
tool_calls: res.message.tool_calls,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const toolCalls = res.message.tool_calls ?? [];
|
|
87
|
+
if (toolCalls.length === 0) {
|
|
88
|
+
return { ok: true, totalCredits, totalIn, totalOut, turns: i + 1, balance: lastBalance, messages };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Execute each tool call. Show the actual args (now that we have them
|
|
92
|
+
// fully assembled) and run.
|
|
93
|
+
for (const call of toolCalls) {
|
|
94
|
+
let args = {};
|
|
95
|
+
try { args = JSON.parse(call.function.arguments || "{}"); } catch { /* leave empty */ }
|
|
96
|
+
console.log("");
|
|
97
|
+
console.log(toolHeader(call.function.name, args));
|
|
98
|
+
|
|
99
|
+
const result = await executeTool(call, { cwd, autoYes, unsafePaths });
|
|
100
|
+
if (result.output) {
|
|
101
|
+
const preview = result.output.length > 800 ? result.output.slice(0, 800) + "\n…(truncated)" : result.output;
|
|
102
|
+
console.log(toolResult(preview, result.ok));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
messages.push({
|
|
106
|
+
role: "tool",
|
|
107
|
+
tool_call_id: call.id,
|
|
108
|
+
content: result.output ?? (result.ok ? "(no output)" : "Failed."),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
console.log(c.yellow(`\nReached max turns (${maxTurns}). Stopping.`));
|
|
114
|
+
return { ok: false, error: new Error("Max turns reached"), totalCredits, totalIn, totalOut, balance: lastBalance, messages };
|
|
115
|
+
}
|