aether-code 0.36.8 → 0.38.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/bin/aether-code.js +3 -5
- package/package.json +2 -2
- package/src/agent.js +14 -11
- package/src/api.js +3 -16
- package/src/mcp.js +2 -1
- package/src/project-context.js +279 -0
- package/src/render.js +33 -0
- package/src/repl.js +161 -35
- package/src/sessions.js +167 -0
- package/src/tools.js +392 -15
- package/src/version.js +15 -0
package/bin/aether-code.js
CHANGED
|
@@ -27,8 +27,9 @@ import {
|
|
|
27
27
|
} from "../src/mcp-registry.js";
|
|
28
28
|
import readline from "node:readline";
|
|
29
29
|
import { c, errorLine, divider, setTerminalTitle } from "../src/render.js";
|
|
30
|
+
import { getVersion } from "../src/version.js";
|
|
30
31
|
|
|
31
|
-
const VERSION =
|
|
32
|
+
const VERSION = getVersion();
|
|
32
33
|
|
|
33
34
|
/**
|
|
34
35
|
* Try to start MCP servers from ~/.aether/mcp.json. Returns a started
|
|
@@ -315,13 +316,10 @@ async function handleBalance() {
|
|
|
315
316
|
const me = await fetchBalance();
|
|
316
317
|
console.log(c.bold(c.magenta("Aether")));
|
|
317
318
|
console.log(c.gray("─".repeat(50)));
|
|
318
|
-
console.log(`Plan ${c.cyan(me.plan)}
|
|
319
|
+
console.log(`Plan ${c.cyan(me.plan)}`);
|
|
319
320
|
console.log(`Balance ${c.bold(me.balance.toLocaleString())} credits`);
|
|
320
321
|
console.log(` plan ${me.planCredits.toLocaleString()}`);
|
|
321
322
|
console.log(` topup ${me.topupCredits.toLocaleString()}`);
|
|
322
|
-
if (me.rate) {
|
|
323
|
-
console.log(`Rate ${me.rate.used}/${me.rate.limit} this hour${me.rate.resetIn ? ` · resets in ${me.rate.resetIn}s` : ""}`);
|
|
324
|
-
}
|
|
325
323
|
if (me.isSuspended) console.log(c.red("\n⚠ Account is suspended."));
|
|
326
324
|
} catch (err) {
|
|
327
325
|
if (err instanceof AetherError && err.code === "NO_API_KEY") {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aether-code",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.0",
|
|
4
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": {
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"node": ">=18"
|
|
24
24
|
},
|
|
25
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",
|
|
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
27
|
"test": "node --test \"test/**/*.test.js\"",
|
|
28
28
|
"prepublishOnly": "npm run lint && npm test"
|
|
29
29
|
},
|
package/src/agent.js
CHANGED
|
@@ -8,27 +8,31 @@ import { agentTurnStream, AetherError } from "./api.js";
|
|
|
8
8
|
import { TOOL_DEFINITIONS, executeTool } from "./tools.js";
|
|
9
9
|
import { unnamespaceToolName } from "./mcp.js";
|
|
10
10
|
import { loadAllSkills, selectSkills, renderSkillsBlock } from "./skills.js";
|
|
11
|
+
import { scanProject } from "./project-context.js";
|
|
11
12
|
import { c, divider, turn, toolLabel, toolSummary, makeTokenStripper, errorLine, startSpinner } from "./render.js";
|
|
12
13
|
|
|
13
14
|
const DEFAULT_MAX_TURNS = 25;
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
export const MOD_RE =
|
|
17
|
+
/\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;
|
|
18
|
+
|
|
19
|
+
export function envContext(cwd) {
|
|
18
20
|
const home = os.homedir();
|
|
19
21
|
const desktop = path.join(home, "Desktop");
|
|
20
22
|
const documents = path.join(home, "Documents");
|
|
21
23
|
const win = process.platform === "win32";
|
|
22
|
-
// OS-specific shell guidance: gemma tends to emit Unix commands (mkdir -p,
|
|
23
|
-
// touch, &&-chains) which FAIL in Windows cmd.exe ("The syntax of the command
|
|
24
|
-
// is incorrect"). Steer it to the file tools (which are cross-platform and
|
|
25
|
-
// auto-create parent dirs) and OS-appropriate shell usage.
|
|
26
24
|
const shellNote = win
|
|
27
25
|
? `shell: Windows cmd.exe. To create files OR directories, use the write_file tool — ` +
|
|
28
26
|
`it creates any missing parent folders automatically. Do NOT use shell "mkdir -p", ` +
|
|
29
27
|
`"touch", "ls", "rm", "cp", "mv", or "&&"-chained Unix commands; they FAIL in cmd.exe. ` +
|
|
30
28
|
`Run programs/tests with their interpreter (python, node, java, etc.).`
|
|
31
29
|
: `shell: POSIX sh. Standard Unix commands are available. write_file still auto-creates parent dirs.`;
|
|
30
|
+
|
|
31
|
+
let projectBlock = "";
|
|
32
|
+
try {
|
|
33
|
+
projectBlock = scanProject(cwd);
|
|
34
|
+
} catch { /* non-fatal */ }
|
|
35
|
+
|
|
32
36
|
return (
|
|
33
37
|
`[environment]\n` +
|
|
34
38
|
`os: ${process.platform}\n` +
|
|
@@ -40,7 +44,8 @@ function envContext(cwd) {
|
|
|
40
44
|
`When the user names a location ("my desktop", "home", "documents"), write to ` +
|
|
41
45
|
`the matching ABSOLUTE path above (e.g. desktop -> ${desktop}). Otherwise ` +
|
|
42
46
|
`work under the cwd. Use absolute paths when a specific location is named.\n` +
|
|
43
|
-
`[/environment]\n\n`
|
|
47
|
+
`[/environment]\n\n` +
|
|
48
|
+
(projectBlock ? projectBlock + "\n" : "")
|
|
44
49
|
);
|
|
45
50
|
}
|
|
46
51
|
|
|
@@ -102,8 +107,6 @@ export async function runAgent({
|
|
|
102
107
|
// Completeness guard: gemma sometimes reads files then narrates the fix as
|
|
103
108
|
// text, or refactors one file but forgets the others. If a MODIFICATION task
|
|
104
109
|
// finishes having read files it never edited, we nudge it once to finish.
|
|
105
|
-
const MOD_RE =
|
|
106
|
-
/\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;
|
|
107
110
|
const looksLikeModification = MOD_RE.test(initialPrompt);
|
|
108
111
|
let appliedNothingNudges = 0;
|
|
109
112
|
|
|
@@ -309,7 +312,7 @@ export async function runAgent({
|
|
|
309
312
|
* worse than no skills at all. Prepending into the user message keeps
|
|
310
313
|
* both layers active.
|
|
311
314
|
*/
|
|
312
|
-
function buildTurnMessages(messages, allSkills, referencedPaths) {
|
|
315
|
+
export function buildTurnMessages(messages, allSkills, referencedPaths) {
|
|
313
316
|
if (allSkills.length === 0) return messages;
|
|
314
317
|
// Find the latest user message — that's where the current task lives.
|
|
315
318
|
let lastUserIdx = -1;
|
package/src/api.js
CHANGED
|
@@ -1,22 +1,9 @@
|
|
|
1
1
|
// API client.
|
|
2
2
|
|
|
3
|
-
import fs from "node:fs";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
6
3
|
import { getConfig } from "./config.js";
|
|
4
|
+
import { getVersion } from "./version.js";
|
|
7
5
|
|
|
8
|
-
|
|
9
|
-
// package.json so it can't drift (the file previously sent three different
|
|
10
|
-
// hardcoded versions across three calls).
|
|
11
|
-
function readVersion() {
|
|
12
|
-
try {
|
|
13
|
-
const pkgPath = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "package.json");
|
|
14
|
-
return JSON.parse(fs.readFileSync(pkgPath, "utf8")).version || "0";
|
|
15
|
-
} catch {
|
|
16
|
-
return "0";
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
const USER_AGENT = `aether-code/${readVersion()}`;
|
|
6
|
+
const USER_AGENT = `aether-code/${getVersion()}`;
|
|
20
7
|
|
|
21
8
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
22
9
|
|
|
@@ -93,7 +80,7 @@ export async function fetchBalance(apiKeyOverride) {
|
|
|
93
80
|
const msg = data?.error || `${res.status} ${res.statusText}`;
|
|
94
81
|
throw new AetherError(msg, code, res.status, data);
|
|
95
82
|
}
|
|
96
|
-
return data; // { plan,
|
|
83
|
+
return data; // { plan, planCredits, topupCredits, balance, isSuspended }
|
|
97
84
|
}
|
|
98
85
|
|
|
99
86
|
export class AetherError extends Error {
|
package/src/mcp.js
CHANGED
|
@@ -31,6 +31,7 @@ import os from "node:os";
|
|
|
31
31
|
import path from "node:path";
|
|
32
32
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
33
33
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
34
|
+
import { getVersion } from "./version.js";
|
|
34
35
|
|
|
35
36
|
const DEFAULT_CONFIG_PATH = path.join(os.homedir(), ".aether", "mcp.json");
|
|
36
37
|
const NAMESPACE_SEP = "__";
|
|
@@ -163,7 +164,7 @@ export class MCPManager {
|
|
|
163
164
|
env: { ...process.env, ...(cfg.env ?? {}) },
|
|
164
165
|
});
|
|
165
166
|
const client = new Client(
|
|
166
|
-
{ name: "aether-code", version:
|
|
167
|
+
{ name: "aether-code", version: getVersion() },
|
|
167
168
|
{ capabilities: {} },
|
|
168
169
|
);
|
|
169
170
|
// Bound the init handshake so a hung server doesn't stall startup.
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
// Project context scanner. Reads the cwd on startup to give the model
|
|
2
|
+
// immediate understanding of what project it's working in — tech stack,
|
|
3
|
+
// file structure, available scripts, dependencies, and any project-specific
|
|
4
|
+
// instructions. This is the single biggest quality-of-life improvement for
|
|
5
|
+
// a weaker model: instead of guessing, it KNOWS.
|
|
6
|
+
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
|
|
10
|
+
const SKIP_DIRS = new Set([
|
|
11
|
+
"node_modules", ".git", ".next", "dist", "build", "__pycache__",
|
|
12
|
+
".venv", "venv", "env", ".env", ".idea", ".vscode", "coverage",
|
|
13
|
+
".cache", ".turbo", "target", "out", ".output", ".nuxt", ".svelte-kit",
|
|
14
|
+
"vendor", "bower_components", ".parcel-cache", ".webpack",
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
const FRAMEWORK_SIGNATURES = {
|
|
18
|
+
"next.config.js": "Next.js",
|
|
19
|
+
"next.config.mjs": "Next.js",
|
|
20
|
+
"next.config.ts": "Next.js",
|
|
21
|
+
"nuxt.config.ts": "Nuxt",
|
|
22
|
+
"svelte.config.js": "SvelteKit",
|
|
23
|
+
"astro.config.mjs": "Astro",
|
|
24
|
+
"remix.config.js": "Remix",
|
|
25
|
+
"angular.json": "Angular",
|
|
26
|
+
"vue.config.js": "Vue CLI",
|
|
27
|
+
"vite.config.ts": "Vite",
|
|
28
|
+
"vite.config.js": "Vite",
|
|
29
|
+
"webpack.config.js": "Webpack",
|
|
30
|
+
"tsconfig.json": "TypeScript",
|
|
31
|
+
"Cargo.toml": "Rust (Cargo)",
|
|
32
|
+
"go.mod": "Go",
|
|
33
|
+
"pyproject.toml": "Python",
|
|
34
|
+
"setup.py": "Python (setuptools)",
|
|
35
|
+
"requirements.txt": "Python",
|
|
36
|
+
"Gemfile": "Ruby (Bundler)",
|
|
37
|
+
"pom.xml": "Java (Maven)",
|
|
38
|
+
"build.gradle": "Java/Kotlin (Gradle)",
|
|
39
|
+
"CMakeLists.txt": "C/C++ (CMake)",
|
|
40
|
+
"Makefile": "Make",
|
|
41
|
+
"Dockerfile": "Docker",
|
|
42
|
+
"docker-compose.yml": "Docker Compose",
|
|
43
|
+
"docker-compose.yaml": "Docker Compose",
|
|
44
|
+
".env": "dotenv config",
|
|
45
|
+
"prisma/schema.prisma": "Prisma ORM",
|
|
46
|
+
"drizzle.config.ts": "Drizzle ORM",
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const DEP_FRAMEWORKS = {
|
|
50
|
+
next: "Next.js",
|
|
51
|
+
react: "React",
|
|
52
|
+
vue: "Vue",
|
|
53
|
+
svelte: "Svelte",
|
|
54
|
+
angular: "Angular",
|
|
55
|
+
express: "Express",
|
|
56
|
+
fastify: "Fastify",
|
|
57
|
+
koa: "Koa",
|
|
58
|
+
hono: "Hono",
|
|
59
|
+
"socket.io": "Socket.IO",
|
|
60
|
+
prisma: "Prisma",
|
|
61
|
+
drizzle: "Drizzle",
|
|
62
|
+
mongoose: "Mongoose",
|
|
63
|
+
sequelize: "Sequelize",
|
|
64
|
+
tailwindcss: "Tailwind CSS",
|
|
65
|
+
"styled-components": "styled-components",
|
|
66
|
+
electron: "Electron",
|
|
67
|
+
jest: "Jest",
|
|
68
|
+
vitest: "Vitest",
|
|
69
|
+
mocha: "Mocha",
|
|
70
|
+
playwright: "Playwright",
|
|
71
|
+
cypress: "Cypress",
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
function readJsonSafe(filePath) {
|
|
75
|
+
try {
|
|
76
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
77
|
+
} catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function readTextSafe(filePath, maxBytes = 2000) {
|
|
83
|
+
try {
|
|
84
|
+
const stat = fs.statSync(filePath);
|
|
85
|
+
if (!stat.isFile() || stat.size > 500_000) return null;
|
|
86
|
+
const buf = Buffer.alloc(Math.min(stat.size, maxBytes));
|
|
87
|
+
const fd = fs.openSync(filePath, "r");
|
|
88
|
+
const bytesRead = fs.readSync(fd, buf, 0, buf.length, 0);
|
|
89
|
+
fs.closeSync(fd);
|
|
90
|
+
return buf.toString("utf8", 0, bytesRead);
|
|
91
|
+
} catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function listTopLevel(cwd) {
|
|
97
|
+
let entries;
|
|
98
|
+
try {
|
|
99
|
+
entries = fs.readdirSync(cwd, { withFileTypes: true });
|
|
100
|
+
} catch {
|
|
101
|
+
return { dirs: [], files: [] };
|
|
102
|
+
}
|
|
103
|
+
const dirs = [];
|
|
104
|
+
const files = [];
|
|
105
|
+
for (const e of entries) {
|
|
106
|
+
if (e.name.startsWith(".") && e.name !== ".env" && e.name !== ".gitignore") continue;
|
|
107
|
+
if (SKIP_DIRS.has(e.name)) continue;
|
|
108
|
+
if (e.isDirectory()) {
|
|
109
|
+
dirs.push(e.name + "/");
|
|
110
|
+
} else if (e.isFile()) {
|
|
111
|
+
files.push(e.name);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
dirs.sort();
|
|
115
|
+
files.sort();
|
|
116
|
+
return { dirs, files };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function listSecondLevel(cwd, dirName) {
|
|
120
|
+
const full = path.join(cwd, dirName);
|
|
121
|
+
let entries;
|
|
122
|
+
try {
|
|
123
|
+
entries = fs.readdirSync(full, { withFileTypes: true });
|
|
124
|
+
} catch {
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
const items = [];
|
|
128
|
+
for (const e of entries) {
|
|
129
|
+
if (e.name.startsWith(".")) continue;
|
|
130
|
+
if (SKIP_DIRS.has(e.name)) continue;
|
|
131
|
+
items.push(e.isDirectory() ? e.name + "/" : e.name);
|
|
132
|
+
}
|
|
133
|
+
items.sort();
|
|
134
|
+
return items.slice(0, 30);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function detectFrameworks(cwd, topFiles) {
|
|
138
|
+
const found = new Set();
|
|
139
|
+
for (const file of topFiles) {
|
|
140
|
+
if (FRAMEWORK_SIGNATURES[file]) found.add(FRAMEWORK_SIGNATURES[file]);
|
|
141
|
+
}
|
|
142
|
+
if (fs.existsSync(path.join(cwd, "prisma", "schema.prisma"))) {
|
|
143
|
+
found.add("Prisma ORM");
|
|
144
|
+
}
|
|
145
|
+
return [...found];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function extractPackageInfo(cwd) {
|
|
149
|
+
const pkg = readJsonSafe(path.join(cwd, "package.json"));
|
|
150
|
+
if (!pkg) return null;
|
|
151
|
+
const info = { name: pkg.name, version: pkg.version };
|
|
152
|
+
if (pkg.scripts && Object.keys(pkg.scripts).length > 0) {
|
|
153
|
+
info.scripts = Object.keys(pkg.scripts);
|
|
154
|
+
}
|
|
155
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
156
|
+
const detectedLibs = [];
|
|
157
|
+
for (const [dep, label] of Object.entries(DEP_FRAMEWORKS)) {
|
|
158
|
+
if (allDeps[dep]) detectedLibs.push(label);
|
|
159
|
+
}
|
|
160
|
+
if (detectedLibs.length > 0) info.frameworks = detectedLibs;
|
|
161
|
+
const depCount = Object.keys(pkg.dependencies || {}).length;
|
|
162
|
+
const devCount = Object.keys(pkg.devDependencies || {}).length;
|
|
163
|
+
if (depCount || devCount) {
|
|
164
|
+
info.depCounts = `${depCount} deps, ${devCount} devDeps`;
|
|
165
|
+
}
|
|
166
|
+
return info;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function extractCargoInfo(cwd) {
|
|
170
|
+
const raw = readTextSafe(path.join(cwd, "Cargo.toml"), 1500);
|
|
171
|
+
if (!raw) return null;
|
|
172
|
+
const name = raw.match(/^name\s*=\s*"(.+?)"/m)?.[1];
|
|
173
|
+
const version = raw.match(/^version\s*=\s*"(.+?)"/m)?.[1];
|
|
174
|
+
return { name, version, type: "Rust" };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function extractGoInfo(cwd) {
|
|
178
|
+
const raw = readTextSafe(path.join(cwd, "go.mod"), 500);
|
|
179
|
+
if (!raw) return null;
|
|
180
|
+
const mod = raw.match(/^module\s+(\S+)/m)?.[1];
|
|
181
|
+
const goVer = raw.match(/^go\s+(\S+)/m)?.[1];
|
|
182
|
+
return { module: mod, goVersion: goVer, type: "Go" };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function extractPythonInfo(cwd) {
|
|
186
|
+
const pyproject = readTextSafe(path.join(cwd, "pyproject.toml"), 1500);
|
|
187
|
+
if (pyproject) {
|
|
188
|
+
const name = pyproject.match(/^name\s*=\s*"(.+?)"/m)?.[1];
|
|
189
|
+
return { name, type: "Python" };
|
|
190
|
+
}
|
|
191
|
+
if (fs.existsSync(path.join(cwd, "requirements.txt"))) {
|
|
192
|
+
return { type: "Python (requirements.txt)" };
|
|
193
|
+
}
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function scanProject(cwd) {
|
|
198
|
+
const { dirs, files } = listTopLevel(cwd);
|
|
199
|
+
if (dirs.length === 0 && files.length === 0) {
|
|
200
|
+
return "[project]\nEmpty directory — no files detected.\n[/project]\n";
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const lines = ["[project]"];
|
|
204
|
+
|
|
205
|
+
const frameworks = detectFrameworks(cwd, files);
|
|
206
|
+
if (frameworks.length > 0) {
|
|
207
|
+
lines.push(`stack: ${frameworks.join(", ")}`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const pkg = extractPackageInfo(cwd);
|
|
211
|
+
const cargo = extractCargoInfo(cwd);
|
|
212
|
+
const goInfo = extractGoInfo(cwd);
|
|
213
|
+
const pyInfo = extractPythonInfo(cwd);
|
|
214
|
+
|
|
215
|
+
if (pkg) {
|
|
216
|
+
if (pkg.name) lines.push(`package: ${pkg.name}${pkg.version ? `@${pkg.version}` : ""}`);
|
|
217
|
+
if (pkg.frameworks) lines.push(`libraries: ${pkg.frameworks.join(", ")}`);
|
|
218
|
+
if (pkg.depCounts) lines.push(`dependencies: ${pkg.depCounts}`);
|
|
219
|
+
if (pkg.scripts) lines.push(`scripts: ${pkg.scripts.join(", ")}`);
|
|
220
|
+
}
|
|
221
|
+
if (cargo) {
|
|
222
|
+
lines.push(`crate: ${cargo.name || "unknown"}${cargo.version ? `@${cargo.version}` : ""}`);
|
|
223
|
+
}
|
|
224
|
+
if (goInfo) {
|
|
225
|
+
lines.push(`module: ${goInfo.module || "unknown"}${goInfo.goVersion ? ` (go ${goInfo.goVersion})` : ""}`);
|
|
226
|
+
}
|
|
227
|
+
if (pyInfo) {
|
|
228
|
+
lines.push(`python project: ${pyInfo.name || pyInfo.type}`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
lines.push("");
|
|
232
|
+
lines.push("structure:");
|
|
233
|
+
for (const d of dirs) {
|
|
234
|
+
lines.push(` ${d}`);
|
|
235
|
+
const children = listSecondLevel(cwd, d.replace(/\/$/, ""));
|
|
236
|
+
for (const child of children.slice(0, 15)) {
|
|
237
|
+
lines.push(` ${child}`);
|
|
238
|
+
}
|
|
239
|
+
if (children.length > 15) {
|
|
240
|
+
lines.push(` ... (${children.length - 15} more)`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
for (const f of files) {
|
|
244
|
+
lines.push(` ${f}`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const contextFile = path.join(cwd, ".aether", "context.md");
|
|
248
|
+
const context = readTextSafe(contextFile, 3000);
|
|
249
|
+
if (context) {
|
|
250
|
+
lines.push("");
|
|
251
|
+
lines.push("project-instructions:");
|
|
252
|
+
lines.push(context.trim());
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const readme = readTextSafe(path.join(cwd, "README.md"), 1500);
|
|
256
|
+
if (readme) {
|
|
257
|
+
lines.push("");
|
|
258
|
+
lines.push("readme-excerpt:");
|
|
259
|
+
lines.push(readme.trim());
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
lines.push("[/project]");
|
|
263
|
+
return lines.join("\n") + "\n";
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function scanProjectQuick(cwd) {
|
|
267
|
+
const { dirs, files } = listTopLevel(cwd);
|
|
268
|
+
const total = dirs.length + files.length;
|
|
269
|
+
if (total === 0) return "empty directory";
|
|
270
|
+
const frameworks = detectFrameworks(cwd, files);
|
|
271
|
+
const pkg = extractPackageInfo(cwd);
|
|
272
|
+
const parts = [];
|
|
273
|
+
if (pkg?.name) parts.push(pkg.name);
|
|
274
|
+
if (frameworks.length > 0) parts.push(frameworks.join(", "));
|
|
275
|
+
else if (pkg?.frameworks?.length) parts.push(pkg.frameworks[0]);
|
|
276
|
+
parts.push(`${total} items`);
|
|
277
|
+
if (pkg?.scripts) parts.push(`${pkg.scripts.length} scripts`);
|
|
278
|
+
return parts.join(" · ");
|
|
279
|
+
}
|
package/src/render.js
CHANGED
|
@@ -88,6 +88,13 @@ export function toolLabel(name, args) {
|
|
|
88
88
|
case "web_fetch": return `${verb("fetch")} ${arg(a.url)}`;
|
|
89
89
|
case "todo_write": return ""; // its Plan box is the label
|
|
90
90
|
case "ask_user": return ""; // the interactive menu is its display
|
|
91
|
+
case "git_status": return `${verb("git")} ${arg("status")}`;
|
|
92
|
+
case "git_diff": return `${verb("git")} ${arg("diff" + (a.staged ? " --staged" : "") + (a.base ? ` ${a.base}...${a.head || "HEAD"}` : ""))}`;
|
|
93
|
+
case "git_log": return `${verb("git")} ${arg("log" + (a.file ? ` -- ${a.file}` : "") + (a.branch ? ` ${a.branch}` : ""))}`;
|
|
94
|
+
case "git_commit": return `${verb("git")} ${arg(`commit "${ellip(a.message || "", 50)}"`)}`
|
|
95
|
+
case "git_branch": return `${verb("git")} ${arg(`branch ${a.name}`)}`;
|
|
96
|
+
case "git_push": return `${verb("git")} ${arg("push" + (a.force ? " --force" : ""))}`;
|
|
97
|
+
case "git_create_pr": return `${verb("git")} ${arg(`pr "${ellip(a.title || "", 50)}"`)}`
|
|
91
98
|
default: return `${verb(name)} ${arg(JSON.stringify(a))}`;
|
|
92
99
|
}
|
|
93
100
|
}
|
|
@@ -142,6 +149,32 @@ export function toolSummary(name, result) {
|
|
|
142
149
|
}
|
|
143
150
|
if (name === "todo_write") return ""; // the Plan is its own feedback
|
|
144
151
|
if (name === "ask_user") return ""; // the menu already showed the answer
|
|
152
|
+
if (name === "git_commit" || name === "git_push" || name === "git_create_pr") {
|
|
153
|
+
return ` ${branch} ${c.gray(ellip(firstLine, 100))}`;
|
|
154
|
+
}
|
|
155
|
+
if (name === "git_status") {
|
|
156
|
+
try {
|
|
157
|
+
const s = JSON.parse(out);
|
|
158
|
+
if (s.clean) return ` ${branch} ${c.gray("clean")}`;
|
|
159
|
+
const parts = [];
|
|
160
|
+
if (s.staged?.length) parts.push(`${s.staged.length} staged`);
|
|
161
|
+
if (s.unstaged?.length) parts.push(`${s.unstaged.length} modified`);
|
|
162
|
+
if (s.untracked?.length) parts.push(`${s.untracked.length} untracked`);
|
|
163
|
+
return ` ${branch} ${c.gray(parts.join(", "))}`;
|
|
164
|
+
} catch { /* fall through */ }
|
|
165
|
+
}
|
|
166
|
+
if (name === "git_diff") {
|
|
167
|
+
if (out === "(no changes)") return ` ${branch} ${c.gray("no changes")}`;
|
|
168
|
+
const diffLines = out.split("\n").length;
|
|
169
|
+
return ` ${branch} ${c.gray(`${diffLines} lines of diff`)}`;
|
|
170
|
+
}
|
|
171
|
+
if (name === "git_log") {
|
|
172
|
+
const logLines = out.trim().split("\n").filter((l) => l.trim()).length;
|
|
173
|
+
return ` ${branch} ${c.gray(`${logLines} lines`)}`;
|
|
174
|
+
}
|
|
175
|
+
if (name === "git_branch") {
|
|
176
|
+
return ` ${branch} ${c.gray(ellip(firstLine, 100))}`;
|
|
177
|
+
}
|
|
145
178
|
if (name === "write_file" || name === "edit_file") {
|
|
146
179
|
// Handler already printed the diff; echo its short status line.
|
|
147
180
|
return ` ${branch} ${c.gray(ellip(firstLine, 100))}`;
|