min-agent 0.1.0 → 0.1.2

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.
Files changed (56) hide show
  1. package/README.md +6 -0
  2. package/bin/min-agent.js +2 -2
  3. package/dist/agent.js +566 -0
  4. package/dist/assistant-stream.js +114 -0
  5. package/dist/cli.js +471 -0
  6. package/dist/compaction.js +99 -0
  7. package/dist/config.js +142 -0
  8. package/dist/confirm.js +37 -0
  9. package/dist/instructions.js +115 -0
  10. package/dist/markdown.js +130 -0
  11. package/dist/mcp.js +237 -0
  12. package/dist/memory.js +131 -0
  13. package/dist/output.js +52 -0
  14. package/dist/plugins.js +66 -0
  15. package/dist/provider.js +41 -0
  16. package/dist/serve.js +351 -0
  17. package/dist/sessions.js +74 -0
  18. package/dist/skills.js +127 -0
  19. package/dist/tool-output.js +119 -0
  20. package/dist/tools/bash.js +93 -0
  21. package/dist/tools/edit.js +51 -0
  22. package/dist/tools/glob.js +36 -0
  23. package/dist/tools/grep.js +35 -0
  24. package/dist/tools/index.js +20 -0
  25. package/dist/tools/read.js +36 -0
  26. package/dist/tools/web_fetch.js +83 -0
  27. package/dist/tools/web_search.js +40 -0
  28. package/dist/tools/write.js +32 -0
  29. package/package.json +4 -5
  30. package/src/agent.ts +0 -609
  31. package/src/assistant-stream.ts +0 -128
  32. package/src/cli.ts +0 -494
  33. package/src/compaction.ts +0 -119
  34. package/src/config.ts +0 -172
  35. package/src/confirm.ts +0 -42
  36. package/src/instructions.ts +0 -123
  37. package/src/markdown.ts +0 -140
  38. package/src/mcp.ts +0 -300
  39. package/src/memory.ts +0 -164
  40. package/src/output.ts +0 -58
  41. package/src/plugins.ts +0 -94
  42. package/src/provider.ts +0 -50
  43. package/src/serve.ts +0 -400
  44. package/src/sessions.ts +0 -94
  45. package/src/skills.ts +0 -146
  46. package/src/tool-output.ts +0 -146
  47. package/src/tools/bash.ts +0 -108
  48. package/src/tools/edit.ts +0 -65
  49. package/src/tools/glob.ts +0 -37
  50. package/src/tools/grep.ts +0 -37
  51. package/src/tools/index.ts +0 -21
  52. package/src/tools/read.ts +0 -38
  53. package/src/tools/web_fetch.ts +0 -87
  54. package/src/tools/web_search.ts +0 -42
  55. package/src/tools/write.ts +0 -36
  56. package/tsconfig.json +0 -15
package/README.md CHANGED
@@ -5,10 +5,16 @@
5
5
 
6
6
  ## 安装
7
7
 
8
+ 需要本机已安装 **Node.js 20+**(LTS)。`npm install -g` 与 `npx -y min-agent` 使用发布包内的 **编译产物**,**不要求安装 Bun**。
9
+
8
10
  ```bash
9
11
  npm install -g min-agent
12
+ # 或
13
+ npx -y min-agent --help
10
14
  ```
11
15
 
16
+ 从本仓库开发时:先执行 `npm install` 与 `npm run build`,再运行 `node bin/min-agent.js …`(或继续用 `bun run src/cli.ts` 做开发)。
17
+
12
18
  ## 快速开始
13
19
 
