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 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
+ }