aether-code 0.11.1 → 0.13.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 +21 -21
- package/README.md +17 -2
- package/bin/aether-code.js +457 -361
- package/package.json +69 -68
- package/src/agent.js +197 -197
- package/src/api.js +287 -234
- package/src/config.js +38 -38
- package/src/diff.js +48 -48
- package/src/mcp-registry.js +266 -0
- package/src/render.js +58 -58
- package/src/repl.js +247 -247
- package/src/setup.js +139 -139
- package/src/skills.js +3 -0
- package/src/tools.js +803 -621
package/src/diff.js
CHANGED
|
@@ -1,48 +1,48 @@
|
|
|
1
|
-
// Tiny line-by-line diff for write_file confirmation prompts.
|
|
2
|
-
// Not a "real" diff — just a side-by-side highlight of what's changing.
|
|
3
|
-
// Good enough for confirmation prompts, deliberately not pretending to be `git diff`.
|
|
4
|
-
|
|
5
|
-
import { c } from "./render.js";
|
|
6
|
-
|
|
7
|
-
export function unifiedDiff(oldText, newText, filename) {
|
|
8
|
-
const oldLines = (oldText || "").split("\n");
|
|
9
|
-
const newLines = (newText || "").split("\n");
|
|
10
|
-
const max = Math.max(oldLines.length, newLines.length);
|
|
11
|
-
|
|
12
|
-
// Find common prefix and suffix to keep the diff focused
|
|
13
|
-
let prefix = 0;
|
|
14
|
-
while (prefix < max && oldLines[prefix] === newLines[prefix]) prefix++;
|
|
15
|
-
let suffix = 0;
|
|
16
|
-
while (
|
|
17
|
-
suffix < max - prefix &&
|
|
18
|
-
oldLines[oldLines.length - 1 - suffix] === newLines[newLines.length - 1 - suffix]
|
|
19
|
-
) {
|
|
20
|
-
suffix++;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const changedOld = oldLines.slice(prefix, oldLines.length - suffix);
|
|
24
|
-
const changedNew = newLines.slice(prefix, newLines.length - suffix);
|
|
25
|
-
|
|
26
|
-
const lines = [];
|
|
27
|
-
lines.push(c.bold(c.cyan(`@@ ${filename} @@`)));
|
|
28
|
-
if (prefix > 0) lines.push(c.gray(` …${prefix} unchanged line${prefix === 1 ? "" : "s"} above…`));
|
|
29
|
-
for (const l of changedOld) lines.push(c.red(`- ${l}`));
|
|
30
|
-
for (const l of changedNew) lines.push(c.green(`+ ${l}`));
|
|
31
|
-
if (suffix > 0) lines.push(c.gray(` …${suffix} unchanged line${suffix === 1 ? "" : "s"} below…`));
|
|
32
|
-
|
|
33
|
-
// Cap output so massive writes don't flood the terminal
|
|
34
|
-
if (lines.length > 60) {
|
|
35
|
-
return [...lines.slice(0, 30), c.gray(` …${lines.length - 60} more lines hidden…`), ...lines.slice(-30)].join(
|
|
36
|
-
"\n",
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
return lines.join("\n");
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function summarizeWrite(oldText, newText, filename) {
|
|
43
|
-
const oldLines = (oldText || "").split("\n").length;
|
|
44
|
-
const newLines = (newText || "").split("\n").length;
|
|
45
|
-
const isCreate = oldText === null || oldText === undefined;
|
|
46
|
-
const verb = isCreate ? "create" : "rewrite";
|
|
47
|
-
return c.dim(`${verb} ${filename} (${oldLines} → ${newLines} lines, ${(newText || "").length} bytes)`);
|
|
48
|
-
}
|
|
1
|
+
// Tiny line-by-line diff for write_file confirmation prompts.
|
|
2
|
+
// Not a "real" diff — just a side-by-side highlight of what's changing.
|
|
3
|
+
// Good enough for confirmation prompts, deliberately not pretending to be `git diff`.
|
|
4
|
+
|
|
5
|
+
import { c } from "./render.js";
|
|
6
|
+
|
|
7
|
+
export function unifiedDiff(oldText, newText, filename) {
|
|
8
|
+
const oldLines = (oldText || "").split("\n");
|
|
9
|
+
const newLines = (newText || "").split("\n");
|
|
10
|
+
const max = Math.max(oldLines.length, newLines.length);
|
|
11
|
+
|
|
12
|
+
// Find common prefix and suffix to keep the diff focused
|
|
13
|
+
let prefix = 0;
|
|
14
|
+
while (prefix < max && oldLines[prefix] === newLines[prefix]) prefix++;
|
|
15
|
+
let suffix = 0;
|
|
16
|
+
while (
|
|
17
|
+
suffix < max - prefix &&
|
|
18
|
+
oldLines[oldLines.length - 1 - suffix] === newLines[newLines.length - 1 - suffix]
|
|
19
|
+
) {
|
|
20
|
+
suffix++;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const changedOld = oldLines.slice(prefix, oldLines.length - suffix);
|
|
24
|
+
const changedNew = newLines.slice(prefix, newLines.length - suffix);
|
|
25
|
+
|
|
26
|
+
const lines = [];
|
|
27
|
+
lines.push(c.bold(c.cyan(`@@ ${filename} @@`)));
|
|
28
|
+
if (prefix > 0) lines.push(c.gray(` …${prefix} unchanged line${prefix === 1 ? "" : "s"} above…`));
|
|
29
|
+
for (const l of changedOld) lines.push(c.red(`- ${l}`));
|
|
30
|
+
for (const l of changedNew) lines.push(c.green(`+ ${l}`));
|
|
31
|
+
if (suffix > 0) lines.push(c.gray(` …${suffix} unchanged line${suffix === 1 ? "" : "s"} below…`));
|
|
32
|
+
|
|
33
|
+
// Cap output so massive writes don't flood the terminal
|
|
34
|
+
if (lines.length > 60) {
|
|
35
|
+
return [...lines.slice(0, 30), c.gray(` …${lines.length - 60} more lines hidden…`), ...lines.slice(-30)].join(
|
|
36
|
+
"\n",
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
return lines.join("\n");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function summarizeWrite(oldText, newText, filename) {
|
|
43
|
+
const oldLines = (oldText || "").split("\n").length;
|
|
44
|
+
const newLines = (newText || "").split("\n").length;
|
|
45
|
+
const isCreate = oldText === null || oldText === undefined;
|
|
46
|
+
const verb = isCreate ? "create" : "rewrite";
|
|
47
|
+
return c.dim(`${verb} ${filename} (${oldLines} → ${newLines} lines, ${(newText || "").length} bytes)`);
|
|
48
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
// Curated registry of well-known MCP servers users can install with
|
|
2
|
+
// `aether mcp install <name>` instead of typing the full npx command.
|
|
3
|
+
//
|
|
4
|
+
// Conservative inclusion criteria:
|
|
5
|
+
// 1. Published to npm under a known scope (@modelcontextprotocol/*,
|
|
6
|
+
// @playwright/*, etc.) OR widely-cited community packages
|
|
7
|
+
// 2. stdio transport (which is all MCPManager supports today)
|
|
8
|
+
// 3. Installable via npx -y with no separate global install step
|
|
9
|
+
//
|
|
10
|
+
// We DON'T include servers that need a local binary (IDA Pro, Ghidra,
|
|
11
|
+
// Wireshark) because the install path is environment-specific — those
|
|
12
|
+
// users need to read each project's README anyway, so `mcp install` won't
|
|
13
|
+
// save them work. The README shows their `mcp add` commands instead.
|
|
14
|
+
//
|
|
15
|
+
// Adding a new entry: confirm the package exists on npm, confirm the
|
|
16
|
+
// stdio entrypoint runs cleanly with `npx -y <pkg>`, then drop in here.
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Each entry has:
|
|
20
|
+
* id — kebab-case name used as the MCP server name in config
|
|
21
|
+
* (must satisfy /^[a-z0-9_-]{1,40}$/i per mcp.js validator)
|
|
22
|
+
* command — base spawn command (typically "npx")
|
|
23
|
+
* args — array of args; entries wrapped in `{placeholder}` get
|
|
24
|
+
* substituted from user-provided inputs
|
|
25
|
+
* requires — array of placeholder names the user must supply
|
|
26
|
+
* prompts — { placeholder: "interactive prompt text shown to user" }
|
|
27
|
+
* description — one-line summary shown by `mcp list`/`mcp search`
|
|
28
|
+
* tags — keywords for `mcp search` matching
|
|
29
|
+
* source — "official" (anthropic / first-party orgs), "community"
|
|
30
|
+
*/
|
|
31
|
+
export const MCP_REGISTRY = [
|
|
32
|
+
{
|
|
33
|
+
id: "filesystem",
|
|
34
|
+
command: "npx",
|
|
35
|
+
args: ["-y", "@modelcontextprotocol/server-filesystem", "{path}"],
|
|
36
|
+
requires: ["path"],
|
|
37
|
+
prompts: { path: "Allowed filesystem path (the server will be sandboxed here)" },
|
|
38
|
+
description: "Read/write/list files under a whitelisted directory",
|
|
39
|
+
tags: ["files", "fs", "disk", "filesystem", "local"],
|
|
40
|
+
source: "official",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: "github",
|
|
44
|
+
command: "npx",
|
|
45
|
+
args: ["-y", "@modelcontextprotocol/server-github"],
|
|
46
|
+
requires: [],
|
|
47
|
+
env: { GITHUB_PERSONAL_ACCESS_TOKEN: "{token}" },
|
|
48
|
+
requiresEnv: ["token"],
|
|
49
|
+
prompts: { token: "GitHub personal access token (https://github.com/settings/tokens — repo scope)" },
|
|
50
|
+
description: "Browse + edit GitHub repos, issues, PRs",
|
|
51
|
+
tags: ["github", "git", "repo", "issues", "pull request", "pr"],
|
|
52
|
+
source: "official",
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: "gitlab",
|
|
56
|
+
command: "npx",
|
|
57
|
+
args: ["-y", "@modelcontextprotocol/server-gitlab"],
|
|
58
|
+
requires: [],
|
|
59
|
+
env: { GITLAB_PERSONAL_ACCESS_TOKEN: "{token}", GITLAB_API_URL: "{url}" },
|
|
60
|
+
requiresEnv: ["token", "url"],
|
|
61
|
+
prompts: {
|
|
62
|
+
token: "GitLab personal access token (api scope)",
|
|
63
|
+
url: "GitLab API URL (default: https://gitlab.com/api/v4)",
|
|
64
|
+
},
|
|
65
|
+
description: "Browse + edit GitLab projects, issues, MRs",
|
|
66
|
+
tags: ["gitlab", "git", "repo", "issues", "merge request", "mr"],
|
|
67
|
+
source: "official",
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: "postgres",
|
|
71
|
+
command: "npx",
|
|
72
|
+
args: ["-y", "@modelcontextprotocol/server-postgres", "{connection}"],
|
|
73
|
+
requires: ["connection"],
|
|
74
|
+
prompts: { connection: "Postgres connection string (postgresql://user:pass@host:port/db)" },
|
|
75
|
+
description: "Query Postgres databases (read-only by default)",
|
|
76
|
+
tags: ["postgres", "postgresql", "database", "db", "sql"],
|
|
77
|
+
source: "official",
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: "sqlite",
|
|
81
|
+
command: "npx",
|
|
82
|
+
args: ["-y", "@modelcontextprotocol/server-sqlite", "{db_path}"],
|
|
83
|
+
requires: ["db_path"],
|
|
84
|
+
prompts: { db_path: "Path to the SQLite database file" },
|
|
85
|
+
description: "Query SQLite databases",
|
|
86
|
+
tags: ["sqlite", "database", "db", "sql"],
|
|
87
|
+
source: "official",
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: "puppeteer",
|
|
91
|
+
command: "npx",
|
|
92
|
+
args: ["-y", "@modelcontextprotocol/server-puppeteer"],
|
|
93
|
+
requires: [],
|
|
94
|
+
description: "Drive a headless browser via Puppeteer (navigate, screenshot, fill forms, scrape)",
|
|
95
|
+
tags: ["browser", "headless", "scrape", "puppeteer", "automation", "chrome"],
|
|
96
|
+
source: "official",
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: "playwright",
|
|
100
|
+
command: "npx",
|
|
101
|
+
args: ["-y", "@playwright/mcp"],
|
|
102
|
+
requires: [],
|
|
103
|
+
description: "Drive Chrome/Firefox/WebKit via Playwright (more capable than puppeteer)",
|
|
104
|
+
tags: ["browser", "playwright", "scrape", "automation", "chromium", "firefox", "webkit"],
|
|
105
|
+
source: "official",
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
id: "slack",
|
|
109
|
+
command: "npx",
|
|
110
|
+
args: ["-y", "@modelcontextprotocol/server-slack"],
|
|
111
|
+
requires: [],
|
|
112
|
+
env: { SLACK_BOT_TOKEN: "{bot_token}", SLACK_TEAM_ID: "{team_id}" },
|
|
113
|
+
requiresEnv: ["bot_token", "team_id"],
|
|
114
|
+
prompts: {
|
|
115
|
+
bot_token: "Slack bot token (xoxb-…)",
|
|
116
|
+
team_id: "Slack team/workspace ID (T…)",
|
|
117
|
+
},
|
|
118
|
+
description: "Read + post in Slack channels, list users, fetch threads",
|
|
119
|
+
tags: ["slack", "chat", "messaging", "workspace"],
|
|
120
|
+
source: "official",
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
id: "google-drive",
|
|
124
|
+
command: "npx",
|
|
125
|
+
args: ["-y", "@modelcontextprotocol/server-gdrive"],
|
|
126
|
+
requires: [],
|
|
127
|
+
description: "List + read Google Drive files (requires OAuth setup, see docs)",
|
|
128
|
+
tags: ["google", "drive", "gdrive", "files", "cloud"],
|
|
129
|
+
source: "official",
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
id: "memory",
|
|
133
|
+
command: "npx",
|
|
134
|
+
args: ["-y", "@modelcontextprotocol/server-memory"],
|
|
135
|
+
requires: [],
|
|
136
|
+
description: "Persistent knowledge-graph memory across agent sessions",
|
|
137
|
+
tags: ["memory", "kg", "knowledge graph", "persistence", "notes"],
|
|
138
|
+
source: "official",
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
id: "fetch",
|
|
142
|
+
command: "npx",
|
|
143
|
+
args: ["-y", "@modelcontextprotocol/server-fetch"],
|
|
144
|
+
requires: [],
|
|
145
|
+
description: "Fetch web pages + convert to markdown (alternative to built-in web_fetch)",
|
|
146
|
+
tags: ["http", "fetch", "web", "url", "markdown"],
|
|
147
|
+
source: "official",
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
id: "everart",
|
|
151
|
+
command: "npx",
|
|
152
|
+
args: ["-y", "@modelcontextprotocol/server-everart"],
|
|
153
|
+
requires: [],
|
|
154
|
+
env: { EVERART_API_KEY: "{key}" },
|
|
155
|
+
requiresEnv: ["key"],
|
|
156
|
+
prompts: { key: "EverArt API key (https://everart.ai)" },
|
|
157
|
+
description: "Generate images via EverArt",
|
|
158
|
+
tags: ["image", "generate", "art", "everart"],
|
|
159
|
+
source: "official",
|
|
160
|
+
},
|
|
161
|
+
];
|
|
162
|
+
|
|
163
|
+
const PLACEHOLDER_RE = /\{([a-zA-Z0-9_]+)\}/g;
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Look up a registry entry by id. Returns null for unknown names.
|
|
167
|
+
*/
|
|
168
|
+
export function findRegistryEntry(id) {
|
|
169
|
+
return MCP_REGISTRY.find((e) => e.id === id) ?? null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Apply user-provided placeholder values to an entry's args + env.
|
|
174
|
+
* Returns { command, args, env } ready to pass to addServer(), or
|
|
175
|
+
* throws if a required placeholder wasn't supplied.
|
|
176
|
+
*
|
|
177
|
+
* resolveEntry(filesystemEntry, { path: "/tmp" })
|
|
178
|
+
* → { command: "npx", args: ["-y", "...", "/tmp"], env: {} }
|
|
179
|
+
*/
|
|
180
|
+
export function resolveEntry(entry, values) {
|
|
181
|
+
if (!entry) throw new Error("resolveEntry: entry is required");
|
|
182
|
+
const seen = new Set();
|
|
183
|
+
const substitute = (s) =>
|
|
184
|
+
s.replace(PLACEHOLDER_RE, (_, key) => {
|
|
185
|
+
seen.add(key);
|
|
186
|
+
const v = values?.[key];
|
|
187
|
+
if (v === undefined || v === null || v === "") {
|
|
188
|
+
throw new Error(`Missing required value for {${key}}`);
|
|
189
|
+
}
|
|
190
|
+
return String(v);
|
|
191
|
+
});
|
|
192
|
+
const args = (entry.args ?? []).map(substitute);
|
|
193
|
+
const env = {};
|
|
194
|
+
for (const [k, raw] of Object.entries(entry.env ?? {})) {
|
|
195
|
+
env[k] = substitute(raw);
|
|
196
|
+
}
|
|
197
|
+
// Sanity-check that we got every required placeholder.
|
|
198
|
+
for (const k of entry.requires ?? []) {
|
|
199
|
+
if (!seen.has(k)) {
|
|
200
|
+
// Required but never referenced in args — flag in case the registry
|
|
201
|
+
// entry is malformed. Defense in depth.
|
|
202
|
+
throw new Error(`Registry entry "${entry.id}": "${k}" listed in requires but never used`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
for (const k of entry.requiresEnv ?? []) {
|
|
206
|
+
if (!seen.has(k)) {
|
|
207
|
+
throw new Error(`Registry entry "${entry.id}": env placeholder "${k}" listed but never used`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return { command: entry.command, args, env };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Search registry by free-text query. Matches id, description, and tags.
|
|
215
|
+
* Case-insensitive substring match. Returns entries ordered by relevance
|
|
216
|
+
* (id matches first, then tag matches, then description matches).
|
|
217
|
+
*/
|
|
218
|
+
export function searchRegistry(query) {
|
|
219
|
+
const q = (query || "").toLowerCase().trim();
|
|
220
|
+
if (!q) return [...MCP_REGISTRY];
|
|
221
|
+
const idHits = [];
|
|
222
|
+
const tagHits = [];
|
|
223
|
+
const descHits = [];
|
|
224
|
+
for (const e of MCP_REGISTRY) {
|
|
225
|
+
if (e.id.includes(q)) {
|
|
226
|
+
idHits.push(e);
|
|
227
|
+
} else if ((e.tags ?? []).some((t) => t.toLowerCase().includes(q))) {
|
|
228
|
+
tagHits.push(e);
|
|
229
|
+
} else if ((e.description ?? "").toLowerCase().includes(q)) {
|
|
230
|
+
descHits.push(e);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return [...idHits, ...tagHits, ...descHits];
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* For "unknown id" errors: return the closest registry ids to a typo,
|
|
238
|
+
* up to 3 suggestions. Simple edit-distance-style proximity using the
|
|
239
|
+
* length-diff + character-overlap heuristic — good enough to suggest
|
|
240
|
+
* "playwright" when the user types "playright".
|
|
241
|
+
*/
|
|
242
|
+
export function suggestSimilar(id) {
|
|
243
|
+
const target = id.toLowerCase();
|
|
244
|
+
const scored = MCP_REGISTRY.map((e) => ({
|
|
245
|
+
id: e.id,
|
|
246
|
+
score: similarity(target, e.id.toLowerCase()),
|
|
247
|
+
}));
|
|
248
|
+
scored.sort((a, b) => b.score - a.score);
|
|
249
|
+
return scored.filter((s) => s.score > 0.4).slice(0, 3).map((s) => s.id);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function similarity(a, b) {
|
|
253
|
+
if (a === b) return 1;
|
|
254
|
+
if (a.length === 0 || b.length === 0) return 0;
|
|
255
|
+
// Crude bigram-overlap. Good enough to catch one-character typos.
|
|
256
|
+
const bigrams = (s) => {
|
|
257
|
+
const out = new Set();
|
|
258
|
+
for (let i = 0; i < s.length - 1; i++) out.add(s.slice(i, i + 2));
|
|
259
|
+
return out;
|
|
260
|
+
};
|
|
261
|
+
const A = bigrams(a);
|
|
262
|
+
const B = bigrams(b);
|
|
263
|
+
let shared = 0;
|
|
264
|
+
for (const x of A) if (B.has(x)) shared++;
|
|
265
|
+
return (2 * shared) / (A.size + B.size);
|
|
266
|
+
}
|
package/src/render.js
CHANGED
|
@@ -1,58 +1,58 @@
|
|
|
1
|
-
// ANSI helpers — no chalk dependency.
|
|
2
|
-
|
|
3
|
-
const isTty = process.stdout.isTTY;
|
|
4
|
-
const noColor = !!process.env.NO_COLOR || !isTty;
|
|
5
|
-
|
|
6
|
-
function wrap(open, close) {
|
|
7
|
-
return (s) => (noColor ? String(s) : `\x1b[${open}m${s}\x1b[${close}m`);
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export const c = {
|
|
11
|
-
bold: wrap(1, 22),
|
|
12
|
-
dim: wrap(2, 22),
|
|
13
|
-
red: wrap(31, 39),
|
|
14
|
-
green: wrap(32, 39),
|
|
15
|
-
yellow: wrap(33, 39),
|
|
16
|
-
blue: wrap(34, 39),
|
|
17
|
-
magenta: wrap(35, 39),
|
|
18
|
-
cyan: wrap(36, 39),
|
|
19
|
-
gray: wrap(90, 39),
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
export function divider() {
|
|
23
|
-
const w = process.stdout.columns || 60;
|
|
24
|
-
return c.gray("─".repeat(Math.min(60, w)));
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function turn(n) {
|
|
28
|
-
return c.gray(`turn ${n}`);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function toolHeader(name, args) {
|
|
32
|
-
// Format args compactly. If any value is huge, truncate it.
|
|
33
|
-
const compact = JSON.stringify(args);
|
|
34
|
-
const trimmed = compact.length > 120 ? compact.slice(0, 117) + "..." : compact;
|
|
35
|
-
return `${c.cyan(c.bold(name))}${c.gray("(")}${c.gray(trimmed)}${c.gray(")")}`;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function toolResult(text, ok = true) {
|
|
39
|
-
const prefix = ok ? c.green(" ✓ ") : c.red(" ✗ ");
|
|
40
|
-
// First line bold-ish, then dim continuation
|
|
41
|
-
const lines = text.split("\n");
|
|
42
|
-
const head = lines[0].slice(0, 200);
|
|
43
|
-
const rest = lines.slice(1, 6).join("\n").slice(0, 600);
|
|
44
|
-
return `${prefix}${head}${rest ? "\n" + c.dim(rest) : ""}`;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function assistant(text) {
|
|
48
|
-
// Indent each line for visual separation from tool calls
|
|
49
|
-
return text.split("\n").map((l) => ` ${l}`).join("\n");
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function errorLine(msg) {
|
|
53
|
-
return `${c.red(c.bold("Error:"))} ${msg}`;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function note(msg) {
|
|
57
|
-
return c.dim(msg);
|
|
58
|
-
}
|
|
1
|
+
// ANSI helpers — no chalk dependency.
|
|
2
|
+
|
|
3
|
+
const isTty = process.stdout.isTTY;
|
|
4
|
+
const noColor = !!process.env.NO_COLOR || !isTty;
|
|
5
|
+
|
|
6
|
+
function wrap(open, close) {
|
|
7
|
+
return (s) => (noColor ? String(s) : `\x1b[${open}m${s}\x1b[${close}m`);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const c = {
|
|
11
|
+
bold: wrap(1, 22),
|
|
12
|
+
dim: wrap(2, 22),
|
|
13
|
+
red: wrap(31, 39),
|
|
14
|
+
green: wrap(32, 39),
|
|
15
|
+
yellow: wrap(33, 39),
|
|
16
|
+
blue: wrap(34, 39),
|
|
17
|
+
magenta: wrap(35, 39),
|
|
18
|
+
cyan: wrap(36, 39),
|
|
19
|
+
gray: wrap(90, 39),
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function divider() {
|
|
23
|
+
const w = process.stdout.columns || 60;
|
|
24
|
+
return c.gray("─".repeat(Math.min(60, w)));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function turn(n) {
|
|
28
|
+
return c.gray(`turn ${n}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function toolHeader(name, args) {
|
|
32
|
+
// Format args compactly. If any value is huge, truncate it.
|
|
33
|
+
const compact = JSON.stringify(args);
|
|
34
|
+
const trimmed = compact.length > 120 ? compact.slice(0, 117) + "..." : compact;
|
|
35
|
+
return `${c.cyan(c.bold(name))}${c.gray("(")}${c.gray(trimmed)}${c.gray(")")}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function toolResult(text, ok = true) {
|
|
39
|
+
const prefix = ok ? c.green(" ✓ ") : c.red(" ✗ ");
|
|
40
|
+
// First line bold-ish, then dim continuation
|
|
41
|
+
const lines = text.split("\n");
|
|
42
|
+
const head = lines[0].slice(0, 200);
|
|
43
|
+
const rest = lines.slice(1, 6).join("\n").slice(0, 600);
|
|
44
|
+
return `${prefix}${head}${rest ? "\n" + c.dim(rest) : ""}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function assistant(text) {
|
|
48
|
+
// Indent each line for visual separation from tool calls
|
|
49
|
+
return text.split("\n").map((l) => ` ${l}`).join("\n");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function errorLine(msg) {
|
|
53
|
+
return `${c.red(c.bold("Error:"))} ${msg}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function note(msg) {
|
|
57
|
+
return c.dim(msg);
|
|
58
|
+
}
|