14
20
  ```bash
package/bin/min-agent.js CHANGED
@@ -1,2 +1,2 @@
1
- #!/usr/bin/env bun
2
- import "../src/cli.ts"
1
+ #!/usr/bin/env node
2
+ import "../dist/cli.js"
package/dist/agent.js ADDED
@@ -0,0 +1,566 @@
1
+ import { streamText, stepCountIs } from "ai";
2
+ import { readFileSync, existsSync } from "fs";
3
+ import path from "path";
4
+ import { resolveModel } from "./provider.js";
5
+ import { createTools } from "./tools/index.js";
6
+ import { initMcp, shutdownMcp, getMcpTools, loadMcpConfig, getMcpStatus } from "./mcp.js";
7
+ import { discoverSkills, getSkillsTool, getSkillsSystemPrompt, getSkills } from "./skills.js";
8
+ import { loadInstructions } from "./instructions.js";
9
+ import { getMemorySystemPrompt, getMemoryTools } from "./memory.js";
10
+ import { needsCompaction, compactMessages, estimateTokens } from "./compaction.js";
11
+ import { loadPluginTools } from "./plugins.js";
12
+ import { MarkdownRenderer } from "./markdown.js";
13
+ import { ThinkingBodySplitter, stripThinkingFromAssistantText } from "./assistant-stream.js";
14
+ import { printHeader, printDivider, printToolCall, printToolResult, printDone } from "./output.js";
15
+ import readline from "readline";
16
+ const MAX_STEPS = 30;
17
+ function dimStyle() {
18
+ if ("NO_COLOR" in process.env)
19
+ return { dim: "", reset: "" };
20
+ return { dim: "\x1b[90m", reset: "\x1b[0m" };
21
+ }
22
+ /** Shown immediately so the terminal does not look frozen while MCP / rules load. */
23
+ function printInitLoading() {
24
+ const { dim, reset } = dimStyle();
25
+ console.log(`${dim}⟳ 正在初始化(MCP、技能、规则)…${reset}`);
26
+ }
27
+ function printInitReady() {
28
+ const { dim, reset } = dimStyle();
29
+ console.log(`${dim}✓ 就绪${reset}`);
30
+ }
31
+ /** Stream thinking to stderr (dim). Set MIN_AGENT_SHOW_THINKING=0 to hide. */
32
+ function writeThinkingDelta(text) {
33
+ if (!text)
34
+ return;
35
+ if (process.env.MIN_AGENT_SHOW_THINKING === "0" || process.env.MIN_AGENT_SHOW_THINKING === "false")
36
+ return;
37
+ const noColor = "NO_COLOR" in process.env;
38
+ const dim = noColor ? "" : "\x1b[2m";
39
+ const reset = noColor ? "" : "\x1b[0m";
40
+ process.stderr.write(`${dim}${text}${reset}`);
41
+ }
42
+ function buildSystemPrompt(instructions) {
43
+ const parts = [
44
+ `You are a helpful coding agent. You can read files, write files, run shell commands, search the web, and search the codebase to help the user with software engineering tasks.`,
45
+ "",
46
+ "Be concise and direct. When you run a command, briefly explain why.",
47
+ "Use the available tools to complete tasks. When multiple independent operations are needed, call tools in parallel.",
48
+ "When the user asks about current events, news, or anything requiring up-to-date information, use the web_search tool.",
49
+ "",
50
+ `Working directory: ${process.cwd()}`,
51
+ `Platform: ${process.platform}`,
52
+ `Date: ${new Date().toDateString()}`,
53
+ "",
54
+ "min-agent: when the user asks to configure or install this agent (MCP, skills, rules, CLI, HTTP API, etc.), use the **read** tool on `README.md` in the working directory first, then follow what it says.",
55
+ ];
56
+ const skillsPrompt = getSkillsSystemPrompt();
57
+ if (skillsPrompt) {
58
+ parts.push("", skillsPrompt);
59
+ }
60
+ const memoryPrompt = getMemorySystemPrompt();
61
+ if (memoryPrompt) {
62
+ parts.push("", memoryPrompt);
63
+ }
64
+ if (instructions.length > 0) {
65
+ parts.push("", "# User Instructions", "");
66
+ parts.push(...instructions);
67
+ }
68
+ return parts.join("\n");
69
+ }
70
+ /** Build user message content, optionally with images */
71
+ export async function buildUserContent(message, imagePaths) {
72
+ if (!imagePaths || imagePaths.length === 0)
73
+ return message;
74
+ const parts = [{ type: "text", text: message }];
75
+ for (const imgPath of imagePaths) {
76
+ const resolved = path.resolve(process.cwd(), imgPath);
77
+ if (!existsSync(resolved)) {
78
+ console.error(`\x1b[33m Warning: Image not found: ${imgPath}\x1b[0m`);
79
+ continue;
80
+ }
81
+ const data = readFileSync(resolved);
82
+ const ext = path.extname(resolved).toLowerCase();
83
+ const mimeMap = {
84
+ ".png": "image/png",
85
+ ".jpg": "image/jpeg",
86
+ ".jpeg": "image/jpeg",
87
+ ".gif": "image/gif",
88
+ ".webp": "image/webp",
89
+ };
90
+ const mimeType = mimeMap[ext] ?? "image/png";
91
+ parts.push({
92
+ type: "image",
93
+ image: data,
94
+ mimeType,
95
+ });
96
+ console.log(`\x1b[90m 📎 ${imgPath}\x1b[0m`);
97
+ }
98
+ return parts;
99
+ }
100
+ /** Single-shot: send one message, get response, exit */
101
+ export async function runAgent(message, modelId, imagePaths) {
102
+ printHeader(modelId);
103
+ printDivider();
104
+ printInitLoading();
105
+ await initMcp();
106
+ discoverSkills();
107
+ const instructions = await loadInstructions();
108
+ printInitReady();
109
+ console.log(`\x1b[36m> ${message}\x1b[0m\n`);
110
+ const content = await buildUserContent(message, imagePaths);
111
+ const messages = [{ role: "user", content }];
112
+ await runOnce(messages, instructions, modelId);
113
+ await shutdownMcp();
114
+ }
115
+ /** Interactive multi-turn chat session */
116
+ export async function runChat(modelId, resumeSessionId) {
117
+ printHeader(modelId);
118
+ printDivider();
119
+ printInitLoading();
120
+ await initMcp();
121
+ discoverSkills();
122
+ const instructions = await loadInstructions();
123
+ printInitReady();
124
+ let messages = [];
125
+ let sessionId = resumeSessionId;
126
+ // Resume existing session
127
+ if (resumeSessionId) {
128
+ const { loadSession } = await import("./sessions.js");
129
+ const session = loadSession(resumeSessionId);
130
+ if (session) {
131
+ messages = session.messages;
132
+ sessionId = resumeSessionId;
133
+ console.log(`\x1b[90m Resumed session: ${session.meta.title} (${messages.length} messages)\x1b[0m`);
134
+ }
135
+ }
136
+ const rl = readline.createInterface({
137
+ input: process.stdin,
138
+ output: process.stdout,
139
+ prompt: "\x1b[36m> \x1b[0m",
140
+ });
141
+ // Handle Ctrl+C: abort current generation, don't exit
142
+ let abortController = null;
143
+ process.on("SIGINT", () => {
144
+ if (abortController) {
145
+ abortController.abort();
146
+ abortController = null;
147
+ console.log("\n\x1b[90m(interrupted)\x1b[0m\n");
148
+ rl.prompt();
149
+ }
150
+ else {
151
+ // No active generation, exit
152
+ console.log();
153
+ rl.close();
154
+ }
155
+ });
156
+ console.log("\x1b[90m输入消息开始对话,输入 /help 查看命令,/exit 退出\x1b[0m\n");
157
+ rl.prompt();
158
+ for await (const line of rl) {
159
+ const input = line.trim();
160
+ if (!input) {
161
+ rl.prompt();
162
+ continue;
163
+ }
164
+ // Handle slash commands
165
+ if (input.startsWith("/")) {
166
+ const handled = await handleSlashCommand(input, messages, instructions, modelId, rl);
167
+ if (handled === "exit")
168
+ break;
169
+ rl.prompt();
170
+ continue;
171
+ }
172
+ console.log();
173
+ messages.push({ role: "user", content: input });
174
+ abortController = new AbortController();
175
+ await runOnce(messages, instructions, modelId, abortController.signal);
176
+ abortController = null;
177
+ console.log();
178
+ rl.prompt();
179
+ }
180
+ // Auto-save session on exit
181
+ if (messages.length > 0) {
182
+ const { saveSession } = await import("./sessions.js");
183
+ sessionId = saveSession(messages, sessionId);
184
+ console.log(`\x1b[90m Session saved: ${sessionId}\x1b[0m`);
185
+ }
186
+ rl.close();
187
+ printDivider();
188
+ console.log("\x1b[90mBye!\x1b[0m");
189
+ await shutdownMcp();
190
+ }
191
+ async function handleSlashCommand(input, messages, instructions, modelId, rl) {
192
+ const [cmd, ...rest] = input.slice(1).split(/\s+/);
193
+ const arg = rest.join(" ");
194
+ switch (cmd) {
195
+ case "exit":
196
+ case "quit":
197
+ case "q":
198
+ return "exit";
199
+ case "clear":
200
+ messages.length = 0;
201
+ console.log("\x1b[90m ✓ Conversation cleared\x1b[0m");
202
+ return "handled";
203
+ case "compact":
204
+ if (messages.length < 4) {
205
+ console.log("\x1b[90m Not enough messages to compact\x1b[0m");
206
+ }
207
+ else {
208
+ console.log("\x1b[90m ⟳ Compacting...\x1b[0m");
209
+ const model = resolveModel(modelId);
210
+ const result = await compactMessages(messages, model, { keepRecentTurns: 2 });
211
+ messages.length = 0;
212
+ messages.push(...result.messages);
213
+ console.log(`\x1b[90m ✓ Compacted (${estimateTokens(messages)} tokens estimated)\x1b[0m`);
214
+ }
215
+ return "handled";
216
+ case "model":
217
+ if (arg) {
218
+ const config = await import("./config.js");
219
+ const cfg = config.loadConfig();
220
+ if (cfg.provider) {
221
+ cfg.provider.defaultModel = arg;
222
+ config.saveConfig(cfg);
223
+ console.log(`\x1b[90m ✓ Default model set to: ${arg}\x1b[0m`);
224
+ }
225
+ }
226
+ else {
227
+ const config = await import("./config.js");
228
+ const cfg = config.loadConfig();
229
+ console.log(`\x1b[90m Current model: ${cfg.provider?.defaultModel ?? "not set"}\x1b[0m`);
230
+ }
231
+ return "handled";
232
+ case "models": {
233
+ const config = await import("./config.js");
234
+ const cfg = config.loadConfig();
235
+ if (!cfg.provider?.baseURL || !cfg.provider?.apiKey) {
236
+ console.log("\x1b[90m Not configured. Run: min-agent setup\x1b[0m");
237
+ return "handled";
238
+ }
239
+ console.log("\x1b[90m Fetching models...\x1b[0m");
240
+ const models = await config.fetchModels(cfg.provider.baseURL, cfg.provider.apiKey);
241
+ if (models.length === 0) {
242
+ console.log("\x1b[90m No models found or unable to fetch model list\x1b[0m");
243
+ }
244
+ else {
245
+ console.log(`\x1b[90m Available models (${models.length}):\x1b[0m`);
246
+ for (const m of models) {
247
+ const marker = m === cfg.provider.defaultModel ? " ← default" : "";
248
+ console.log(`\x1b[90m - ${m}${marker}\x1b[0m`);
249
+ }
250
+ }
251
+ return "handled";
252
+ }
253
+ case "memory":
254
+ if (arg) {
255
+ const { addMemory } = await import("./memory.js");
256
+ addMemory(arg);
257
+ console.log(`\x1b[90m ✓ Memory saved: "${arg}"\x1b[0m`);
258
+ }
259
+ else {
260
+ const { loadMemories } = await import("./memory.js");
261
+ const memories = loadMemories();
262
+ if (memories.length === 0) {
263
+ console.log("\x1b[90m No memories stored\x1b[0m");
264
+ }
265
+ else {
266
+ for (let i = 0; i < memories.length; i++) {
267
+ const m = memories[i];
268
+ const tags = m.tags.length > 0 ? ` [${m.tags.join(", ")}]` : "";
269
+ console.log(`\x1b[90m #${i + 1}: ${m.content}${tags}\x1b[0m`);
270
+ }
271
+ }
272
+ }
273
+ return "handled";
274
+ case "skills": {
275
+ discoverSkills();
276
+ const skills = getSkills();
277
+ if (skills.length === 0) {
278
+ console.log("\x1b[90m No skills found\x1b[0m");
279
+ }
280
+ else {
281
+ console.log(`\x1b[90m Available skills (${skills.length}):\x1b[0m`);
282
+ for (const skill of skills) {
283
+ console.log(`\x1b[90m - ${skill.name}: ${skill.description}\x1b[0m`);
284
+ }
285
+ }
286
+ return "handled";
287
+ }
288
+ case "mcp": {
289
+ const config = loadMcpConfig();
290
+ const servers = Object.entries(config.mcpServers);
291
+ if (servers.length === 0) {
292
+ console.log("\x1b[90m No MCP servers configured\x1b[0m");
293
+ }
294
+ else {
295
+ const status = getMcpStatus();
296
+ console.log(`\x1b[90m MCP servers (${servers.length}):\x1b[0m`);
297
+ for (const [name, cfg] of servers) {
298
+ const disabled = cfg.enabled === false;
299
+ const connected = status[name]?.connected ?? false;
300
+ const toolCount = status[name]?.tools.length ?? 0;
301
+ const state = disabled ? "disabled" : connected ? "connected" : "disconnected";
302
+ const toolsText = toolCount > 0 ? `, ${toolCount} tools` : "";
303
+ console.log(`\x1b[90m - ${name}: ${state}${toolsText}\x1b[0m`);
304
+ }
305
+ }
306
+ return "handled";
307
+ }
308
+ case "tokens":
309
+ console.log(`\x1b[90m Estimated tokens in context: ${estimateTokens(messages)}\x1b[0m`);
310
+ console.log(`\x1b[90m Messages: ${messages.length}\x1b[0m`);
311
+ return "handled";
312
+ case "help":
313
+ console.log(`\x1b[90m Slash commands:
314
+ /clear Clear conversation history
315
+ /compact Force context compaction
316
+ /model [name] Show or change current model
317
+ /models List available models from provider
318
+ /memory [text] List memories or save a new one
319
+ /skills List discovered skills
320
+ /mcp List MCP servers and connection status
321
+ /tokens Show estimated token usage
322
+ /help Show this help
323
+ /exit Exit the chat\x1b[0m`);
324
+ return "handled";
325
+ default:
326
+ console.log(`\x1b[90m Unknown command: /${cmd}. Type /help for available commands.\x1b[0m`);
327
+ return "handled";
328
+ }
329
+ }
330
+ export async function runOnce(messages, instructions, modelId, abortSignal, callbacks) {
331
+ const model = resolveModel(modelId);
332
+ const api = !!callbacks;
333
+ // Auto-compact if context is getting too large
334
+ if (needsCompaction(messages)) {
335
+ if (api) {
336
+ callbacks.onCompaction?.("compacting_start");
337
+ }
338
+ else {
339
+ console.log("\x1b[90m⟳ Compacting context...\x1b[0m");
340
+ }
341
+ const result = await compactMessages(messages, model);
342
+ if (result.compacted) {
343
+ messages.length = 0;
344
+ messages.push(...result.messages);
345
+ if (api) {
346
+ callbacks.onCompaction?.(`compacted_ok estimated_tokens=${estimateTokens(messages)}`);
347
+ }
348
+ else {
349
+ console.log(`\x1b[90m ✓ Compacted (${estimateTokens(messages)} tokens estimated)\x1b[0m`);
350
+ }
351
+ }
352
+ }
353
+ // Merge all tools: builtin + MCP + skill + memory + plugins
354
+ const builtinTools = createTools();
355
+ const mcpTools = getMcpTools();
356
+ const memoryTools = getMemoryTools();
357
+ const pluginTools = await loadPluginTools();
358
+ const skills = getSkills();
359
+ const allTools = { ...builtinTools, ...memoryTools, ...pluginTools };
360
+ for (const [id, t] of Object.entries(mcpTools)) {
361
+ allTools[id] = t;
362
+ }
363
+ if (skills.length > 0) {
364
+ allTools["skill"] = getSkillsTool();
365
+ }
366
+ let stepCount = 0;
367
+ let hasError = false;
368
+ const result = streamText({
369
+ model,
370
+ system: buildSystemPrompt(instructions),
371
+ messages,
372
+ tools: allTools,
373
+ stopWhen: stepCountIs(MAX_STEPS),
374
+ maxRetries: 3,
375
+ abortSignal,
376
+ onStepFinish() {
377
+ stepCount++;
378
+ },
379
+ onError() { },
380
+ });
381
+ Promise.resolve(result.usage).catch(() => { });
382
+ let rawText = "";
383
+ let assistantText = "";
384
+ const md = new MarkdownRenderer();
385
+ const thinkingSplit = new ThinkingBodySplitter();
386
+ const emitThinking = (t) => {
387
+ if (!t)
388
+ return;
389
+ if (callbacks?.onThinkingDelta)
390
+ callbacks.onThinkingDelta(t);
391
+ else
392
+ writeThinkingDelta(t);
393
+ };
394
+ try {
395
+ for await (const event of result.fullStream) {
396
+ switch (event.type) {
397
+ case "text-delta": {
398
+ const { display, thinking } = thinkingSplit.feed(event.text);
399
+ emitThinking(thinking);
400
+ if (display) {
401
+ rawText += display;
402
+ assistantText += display;
403
+ if (callbacks?.onAssistantDisplayDelta)
404
+ callbacks.onAssistantDisplayDelta(display);
405
+ else {
406
+ const formatted = md.write(display);
407
+ if (formatted)
408
+ process.stdout.write(formatted);
409
+ }
410
+ }
411
+ break;
412
+ }
413
+ case "tool-call": {
414
+ const splitFlush = thinkingSplit.flush();
415
+ emitThinking(splitFlush.thinking);
416
+ if (splitFlush.display) {
417
+ rawText += splitFlush.display;
418
+ assistantText += splitFlush.display;
419
+ if (callbacks?.onAssistantDisplayDelta)
420
+ callbacks.onAssistantDisplayDelta(splitFlush.display);
421
+ else {
422
+ const extra = md.write(splitFlush.display);
423
+ if (extra)
424
+ process.stdout.write(extra);
425
+ }
426
+ }
427
+ if (!callbacks) {
428
+ const flushed = md.flush();
429
+ if (flushed)
430
+ process.stdout.write(flushed);
431
+ if (rawText.trim())
432
+ console.log();
433
+ }
434
+ rawText = "";
435
+ if (callbacks?.onToolCall)
436
+ callbacks.onToolCall(event.toolName, event.input);
437
+ else
438
+ printToolCall(event.toolName, event.input);
439
+ break;
440
+ }
441
+ case "tool-result":
442
+ if (callbacks?.onToolResult)
443
+ callbacks.onToolResult(event.toolName, event.output);
444
+ else
445
+ printToolResult(event.toolName, event.output);
446
+ break;
447
+ case "error":
448
+ hasError = true;
449
+ const errorMsg = String(event.error);
450
+ if (callbacks?.onStreamError) {
451
+ callbacks.onStreamError(errorMsg);
452
+ }
453
+ else if (errorMsg.includes("Forbidden") || errorMsg.includes("Unauthorized") || errorMsg.includes("API key")) {
454
+ console.error(`\x1b[31mAuthentication error: Check your API key.\x1b[0m`);
455
+ }
456
+ else {
457
+ console.error(`\x1b[31mError: ${errorMsg}\x1b[0m`);
458
+ }
459
+ break;
460
+ case "finish":
461
+ break;
462
+ }
463
+ }
464
+ const splitEnd = thinkingSplit.flush();
465
+ emitThinking(splitEnd.thinking);
466
+ if (splitEnd.display) {
467
+ rawText += splitEnd.display;
468
+ assistantText += splitEnd.display;
469
+ if (callbacks?.onAssistantDisplayDelta)
470
+ callbacks.onAssistantDisplayDelta(splitEnd.display);
471
+ else {
472
+ const tail = md.write(splitEnd.display);
473
+ if (tail)
474
+ process.stdout.write(tail);
475
+ }
476
+ }
477
+ if (!callbacks) {
478
+ const remaining = md.flush();
479
+ if (remaining)
480
+ process.stdout.write(remaining);
481
+ if (rawText.trim())
482
+ console.log();
483
+ }
484
+ const cleanedAssistant = stripThinkingFromAssistantText(assistantText);
485
+ if (cleanedAssistant.trim()) {
486
+ messages.push({ role: "assistant", content: cleanedAssistant });
487
+ }
488
+ let usage;
489
+ try {
490
+ usage = await result.usage;
491
+ }
492
+ catch {
493
+ usage = undefined;
494
+ }
495
+ if (callbacks?.onRunFinish) {
496
+ callbacks.onRunFinish({ stepCount, usage, hasError, aborted: false });
497
+ }
498
+ else {
499
+ if (hasError) {
500
+ printDivider();
501
+ return;
502
+ }
503
+ printDivider();
504
+ printDone(stepCount, usage);
505
+ }
506
+ }
507
+ catch (err) {
508
+ if (!callbacks && rawText.trim())
509
+ console.log();
510
+ if (err.name === "AbortError" || abortSignal?.aborted) {
511
+ const splitAbort = thinkingSplit.flush();
512
+ emitThinking(splitAbort.thinking);
513
+ if (splitAbort.display) {
514
+ assistantText += splitAbort.display;
515
+ if (callbacks?.onAssistantDisplayDelta)
516
+ callbacks.onAssistantDisplayDelta(splitAbort.display);
517
+ else {
518
+ const w = md.write(splitAbort.display);
519
+ if (w)
520
+ process.stdout.write(w);
521
+ }
522
+ }
523
+ if (!callbacks)
524
+ process.stdout.write(md.flush());
525
+ const cleaned = stripThinkingFromAssistantText(assistantText);
526
+ if (cleaned.trim()) {
527
+ messages.push({ role: "assistant", content: cleaned });
528
+ }
529
+ let usage;
530
+ try {
531
+ usage = await result.usage;
532
+ }
533
+ catch {
534
+ usage = undefined;
535
+ }
536
+ callbacks?.onRunFinish?.({ stepCount, usage, hasError: false, aborted: true });
537
+ return;
538
+ }
539
+ if (!callbacks)
540
+ printDivider();
541
+ const msg = err.message ?? String(err);
542
+ if (callbacks?.onStreamError) {
543
+ callbacks.onStreamError(msg);
544
+ }
545
+ else if (msg.includes("API key") || msg.includes("Unauthorized") || msg.includes("Forbidden")) {
546
+ console.error(`\x1b[31mAuthentication error: Check your API key.\x1b[0m`);
547
+ }
548
+ else if (msg.includes("429") || msg.includes("rate limit") || msg.includes("Rate limit")) {
549
+ console.error(`\x1b[31mRate limited after retries. Please wait and try again.\x1b[0m`);
550
+ }
551
+ else if (msg.includes("timeout") || msg.includes("ETIMEDOUT") || msg.includes("ECONNRESET")) {
552
+ console.error(`\x1b[31mNetwork error (retries exhausted): ${msg}\x1b[0m`);
553
+ }
554
+ else {
555
+ console.error(`\x1b[31mError: ${msg}\x1b[0m`);
556
+ }
557
+ let usage;
558
+ try {
559
+ usage = await result.usage;
560
+ }
561
+ catch {
562
+ usage = undefined;
563
+ }
564
+ callbacks?.onRunFinish?.({ stepCount, usage, hasError: true, aborted: false });
565
+ }
566
+ }