aether-code 0.41.0 → 0.43.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 +2 -0
- package/bin/aether-code.js +514 -514
- package/package.json +75 -75
- package/src/agent.js +54 -4
- package/src/api.js +23 -3
- package/src/project-context.js +712 -707
- package/src/render.js +254 -254
- package/src/repl.js +444 -444
- package/src/sessions.js +167 -167
- package/src/setup.js +141 -141
- package/src/tools.js +1288 -1288
- package/src/version.js +15 -15
package/package.json
CHANGED
|
@@ -1,75 +1,75 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "aether-code",
|
|
3
|
-
"version": "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 src/update-check.js src/ink-input.js src/version.js src/sessions.js src/project-context.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
|
-
"ink": "^7.1.0",
|
|
69
|
-
"ink-text-input": "^6.0.0",
|
|
70
|
-
"react": "^19.2.7"
|
|
71
|
-
},
|
|
72
|
-
"devDependencies": {
|
|
73
|
-
"ink-testing-library": "^4.0.0"
|
|
74
|
-
}
|
|
75
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "aether-code",
|
|
3
|
+
"version": "0.43.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 src/update-check.js src/ink-input.js src/version.js src/sessions.js src/project-context.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
|
+
"ink": "^7.1.0",
|
|
69
|
+
"ink-text-input": "^6.0.0",
|
|
70
|
+
"react": "^19.2.7"
|
|
71
|
+
},
|
|
72
|
+
"devDependencies": {
|
|
73
|
+
"ink-testing-library": "^4.0.0"
|
|
74
|
+
}
|
|
75
|
+
}
|
package/src/agent.js
CHANGED
|
@@ -14,6 +14,35 @@ import { c, divider, turn, toolLabel, toolSummary, makeTokenStripper, errorLine,
|
|
|
14
14
|
|
|
15
15
|
const DEFAULT_MAX_TURNS = 25;
|
|
16
16
|
|
|
17
|
+
// Rolling history window (messages) sent upstream per turn. The session keeps
|
|
18
|
+
// the full transcript locally, but we only forward the anchor (first message,
|
|
19
|
+
// which carries the [environment] block) plus the most recent N messages. This
|
|
20
|
+
// bounds per-turn token cost AND keeps us safely under the server's own cap —
|
|
21
|
+
// a long session used to grow unbounded and, past the server's limit, every
|
|
22
|
+
// send failed with "Error: Invalid input." with no recovery but /clear.
|
|
23
|
+
const HISTORY_WINDOW = 100;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Trim a message array to `maxMessages`, preserving:
|
|
27
|
+
* - the anchor message at index 0 (the first user turn carries the
|
|
28
|
+
* [environment] block with real absolute paths — dropping it makes the
|
|
29
|
+
* model lose cwd/home/desktop);
|
|
30
|
+
* - tool-call/tool-result pairing at the window boundary: a `tool` message
|
|
31
|
+
* must immediately follow the assistant message whose `tool_calls` produced
|
|
32
|
+
* it, so the window is never allowed to BEGIN on an orphaned `tool` result.
|
|
33
|
+
* Returns the original array untouched when it's already within the window.
|
|
34
|
+
*/
|
|
35
|
+
export function trimHistory(messages, maxMessages = HISTORY_WINDOW) {
|
|
36
|
+
if (messages.length <= maxMessages) return messages;
|
|
37
|
+
const head = [messages[0]];
|
|
38
|
+
const windowSize = Math.max(1, maxMessages - 1);
|
|
39
|
+
let window = messages.slice(1).slice(-windowSize);
|
|
40
|
+
let start = 0;
|
|
41
|
+
while (start < window.length && window[start]?.role === "tool") start++;
|
|
42
|
+
window = window.slice(start);
|
|
43
|
+
return [...head, ...window];
|
|
44
|
+
}
|
|
45
|
+
|
|
17
46
|
export const MOD_RE =
|
|
18
47
|
/\b(refactor|fix|add|adds?|change|update|implement|creat|writ|remov|delet|rename|extract|move|replace|build|generate|insert|append|convert|migrat|wire|hook|edit|modif|patch|integrat)/i;
|
|
19
48
|
|
|
@@ -130,7 +159,7 @@ export async function runAgent({
|
|
|
130
159
|
// to the last user message of a shallow-cloned messages array — we
|
|
131
160
|
// don't want skill text accumulating in the persisted history, only
|
|
132
161
|
// being available to the model for the turn where it's relevant.
|
|
133
|
-
const turnMessages = buildTurnMessages(messages, allSkills, referencedPaths);
|
|
162
|
+
const turnMessages = trimHistory(buildTurnMessages(messages, allSkills, referencedPaths));
|
|
134
163
|
|
|
135
164
|
let res;
|
|
136
165
|
// "Thinking" spinner: shown from request-send until the first token or tool
|
|
@@ -383,8 +412,23 @@ export async function runAgent({
|
|
|
383
412
|
return { ok: false, error: new Error("Max turns reached"), totalCredits, totalIn, totalOut, balance: lastBalance, messages };
|
|
384
413
|
}
|
|
385
414
|
|
|
415
|
+
// A build/verify command failing because the TOOLCHAIN ITSELF is missing (e.g.
|
|
416
|
+
// g++/cargo/dotnet not installed) is not a code bug — it must NOT be reported as
|
|
417
|
+
// "build failed" and fed back to the model, which would derail the run into
|
|
418
|
+
// 5 rounds of "fixing" code that's actually fine. These are the shell's
|
|
419
|
+
// command-not-found messages (Windows cmd.exe + POSIX sh); real compiler
|
|
420
|
+
// diagnostics look like `main.cpp:5:10: error: ...` and never match these.
|
|
421
|
+
// Matches ONLY the shell's "I can't find this executable" messages — Windows
|
|
422
|
+
// cmd.exe ("'g++' is not recognized…") and POSIX sh ("g++: command not found",
|
|
423
|
+
// "sh: 1: g++: not found"). Real compiler diagnostics (`main.cpp:5: error:`,
|
|
424
|
+
// `fatal error: foo.h: No such file or directory`) never match these, so a
|
|
425
|
+
// genuine build error is still reported and fixed.
|
|
426
|
+
const TOOLCHAIN_MISSING_RE =
|
|
427
|
+
/is not recognized as an internal or external command|: command not found|: not found\b/i;
|
|
428
|
+
|
|
386
429
|
// Auto-verify: run the project's typecheck/lint command silently and return
|
|
387
|
-
// { ok, output, label }. Returns null if no verify command is detected
|
|
430
|
+
// { ok, output, label }. Returns null if no verify command is detected OR the
|
|
431
|
+
// toolchain isn't installed (skip, don't report a false failure).
|
|
388
432
|
let cachedVerify = undefined;
|
|
389
433
|
async function runVerify(cwd) {
|
|
390
434
|
if (cachedVerify === undefined) cachedVerify = detectVerifyCommand(cwd);
|
|
@@ -400,6 +444,8 @@ async function runVerify(cwd) {
|
|
|
400
444
|
child.on("close", (code) => {
|
|
401
445
|
clearTimeout(timer);
|
|
402
446
|
const output = (stdout + "\n" + stderr).trim();
|
|
447
|
+
// Toolchain not installed → skip silently rather than nag the model.
|
|
448
|
+
if (code !== 0 && TOOLCHAIN_MISSING_RE.test(output)) { resolve(null); return; }
|
|
403
449
|
resolve({ ok: code === 0, output, label });
|
|
404
450
|
});
|
|
405
451
|
child.on("error", () => {
|
|
@@ -437,11 +483,15 @@ async function runBuildCmd(cwd, { cmd, label, type, runCmd }) {
|
|
|
437
483
|
child.on("close", (code) => {
|
|
438
484
|
clearTimeout(timer);
|
|
439
485
|
const output = (stdout + "\n" + stderr).trim();
|
|
486
|
+
// Compiler/toolchain not installed → skip auto-build silently instead of
|
|
487
|
+
// reporting a build failure and derailing the run.
|
|
488
|
+
if (code !== 0 && TOOLCHAIN_MISSING_RE.test(output)) { resolve(null); return; }
|
|
440
489
|
resolve({ ok: code === 0, output, label, type, runCmd });
|
|
441
490
|
});
|
|
442
|
-
child.on("error", (
|
|
491
|
+
child.on("error", () => {
|
|
443
492
|
clearTimeout(timer);
|
|
444
|
-
|
|
493
|
+
// ENOENT (command not found) → toolchain missing → skip.
|
|
494
|
+
resolve(null);
|
|
445
495
|
});
|
|
446
496
|
});
|
|
447
497
|
}
|
package/src/api.js
CHANGED
|
@@ -7,18 +7,37 @@ const USER_AGENT = `aether-code/${getVersion()}`;
|
|
|
7
7
|
|
|
8
8
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
9
9
|
|
|
10
|
+
// Per-request connect timeout. The server retries upstream internally (up to
|
|
11
|
+
// 5 × 75s = 375s worst case), and during that time the CLI's fetch() just
|
|
12
|
+
// hangs with zero feedback. This cap ensures we abort, show a retry message,
|
|
13
|
+
// and try again rather than sitting dead for minutes.
|
|
14
|
+
const CONNECT_TIMEOUT_MS = 90_000;
|
|
15
|
+
|
|
10
16
|
/**
|
|
11
17
|
* fetch() with bounded exponential-backoff retries. Retries on thrown network
|
|
12
18
|
* errors (DNS/reset/offline blip) and transient 5xx; never retries 4xx
|
|
13
19
|
* (auth/credit/validation — a retry won't help). Body is always a small JSON
|
|
14
20
|
* string, so resending is safe. For the streaming endpoint only the initial
|
|
15
21
|
* connection is retried.
|
|
22
|
+
*
|
|
23
|
+
* Each request has a hard 90s connect timeout via AbortController — if the
|
|
24
|
+
* server hasn't responded with headers by then, we abort and retry. This
|
|
25
|
+
* prevents the CLI from hanging indefinitely while the server retries
|
|
26
|
+
* upstream internally.
|
|
16
27
|
*/
|
|
17
|
-
export async function fetchWithRetry(url, options, { retries = 4, baseDelay = 1000, onRetry } = {}) {
|
|
28
|
+
export async function fetchWithRetry(url, options, { retries = 4, baseDelay = 1000, timeoutMs = CONNECT_TIMEOUT_MS, onRetry } = {}) {
|
|
18
29
|
let attempt = 0;
|
|
19
30
|
while (true) {
|
|
31
|
+
const ac = new AbortController();
|
|
32
|
+
const timer = setTimeout(() => ac.abort(new Error(`Server did not respond within ${Math.round(timeoutMs / 1000)}s`)), timeoutMs);
|
|
33
|
+
// If the caller already set a signal, chain it so both can abort.
|
|
34
|
+
const callerSignal = options?.signal;
|
|
35
|
+
if (callerSignal) {
|
|
36
|
+
callerSignal.addEventListener("abort", () => ac.abort(callerSignal.reason), { once: true });
|
|
37
|
+
}
|
|
20
38
|
try {
|
|
21
|
-
const res = await fetch(url, options);
|
|
39
|
+
const res = await fetch(url, { ...options, signal: ac.signal });
|
|
40
|
+
clearTimeout(timer);
|
|
22
41
|
if (res.status >= 500 && attempt < retries) {
|
|
23
42
|
// Release the connection back to the pool before retrying — an
|
|
24
43
|
// unconsumed body keeps the socket open (undici won't reuse it).
|
|
@@ -30,6 +49,7 @@ export async function fetchWithRetry(url, options, { retries = 4, baseDelay = 10
|
|
|
30
49
|
}
|
|
31
50
|
return res;
|
|
32
51
|
} catch (e) {
|
|
52
|
+
clearTimeout(timer);
|
|
33
53
|
if (attempt < retries) {
|
|
34
54
|
attempt++;
|
|
35
55
|
if (onRetry) onRetry(attempt, e.message);
|
|
@@ -207,7 +227,7 @@ export async function agentTurnStream({
|
|
|
207
227
|
let timer;
|
|
208
228
|
const timeout = new Promise((_, reject) => {
|
|
209
229
|
timer = setTimeout(
|
|
210
|
-
() => reject(new AetherError(
|
|
230
|
+
() => reject(new AetherError(`Stream stalled — no data for ${Math.round(READ_TIMEOUT_MS / 1000)}s`, "STREAM_TIMEOUT", 0)),
|
|
211
231
|
READ_TIMEOUT_MS,
|
|
212
232
|
);
|
|
213
233
|
});
|