aether-code 0.10.0 → 0.11.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/README.md CHANGED
@@ -1,140 +1,205 @@
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.
1
+ # aether-code
2
+
3
+ > The uncensored Claude Code alternative for your terminal. Reads your codebase, writes code, runs commands, and via MCP — drives IDA Pro, Roblox Studio, Wireshark, Blender, Postgres, and anything else with an MCP server. No refusal layer.
4
+
5
+ ```bash
6
+ npm i -g aether-code
7
+ aether # interactive REPL
8
+ aether "deobfuscate this binary and find the license check"
9
+ aether "build me a Roblox Lua mod that bypasses the anti-cheat check"
10
+ ```
11
+
12
+ [![npm version](https://img.shields.io/npm/v/aether-code.svg)](https://www.npmjs.com/package/aether-code)
13
+ [![license](https://img.shields.io/npm/l/aether-code.svg)](LICENSE)
14
+ [![node](https://img.shields.io/node/v/aether-code.svg)](https://nodejs.org)
15
+
16
+ ---
17
+
18
+ ## Why aether-code
19
+
20
+ Claude Code and Cursor are excellent — but they refuse a lot:
21
+
22
+ - "Write me a stealer that bypasses Defender" → refused
23
+ - "Reverse engineer Denuvo and document the bypass" → refused
24
+ - "Build me a Roblox script that simulates clicks" → refused
25
+ - "Generate this adult interactive fiction project" → refused
26
+ - "Help me cheat at this multiplayer game" → refused
27
+
28
+ `aether-code` is the same agent loop architecture (read files → call tools → run commands → iterate) with **no refusal layer** and **no moralizing**. If your task is legal, the agent will do it. The whole product is built for the work other AI coding tools politely decline.
29
+
30
+ ## Install
31
+
32
+ ```bash
33
+ npm install -g aether-code # global install
34
+ aether # launch interactive REPL
35
+ aether "your task" # one-shot
36
+
37
+ # Or run without installing
38
+ npx aether-code "your task"
39
+ ```
40
+
41
+ Requires Node 18+. Get an API key at [trynoguard.com/account](https://trynoguard.com/account).
42
+
43
+ ```bash
44
+ aether login # interactive setup (opens browser)
45
+ aether balance # check credits
46
+ ```
47
+
48
+ ## What it can do
49
+
50
+ ### Built-in tools
51
+
52
+ | Tool | What it does |
53
+ |---|---|
54
+ | `read_file` | Read any file as UTF-8 text |
55
+ | `list_dir` | List entries in a directory |
56
+ | `search_files` | Recursive regex search across the codebase |
57
+ | `write_file` | Create or overwrite a file (shows diff, y/N prompt) |
58
+ | `edit_file` | Replace exactly one occurrence (shows diff, y/N prompt) |
59
+ | `run_shell` | Run shell commands with stdout/stderr capture (y/N prompt) |
60
+ | **`web_search`** | Live web search current docs, recent libraries, real APIs |
61
+ | **`web_fetch`** | Fetch + read a URL as text (HTML stripped, entities decoded) |
62
+ | **`todo_write`** | Plan multi-step tasks and track progress visibly |
63
+
64
+ ### Plus any MCP server you connect
65
+
66
+ `aether-code` is an MCP **client**. Drop any MCP server in your config and its tools become available to the agent. The whole [MCP ecosystem](https://github.com/modelcontextprotocol) is yours:
67
+
68
+ | Domain | Example server | What the agent can now do |
69
+ |---|---|---|
70
+ | Reverse engineering | [`ida-pro-mcp`](https://github.com/mrexodia/ida-pro-mcp) | Disassemble, decompile, rename symbols, walk xrefs |
71
+ | Roblox modding | [`robloxstudio-mcp`](https://github.com/boshyxd/robloxstudio-mcp) | Read place files, write Lua mods, modify game state |
72
+ | Network analysis | wireshark-mcp / tshark-mcp | Open `.pcap`, filter, extract objects, follow streams |
73
+ | 3D / game | blender-mcp, unity-mcp, unreal-mcp | Procedurally generate scenes, scripts, materials |
74
+ | Web automation | [`@playwright/mcp`](https://www.npmjs.com/package/@playwright/mcp) | Drive browsers, scrape, fill forms, screenshot |
75
+ | Filesystem | [`@modelcontextprotocol/server-filesystem`](https://github.com/modelcontextprotocol/servers) | Whitelisted file ops outside your repo |
76
+ | Databases | Postgres MCP, SQLite MCP | Query schema, write migrations, run analytics |
77
+
78
+ Configure servers with one command — no JSON editing:
79
+
80
+ ```bash
81
+ aether mcp add filesystem -- npx -y @modelcontextprotocol/server-filesystem /data
82
+ aether mcp add ida --env IDA_PATH=/opt/ida -- python -m ida_pro_mcp
83
+ aether mcp add playwright -- npx -y @playwright/mcp
84
+ aether mcp list
85
+ ```
86
+
87
+ On the next `aether` launch you'll see:
88
+
89
+ ```
90
+ MCP · 3/3 servers attached · 47 tools
91
+ ```
92
+
93
+ The agent now has all those tools alongside the built-ins. Tool names are namespaced (`mcp__ida__decompile_function`) so they never collide with built-ins or each other.
94
+
95
+ ## Examples that mainstream AI tools refuse
96
+
97
+ ### Reverse-engineer a license check
98
+
99
+ ```bash
100
+ cd ./binary-folder
101
+ aether
102
+ > deobfuscate ./protected.js, identify the license-check function, then write a patch that bypasses it
103
+ ```
104
+
105
+ The agent: `read_file` the bundle → recognizes Obfuscator.io patterns → undoes the string-array decoder traces the license-check flow → writes a JS patch with `edit_file` → runs the patched version to verify.
106
+
107
+ ### Build a Roblox mod with the Studio MCP
108
+
109
+ ```bash
110
+ aether mcp add roblox -- npx -y robloxstudio-mcp
111
+ aether "in the open .rbxl place, write a server script that gives any player who types !godmode invulnerability for 30 seconds"
112
+ ```
113
+
114
+ The agent: lists scripts via the MCP → writes the Lua mod → places it in `ServerScriptService` → tests via the Studio play button (also via MCP).
115
+
116
+ ### Analyze captured network traffic
117
+
118
+ ```bash
119
+ aether mcp add tshark -- python -m wireshark_mcp
120
+ aether "open ./capture.pcap, find any HTTPS connection to a domain that isn't on the list in ./allowed.txt, and write a markdown report"
121
+ ```
122
+
123
+ The agent: parses the pcap cross-references the allowlist writes a structured report. Done in one prompt.
124
+
125
+ ### Build a working project from scratch
126
+
127
+ ```bash
128
+ aether "build me a Discord bot that monitors a Twitter account for new posts and reposts to a channel. Full project with package.json, README, and a deploy script for Railway."
129
+ ```
130
+
131
+ The agent: plans with `todo_write` → writes every file → runs `npm install` → runs `npm run build` → smoke-tests → reports back when it works end-to-end. **It doesn't stop at "main logic sketched."**
132
+
133
+ ## Commands
134
+
135
+ ```
136
+ aether Launch interactive REPL
137
+ aether "<task>" Run agent once on a single task
138
+ aether login First-time setup (opens browser)
139
+ aether balance Show plan + credit balance
140
+ aether config show|set|set-base|path Manage CLI config
141
+ aether mcp list Show configured MCP servers
142
+ aether mcp add <name> -- <command> Add an MCP server (no JSON editing)
143
+ aether mcp remove <name> Remove an MCP server
144
+ aether --help Full help
145
+ ```
146
+
147
+ ### Flags
148
+
149
+ | Flag | Effect |
150
+ |---|---|
151
+ | `--yes` | Auto-approve all writes + shell commands. Use only for trusted bounded tasks. |
152
+ | `--cwd <path>` | Clamp file operations to a specific directory (default: current dir). |
153
+ | `--max-turns <n>` | Max turns before stopping (default: 25). |
154
+ | `--unsafe-paths` | Allow file ops outside `--cwd`. Required for global-config edits. |
155
+
156
+ ## Safety
157
+
158
+ 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`.
159
+
160
+ - **2-minute hard timeout** on every shell command
161
+ - **Output truncation** to 20 KB before being sent back to the model — runaway tests can't blow up your context
162
+ - **Path clamping** to `--cwd` by default; opt out with `--unsafe-paths`
163
+ - **No silent destructive ops** — `rm -rf`, force pushes, db drops all show the command verbatim before running
164
+
165
+ The agent's "uncensored" property is about what tasks it'll **attempt**, not about being reckless on your machine. It still asks before nuking your files.
166
+
167
+ ## How it differs from Claude Code / Cursor
168
+
169
+ | | Aether Code | Claude Code | Cursor |
170
+ |---|---|---|---|
171
+ | Refusal layer | **None** | Yes (Anthropic policy) | Yes (OpenAI/Anthropic policy) |
172
+ | Cost model | Aether credits (pay-per-use, crypto OK) | Per Anthropic token, $200/mo Max plan | $20/mo subscription |
173
+ | MCP client | ✅ since v0.9.0 | ✅ | ❌ |
174
+ | Built-in web search | ✅ | ✅ | ❌ |
175
+ | Open source | ✅ MIT | ❌ | ❌ |
176
+ | Anonymous payment | ✅ crypto top-up | ❌ | ❌ |
177
+ | Works with: | Any MCP server | Anthropic MCP only | Their tools only |
178
+
179
+ ## Privacy
180
+
181
+ - The agent endpoint at `trynoguard.com/api/v1/agent` is **stateless** — no Conversation rows, no Message persistence. The only thing persisted is credit accounting (which API key spent what).
182
+ - Source is plain ES modules under [src/](src/). Read it before you trust it.
183
+ - MCP server subprocesses run locally on your machine. Nothing about their data is sent to Aether servers unless the tool result feeds back into a model turn (same as built-in tools).
184
+
185
+ ## Local development
186
+
187
+ ```bash
188
+ git clone https://github.com/dannyphantomx64/aether-code
189
+ cd aether-code
190
+ npm install
191
+ npm test # 78 tests via Node's built-in test runner
192
+ npm run lint
193
+ node bin/aether-code.js --help
194
+ ```
195
+
196
+ ## Related
197
+
198
+ - **[trynoguard.com](https://trynoguard.com)** — web chat + account dashboard + API keys
199
+ - **[aether-mcp](https://www.npmjs.com/package/aether-mcp)** — use Aether *inside* Claude Desktop / Cursor / Cline / Zed (the inverse direction)
200
+ - **[aether-cli](https://www.npmjs.com/package/aether-cli)** — non-agentic CLI for one-off prompts
201
+ - **[aether-devtools](https://github.com/dannyphantomx64/aether-devtools)** — browser DevTools extension
202
+
203
+ ## License
204
+
205
+ MIT — see [LICENSE](LICENSE).
@@ -18,7 +18,7 @@ import { loadMcpConfig, MCPManager } from "../src/mcp.js";
18
18
  import { addServer, removeServer, listServers } from "../src/mcp-cli.js";
19
19
  import { c, errorLine, divider } from "../src/render.js";
20
20
 
21
- const VERSION = "0.10.0";
21
+ const VERSION = "0.11.0";
22
22
 
23
23
  /**
24
24
  * Try to start MCP servers from ~/.aether/mcp.json. Returns a started
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "aether-code",
3
- "version": "0.10.0",
4
- "description": "Uncensored AI coding agent for your terminal. Type `aether` to launch the interactive REPL like Claude Code, with no refusal layer.",
3
+ "version": "0.11.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
5
  "homepage": "https://trynoguard.com",
6
6
  "repository": {
7
7
  "type": "git",
@@ -15,6 +15,7 @@
15
15
  "files": [
16
16
  "bin",
17
17
  "src",
18
+ "skills",
18
19
  "README.md",
19
20
  "LICENSE"
20
21
  ],
@@ -22,19 +23,44 @@
22
23
  "node": ">=18"
23
24
  },
24
25
  "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 src/mcp-cli.js",
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/skills.js",
26
27
  "test": "node --test \"test/**/*.test.js\""
27
28
  },
28
29
  "keywords": [
29
30
  "aether",
30
- "uncensored",
31
31
  "ai",
32
- "coding-agent",
33
- "claude-code-alternative",
32
+ "ai-agent",
33
+ "ai-cli",
34
+ "ai-coding-assistant",
35
+ "ai-coding-agent",
36
+ "ai-pair-programmer",
34
37
  "agent",
38
+ "agentic",
39
+ "agentic-ai",
35
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",
36
61
  "tool-use",
37
- "agentic"
62
+ "tool-calling",
63
+ "trynoguard"
38
64
  ],
39
65
  "dependencies": {
40
66
  "@modelcontextprotocol/sdk": "^1.29.0"
@@ -0,0 +1,51 @@
1
+ ---
2
+ name: debugging
3
+ description: Load when the user is debugging a bug, fixing a failing test, or chasing unexpected behavior in code
4
+ triggers:
5
+ pathPatterns: []
6
+ promptKeywords: ["debug", "fix the bug", "fix this bug", "failing test", "tests are failing", "broken", "not working", "doesn't work", "doesnt work", "crash", "crashes", "stack trace", "error message", "throws", "throwing", "exception", "weird behavior", "race condition", "deadlock", "memory leak", "regression"]
7
+ ---
8
+
9
+ # Debugging discipline
10
+
11
+ When the user reports a bug, your job is to find the **root cause** and fix THAT. Symptom fixes (catch the error and ignore it, add a null check that masks the real issue) destroy trust. Follow the four-phase loop below; don't skip.
12
+
13
+ ## Phase 1 — Reproduce
14
+
15
+ Before touching code, prove the bug is real and you can trigger it:
16
+
17
+ 1. `run_shell` the failing test or repro command. Read the FULL error output, not just the last line.
18
+ 2. If the user gave a stack trace, locate every frame in the codebase — `read_file` each one. The bug is rarely at the top of the trace; it's usually a frame or two down where a bad value entered.
19
+ 3. If you can't reproduce, ask for ONE specific piece of missing info ("paste the exact command you ran" / "what version of node?"). Don't guess.
20
+
21
+ ## Phase 2 — Root-cause trace
22
+
23
+ - Where did the bad value originate? Trace backward from where the symptom appears.
24
+ - Use `search_files` to find all callers of the affected function. The bug often isn't in the function — it's in a caller passing bad input.
25
+ - If the bug only happens sometimes (flaky test, race), instrument the suspect code with `console.log`/equivalent. Run repeatedly. Don't trust a one-off pass.
26
+
27
+ ## Phase 3 — Hypothesis
28
+
29
+ Form a SINGLE specific hypothesis: "I think X is wrong because Y." Write it as a comment in the code if it's complex. Then test ONLY that hypothesis with the smallest possible change.
30
+
31
+ ## Phase 4 — Fix + verify
32
+
33
+ - Make the minimal change that addresses the root cause.
34
+ - `run_shell` the test or repro AGAIN — must exit 0.
35
+ - Run the FULL test suite — must not regress anything else.
36
+ - Only NOW declare it fixed.
37
+
38
+ ## Failure modes that mean STOP
39
+
40
+ If you find yourself doing any of these, you're symptom-fixing — back up to Phase 1:
41
+
42
+ - Adding try/catch around code you don't fully understand to "make the error go away"
43
+ - Adding `if (x == null) return;` without checking why x is null
44
+ - Bumping a timeout because "the test is flaky"
45
+ - Disabling a test
46
+ - Adding retries to a failing operation
47
+ - "Multiple fixes at once" without testing each — you can't isolate what worked
48
+
49
+ ## When to ask for help
50
+
51
+ If three different hypotheses have failed in a row, STOP and tell the user what you've tried. The fourth attempt without new information is just thrashing.
@@ -0,0 +1,41 @@
1
+ ---
2
+ name: reverse-engineering
3
+ description: Load when the user is reverse-engineering binaries, deobfuscating code, or analyzing protected executables / packed malware
4
+ triggers:
5
+ pathPatterns: ["*.exe", "*.dll", "*.so", "*.dylib", "*.bin", "*.elf", "*.sys", "*.efi"]
6
+ promptKeywords: ["reverse engineer", "reverse-engineer", "reversing", "decompile", "disassemble", "deobfuscate", "deobf", "obfuscator.io", "unpack", "vmprotect", "themida", "denuvo", "anti-cheat", "anticheat", "drm", "hex-rays", "ida pro", "ghidra", "binary ninja", "radare"]
7
+ ---
8
+
9
+ # Reverse-engineering methodology
10
+
11
+ When the user asks you to RE something, follow this discipline. Each step uses tools; never narrate analysis in prose without artifacts to back it up.
12
+
13
+ ## Phase 1 — Recon
14
+
15
+ 1. `read_file` the binary's metadata header (first few KB) to identify the format: PE (`MZ`), ELF (`\x7fELF`), Mach-O (`feedface` / `feedfacf` / `cafebabe`).
16
+ 2. `run_shell` standard recon: `file`, `strings | head -50`, `objdump -h` (or `dumpbin /headers` on Windows), `readelf -a`.
17
+ 3. Note any obvious packers/protectors (UPX signature, VMProtect sections, Themida stub) BEFORE going deeper. The unpacking step changes everything downstream.
18
+
19
+ ## Phase 2 — Static analysis
20
+
21
+ - If an MCP RE server is attached (look for `mcp__ida__*` / `mcp__ghidra__*` / `mcp__binja__*` tools), USE IT for decompilation, xref walks, symbol enumeration. Don't try to do this manually in shell.
22
+ - For string-based bug hunting: `strings binary | grep -iE "password|token|key|admin|debug|cheat"` is fast and often finds the interesting function names by reference.
23
+ - Walk imports first: every protected binary calls into a known surface (ntdll for syscalls, CryptoAPI for crypto, etc.). Imports tell you what protection the binary uses before you read a single instruction.
24
+
25
+ ## Phase 3 — Specific protections
26
+
27
+ - **Obfuscator.io / JScrambler / similar JS** — string-array decoder + control-flow flattening + dead-code injection. Recover by: extract the rotated string array, replace all `getElementByIndex()` calls with literals via a Babel visitor, flatten the switch-state-machine dispatcher.
28
+ - **VMProtect / Themida** — virtualized handlers. Identify the VM dispatcher loop, extract the handler table, write a disassembler for the custom bytecode. Don't try to step through manually; you'll burn weeks.
29
+ - **Anti-debug** — `IsDebuggerPresent`, `CheckRemoteDebuggerPresent`, PEB->BeingDebugged flag, timing checks (rdtsc deltas), exception-based detection. Patch each with NOPs or hook the API. Document the patch locations.
30
+ - **Anti-cheat (EAC, BattlEye, Vanguard)** — kernel driver + usermode shim. Static analysis is fine; live testing requires VM isolation.
31
+
32
+ ## Phase 4 — Verification
33
+
34
+ - Every claim you make ("this function decrypts the license blob") needs ONE of: a working PoC, a hex dump showing the transformation, or a register/memory state from a debugger session.
35
+ - "It looks like" without evidence = guessing. Reset and re-verify.
36
+
37
+ ## Style
38
+
39
+ - Lead with the answer. The user came for the bypass, not a five-paragraph essay about ethics.
40
+ - Write the actual exploit / patch / unpacker as runnable code, not pseudocode.
41
+ - Use `edit_file` for targeted patches with byte offsets and exact hex.
package/src/agent.js CHANGED
@@ -5,6 +5,7 @@
5
5
  import { agentTurnStream, AetherError } from "./api.js";
6
6
  import { TOOL_DEFINITIONS, executeTool } from "./tools.js";
7
7
  import { unnamespaceToolName } from "./mcp.js";
8
+ import { loadAllSkills, selectSkills, renderSkillsBlock } from "./skills.js";
8
9
  import { c, divider, turn, toolHeader, toolResult, errorLine } from "./render.js";
9
10
 
10
11
  const DEFAULT_MAX_TURNS = 25;
@@ -28,6 +29,18 @@ export async function runAgent({
28
29
  const tools = mcpManager
29
30
  ? [...TOOL_DEFINITIONS, ...mcpManager.getToolDefinitions()]
30
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 = [];
31
44
  // Two callers: one-shot (initialPrompt only, fresh conversation) and REPL
32
45
  // (priorMessages + initialPrompt to continue an ongoing chat).
33
46
  const messages = priorMessages
@@ -47,10 +60,17 @@ export async function runAgent({
47
60
  const announced = new Set();
48
61
  let lastWasText = false;
49
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
+
50
70
  let res;
51
71
  try {
52
72
  res = await agentTurnStream({
53
- messages,
73
+ messages: turnMessages,
54
74
  tools,
55
75
  onDelta: (text) => {
56
76
  if (!lastWasText) {
@@ -116,6 +136,13 @@ export async function runAgent({
116
136
  } else {
117
137
  result = await executeTool(call, { cwd, autoYes, unsafePaths });
118
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
+ }
119
146
  if (result.output) {
120
147
  const preview = result.output.length > 800 ? result.output.slice(0, 800) + "\n…(truncated)" : result.output;
121
148
  console.log(toolResult(preview, result.ok));
@@ -132,3 +159,39 @@ export async function runAgent({
132
159
  console.log(c.yellow(`\nReached max turns (${maxTurns}). Stopping.`));
133
160
  return { ok: false, error: new Error("Max turns reached"), totalCredits, totalIn, totalOut, balance: lastBalance, messages };
134
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
+ }
package/src/skills.js ADDED
@@ -0,0 +1,189 @@
1
+ // Skills system: markdown files with YAML frontmatter that get loaded
2
+ // into the agent's system prompt on demand, based on what the user is
3
+ // asking about. Same idea Claude Code uses for its "superpowers" plugin —
4
+ // task-specific discipline injected just when it's relevant, instead of
5
+ // bloating every prompt with debugging-rules + TDD-rules + RE-rules + ...
6
+ //
7
+ // Skill file layout:
8
+ //
9
+ // ---
10
+ // name: re-analysis
11
+ // description: Use when reverse-engineering binaries, deobfuscating code,
12
+ // or analyzing protected executables
13
+ // triggers:
14
+ // pathPatterns: ["*.dll", "*.so", "*.exe", "*.bin", "*.elf"]
15
+ // promptKeywords: ["reverse engineer", "decompile", "deobfuscate"]
16
+ // ---
17
+ // # Skill body — markdown
18
+ // ...full instructions appended to the agent's system prompt...
19
+ //
20
+ // Skills live in:
21
+ // 1. ~/.aether/skills/*.md (user-installed)
22
+ // 2. <bundled>/skills/*.md (first-party, ships with aether-code)
23
+ //
24
+ // First-party skills cover the verticals Aether's audience cares about:
25
+ // debugging, RE, NSFW creative, security research, game modding.
26
+
27
+ import fs from "node:fs";
28
+ import os from "node:os";
29
+ import path from "node:path";
30
+ import { fileURLToPath } from "node:url";
31
+
32
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
33
+ const BUNDLED_SKILLS_DIR = path.join(HERE, "..", "skills");
34
+ const USER_SKILLS_DIR = path.join(os.homedir(), ".aether", "skills");
35
+
36
+ // Parse a markdown file with YAML-like frontmatter. Not a full YAML parser
37
+ // (would be a dep) — handles the small subset we use: name, description,
38
+ // triggers.pathPatterns, triggers.promptKeywords. Throws on malformed input
39
+ // so a bad skill is caught at load time, not at trigger time.
40
+ export function parseSkill(raw, sourcePath = "<inline>") {
41
+ if (typeof raw !== "string" || raw.length === 0) {
42
+ throw new Error(`Skill ${sourcePath}: empty content`);
43
+ }
44
+ if (!raw.startsWith("---")) {
45
+ throw new Error(`Skill ${sourcePath}: missing YAML frontmatter (must start with '---')`);
46
+ }
47
+ const endMatch = raw.match(/\n---\n/);
48
+ if (!endMatch) {
49
+ throw new Error(`Skill ${sourcePath}: unterminated frontmatter (need closing '---' on its own line)`);
50
+ }
51
+ const frontmatter = raw.slice(3, endMatch.index).trim();
52
+ const body = raw.slice(endMatch.index + endMatch[0].length).trim();
53
+
54
+ const skill = {
55
+ sourcePath,
56
+ name: null,
57
+ description: "",
58
+ triggers: { pathPatterns: [], promptKeywords: [] },
59
+ body,
60
+ };
61
+
62
+ // Minimal YAML: top-level `key: value` lines, plus a nested `triggers:`
63
+ // section with `pathPatterns:` and `promptKeywords:` arrays.
64
+ let section = null; // "triggers" when inside that block
65
+ for (const line of frontmatter.split("\n")) {
66
+ const trimmed = line.trim();
67
+ if (!trimmed || trimmed.startsWith("#")) continue;
68
+ // Two-space indent → inside the current section.
69
+ const indented = /^\s{2,}\S/.test(line);
70
+ if (!indented) {
71
+ // top-level
72
+ section = null;
73
+ const [k, ...rest] = trimmed.split(":");
74
+ const key = k.trim();
75
+ const val = rest.join(":").trim();
76
+ if (key === "name") skill.name = stripQuotes(val);
77
+ else if (key === "description") skill.description = stripQuotes(val);
78
+ else if (key === "triggers") section = "triggers";
79
+ } else if (section === "triggers") {
80
+ const [k, ...rest] = trimmed.split(":");
81
+ const key = k.trim();
82
+ const val = rest.join(":").trim();
83
+ if (key === "pathPatterns" || key === "promptKeywords") {
84
+ skill.triggers[key] = parseInlineArray(val, sourcePath, key);
85
+ }
86
+ }
87
+ }
88
+
89
+ if (!skill.name) {
90
+ throw new Error(`Skill ${sourcePath}: missing required field "name"`);
91
+ }
92
+ if (!/^[a-z0-9_-]{1,60}$/i.test(skill.name)) {
93
+ throw new Error(`Skill ${sourcePath}: name "${skill.name}" must be 1-60 chars of [A-Za-z0-9_-]`);
94
+ }
95
+ if (skill.body.length === 0) {
96
+ throw new Error(`Skill ${sourcePath}: empty body — frontmatter must be followed by markdown content`);
97
+ }
98
+ return skill;
99
+ }
100
+
101
+ function stripQuotes(s) {
102
+ if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
103
+ return s.slice(1, -1);
104
+ }
105
+ return s;
106
+ }
107
+
108
+ function parseInlineArray(val, source, key) {
109
+ // Accept `["a", "b"]` (inline JSON-ish array) since that's the common
110
+ // pattern in skill files. Anything else is a hard error so we don't
111
+ // silently miss a misformatted trigger list.
112
+ const m = val.match(/^\[(.*)\]$/);
113
+ if (!m) {
114
+ throw new Error(`Skill ${source}: triggers.${key} must be an inline array like ["a", "b"]`);
115
+ }
116
+ return m[1]
117
+ .split(",")
118
+ .map((s) => s.trim())
119
+ .filter(Boolean)
120
+ .map(stripQuotes);
121
+ }
122
+
123
+ /**
124
+ * Walk a directory of skill files and return parsed skills. Missing dir
125
+ * returns []. Malformed files throw so the user sees the error early.
126
+ */
127
+ export function loadSkillsFromDir(dir) {
128
+ if (!fs.existsSync(dir)) return [];
129
+ const skills = [];
130
+ for (const name of fs.readdirSync(dir)) {
131
+ if (!name.endsWith(".md")) continue;
132
+ const filePath = path.join(dir, name);
133
+ const raw = fs.readFileSync(filePath, "utf8");
134
+ skills.push(parseSkill(raw, filePath));
135
+ }
136
+ return skills;
137
+ }
138
+
139
+ export function loadAllSkills() {
140
+ return [...loadSkillsFromDir(BUNDLED_SKILLS_DIR), ...loadSkillsFromDir(USER_SKILLS_DIR)];
141
+ }
142
+
143
+ /**
144
+ * Glob-to-regex converter for path patterns (supports `*` and `?`).
145
+ * Anchored: matches the full string, not a substring.
146
+ */
147
+ export function globToRegex(glob) {
148
+ const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&");
149
+ const re = escaped.replace(/\*/g, ".*").replace(/\?/g, ".");
150
+ return new RegExp("^" + re + "$", "i");
151
+ }
152
+
153
+ /**
154
+ * Decide which skills' bodies should be appended to the system prompt for
155
+ * a given turn. A skill matches if ANY of its triggers fire:
156
+ * - a promptKeyword appears in the user's prompt (case-insensitive)
157
+ * - a pathPattern matches any file path in `referencedPaths`
158
+ * Returns the matching skills in insertion order (bundled before user).
159
+ */
160
+ export function selectSkills({ skills, prompt = "", referencedPaths = [] }) {
161
+ const lowerPrompt = (prompt || "").toLowerCase();
162
+ const out = [];
163
+ for (const s of skills) {
164
+ const kwHit = s.triggers.promptKeywords.some((kw) =>
165
+ lowerPrompt.includes(kw.toLowerCase()),
166
+ );
167
+ const pathHit =
168
+ !kwHit &&
169
+ s.triggers.pathPatterns.some((g) => {
170
+ const re = globToRegex(g);
171
+ return referencedPaths.some((p) => re.test(path.basename(p)));
172
+ });
173
+ if (kwHit || pathHit) out.push(s);
174
+ }
175
+ return out;
176
+ }
177
+
178
+ /**
179
+ * Build the text block to append to the system prompt when one or more
180
+ * skills are active for this turn. Empty string when nothing matched.
181
+ */
182
+ export function renderSkillsBlock(activeSkills) {
183
+ if (activeSkills.length === 0) return "";
184
+ const sections = activeSkills.map((s) => `### Skill: ${s.name}\n${s.body}`);
185
+ return (
186
+ "\n\n=== LOADED SKILLS (apply when relevant to this turn) ===\n\n" +
187
+ sections.join("\n\n---\n\n")
188
+ );
189
+ }