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/package.json CHANGED
@@ -1,75 +1,75 @@
1
- {
2
- "name": "aether-code",
3
- "version": "0.41.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", (err) => {
491
+ child.on("error", () => {
443
492
  clearTimeout(timer);
444
- resolve({ ok: false, output: err.message, label, type, runCmd });
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("Stream stalled — no data for 120s", "STREAM_TIMEOUT", 0)),
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
  });