@axeai/axe 0.0.1-beta

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 ADDED
@@ -0,0 +1,53 @@
1
+ # axe
2
+
3
+ AI-powered TUI code assistant. Talk to your codebase from the terminal.
4
+
5
+ ![axe demo](public/image.png)
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ bun install -g axe-cli
11
+ ```
12
+
13
+ Or run directly:
14
+ ```bash
15
+ bunx axe-cli
16
+ ```
17
+
18
+ ## Features
19
+
20
+ - **Multi-provider** - Google, OpenAI, Anthropic, Groq, xAI, DeepSeek, Qwen, Kimi, MiniMax
21
+ - **File system access** - Read, write, search files via MCP
22
+ - **Shell commands** - Run terminal commands through AI
23
+ - **Per-directory sessions** - Chat history tied to your project
24
+ - **Lightweight** - Built with Bun + Ink
25
+ - **Ralph Loop** - Continuous autonomy for complex tasks (experimental)
26
+
27
+ ## Commands
28
+
29
+ | Command | Action |
30
+ |---------|--------|
31
+ | `/provider` | Switch AI provider |
32
+ | `/model` | Switch model |
33
+ | `/agent` | Switch agent type (Standard vs Ralph Loop) |
34
+ | `/history` | View chat sessions |
35
+ | `/clear` | Clear chat |
36
+
37
+ ## Config
38
+
39
+ API keys stored in `~/.axe/config.json`:
40
+
41
+ ```json
42
+ {
43
+ "provider": "google",
44
+ "model": "gemini-2.5-flash",
45
+ "keys": {
46
+ "google": "your-api-key"
47
+ }
48
+ }
49
+ ```
50
+
51
+ ## License
52
+
53
+ MIT
package/bin/axe.js ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bun
2
+ import "../src/index.tsx";
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@axeai/axe",
3
+ "version": "0.0.1-beta",
4
+ "description": "AI-powered TUI code editor with MCP filesystem access",
5
+ "module": "src/index.tsx",
6
+ "type": "module",
7
+ "bin": {
8
+ "axe": "bin/axe.js"
9
+ },
10
+ "files": [
11
+ "src",
12
+ "bin"
13
+ ],
14
+ "keywords": [
15
+ "ai",
16
+ "cli",
17
+ "tui",
18
+ "code-editor",
19
+ "mcp",
20
+ "gemini",
21
+ "openai",
22
+ "anthropic"
23
+ ],
24
+ "author": "iamanishx",
25
+ "license": "MIT",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/iamanishx/axe.git"
29
+ },
30
+ "devDependencies": {
31
+ "@types/bun": "latest",
32
+ "@types/react": "^19.2.7",
33
+ "@types/react-dom": "^19.2.3",
34
+ "bun-types": "^1.3.5"
35
+ },
36
+ "peerDependencies": {
37
+ "typescript": "^5"
38
+ },
39
+ "dependencies": {
40
+ "@ai-sdk/anthropic": "^3.0.1",
41
+ "@ai-sdk/google": "^3.0.1",
42
+ "@ai-sdk/groq": "^3.0.1",
43
+ "@ai-sdk/mcp": "^1.0.1",
44
+ "@ai-sdk/openai": "^3.0.1",
45
+ "@ai-sdk/openai-compatible": "^2.0.1",
46
+ "@ai-sdk/xai": "^3.0.1",
47
+ "@modelcontextprotocol/sdk": "^1.25.1",
48
+ "ai": "^6.0.3",
49
+ "ai-sdk-provider-gemini-cli": "^2.0.0",
50
+ "dotenv": "^17.2.3",
51
+ "ink": "^6.6.0",
52
+ "ink-spinner": "^5.0.0",
53
+ "ink-text-input": "^6.0.0",
54
+ "install": "^0.13.0",
55
+ "ralph-loop-agent": "^0.0.3",
56
+ "react": "^19.2.3",
57
+ "react-dom": "^19.2.3",
58
+ "zod": "^4.2.1"
59
+ }
60
+ }
package/src/app.tsx ADDED
@@ -0,0 +1,400 @@
1
+ import React, { useState, useEffect, useCallback } from "react";
2
+ import { Box, Text, useInput, Static } from "ink";
3
+ import { Header } from "./ui/header";
4
+ import { MessageComponent } from "./ui/message";
5
+ import { InputArea } from "./ui/input-area";
6
+ import { SessionPicker } from "./ui/session-picker";
7
+ import {
8
+ getRecentMessages,
9
+ getCurrentDirSessions,
10
+ getOtherDirSessions,
11
+ getSessionMessages,
12
+ getSessionId,
13
+ createNewSession,
14
+ setSessionId,
15
+ type Message,
16
+ type Session,
17
+ } from "./lib/db";
18
+ import { runAgentStream } from "./lib/agent";
19
+ import { loadConfig, setProvider, type ProviderName } from "./lib/config";
20
+ import { PROVIDER_MODELS } from "./lib/provider";
21
+ import { triggerRerender } from "./index";
22
+
23
+ type View = "session_picker" | "chat" | "history" | "provider" | "model" | "agent";
24
+
25
+ type AppProps = {
26
+ skipInitialLoad?: boolean;
27
+ };
28
+
29
+ export const App = ({ skipInitialLoad = false }: AppProps) => {
30
+ const [messages, setMessages] = useState<Message[]>([]);
31
+ const [streamingContent, setStreamingContent] = useState("");
32
+ const [thinking, setThinking] = useState<string | null>(null);
33
+ const [isLoading, setIsLoading] = useState(false);
34
+ const [error, setError] = useState<string | null>(null);
35
+ const [view, setView] = useState<View>("session_picker");
36
+ const [currentDirSessions, setCurrentDirSessions] = useState<Session[]>([]);
37
+ const [otherDirSessions, setOtherDirSessions] = useState<Session[]>([]);
38
+ const [selectedIdx, setSelectedIdx] = useState(0);
39
+ const [config, setConfig] = useState(loadConfig());
40
+ const [agentType, setAgentType] = useState<"tool-loop" | "ralph-loop">("tool-loop");
41
+
42
+ useEffect(() => {
43
+ if (skipInitialLoad) {
44
+ setView("chat");
45
+ return;
46
+ }
47
+
48
+ const sessions = getCurrentDirSessions();
49
+ setCurrentDirSessions(sessions);
50
+
51
+ if (sessions.length === 0) {
52
+ createNewSession();
53
+ setView("chat");
54
+ } else {
55
+ setView("session_picker");
56
+ }
57
+ }, [skipInitialLoad]);
58
+
59
+ const providers = Object.keys(PROVIDER_MODELS) as ProviderName[];
60
+ const currentModels = PROVIDER_MODELS[config.provider] || [];
61
+
62
+ const handleSessionSelect = (session: Session | null) => {
63
+ if (session === null) {
64
+ createNewSession();
65
+ setMessages([]);
66
+ } else {
67
+ setSessionId(session.id);
68
+ const msgs = getSessionMessages(session.id, 100);
69
+ setMessages(msgs);
70
+ }
71
+ setView("chat");
72
+ };
73
+
74
+ const handleSessionNavigate = (direction: "up" | "down") => {
75
+ const totalItems = currentDirSessions.length + 1;
76
+ if (direction === "up") {
77
+ setSelectedIdx((p) => Math.max(0, p - 1));
78
+ } else {
79
+ setSelectedIdx((p) => Math.min(totalItems - 1, p + 1));
80
+ }
81
+ };
82
+
83
+ useInput((input, key) => {
84
+ if (view === "history") {
85
+ if (key.escape || input === "q") {
86
+ setView("chat");
87
+ return;
88
+ }
89
+ const allSessions = [...currentDirSessions, ...otherDirSessions];
90
+ if (key.upArrow) setSelectedIdx((p) => Math.max(0, p - 1));
91
+ if (key.downArrow) setSelectedIdx((p) => Math.min(allSessions.length - 1, p + 1));
92
+ if (key.return && allSessions[selectedIdx]) {
93
+ const session = allSessions[selectedIdx];
94
+ const msgs = getSessionMessages(session.id, 1000);
95
+ setMessages(msgs);
96
+ setView("chat");
97
+ }
98
+ return;
99
+ }
100
+
101
+ if (view === "provider") {
102
+ if (key.escape || input === "q") {
103
+ setView("chat");
104
+ return;
105
+ }
106
+ if (key.upArrow) setSelectedIdx((p) => Math.max(0, p - 1));
107
+ if (key.downArrow) setSelectedIdx((p) => Math.min(providers.length - 1, p + 1));
108
+ if (key.return) {
109
+ const newProvider = providers[selectedIdx];
110
+ const defaultModel = PROVIDER_MODELS[newProvider][0];
111
+ setProvider(newProvider, defaultModel);
112
+ setConfig(loadConfig());
113
+ setView("chat");
114
+ }
115
+ return;
116
+ }
117
+
118
+ if (view === "model") {
119
+ if (key.escape || input === "q") {
120
+ setView("chat");
121
+ return;
122
+ }
123
+ if (key.upArrow) setSelectedIdx((p) => Math.max(0, p - 1));
124
+ if (key.downArrow) setSelectedIdx((p) => Math.min(currentModels.length - 1, p + 1));
125
+ if (key.return) {
126
+ const newModel = currentModels[selectedIdx];
127
+ setProvider(config.provider, newModel);
128
+ setConfig(loadConfig());
129
+ setView("chat");
130
+ }
131
+ return;
132
+ }
133
+ }, { isActive: view !== "chat" && view !== "session_picker" });
134
+
135
+ useInput((input, key) => {
136
+ if (view === "agent") {
137
+ if (key.escape || input === "q") {
138
+ setView("chat");
139
+ return;
140
+ }
141
+ if (key.upArrow) setSelectedIdx((p) => Math.max(0, p - 1));
142
+ if (key.downArrow) setSelectedIdx((p) => Math.min(agentTypes.length - 1, p + 1));
143
+ if (key.return) {
144
+ const newAgentType = agentTypes[selectedIdx] as "tool-loop" | "ralph-loop";
145
+ setAgentType(newAgentType);
146
+ setMessages(prev => [...prev, {
147
+ id: Date.now(),
148
+ session_id: getSessionId(),
149
+ role: "assistant",
150
+ content: `Switched agent to ${newAgentType === "tool-loop" ? "Tool Loop" : "Ralph Loop"}`,
151
+ created_at: new Date().toISOString(),
152
+ }]);
153
+ setView("chat");
154
+ }
155
+ return;
156
+ }
157
+ }, { isActive: view === "agent" });
158
+
159
+ const agentTypes = ["tool-loop", "ralph-loop"];
160
+
161
+ const handleInput = useCallback(async (input: string) => {
162
+ const cmd = input.toLowerCase().trim();
163
+
164
+ if (cmd === "/history") {
165
+ setCurrentDirSessions(getCurrentDirSessions());
166
+ setOtherDirSessions(getOtherDirSessions());
167
+ setSelectedIdx(0);
168
+ setView("history");
169
+ return;
170
+ }
171
+
172
+ if (cmd === "/provider") {
173
+ setSelectedIdx(providers.indexOf(config.provider));
174
+ setView("provider");
175
+ return;
176
+ }
177
+
178
+ if (cmd === "/model") {
179
+ setSelectedIdx(currentModels.indexOf(config.model));
180
+ setView("model");
181
+ return;
182
+ }
183
+
184
+ if (cmd === "/agent") {
185
+ setSelectedIdx(agentTypes.indexOf(agentType));
186
+ setView("agent");
187
+ return;
188
+ }
189
+
190
+ if (cmd === "/clear" || cmd === "/new") {
191
+ if (cmd === "/new") {
192
+ createNewSession();
193
+ }
194
+ triggerRerender();
195
+ return;
196
+ }
197
+
198
+ setIsLoading(true);
199
+ setError(null);
200
+
201
+ const currentSessionId = getSessionId();
202
+
203
+ const userMsg: Message = {
204
+ id: Date.now(),
205
+ session_id: currentSessionId,
206
+ role: "user",
207
+ content: input,
208
+ created_at: new Date().toISOString(),
209
+ };
210
+
211
+ setMessages((prev) => {
212
+ const newHistory = [...prev, userMsg];
213
+
214
+ (async () => {
215
+ try {
216
+ const agentHistory = newHistory.slice(-50).map((m) => ({
217
+ role: m.role as "user" | "assistant",
218
+ content: m.content,
219
+ }));
220
+
221
+ const fileRefs = input.match(/@([a-zA-Z0-9_./-]+)/g);
222
+ let finalInput = input;
223
+
224
+ if (fileRefs && fileRefs.length > 0) {
225
+ const files = fileRefs.map(ref => ref.substring(1)).join(", ");
226
+ finalInput = `${input}\n\n[System Note: The user referenced the following files: ${files}. Please read them if necessary to answer the query.]`;
227
+ }
228
+
229
+ const stream = runAgentStream(finalInput, agentHistory, agentType);
230
+
231
+ let accumulatedContent = "";
232
+
233
+ for await (const event of stream) {
234
+ if (event.type === "text") {
235
+ accumulatedContent += event.content;
236
+ setStreamingContent(accumulatedContent);
237
+ } else if (event.type === "thinking") {
238
+ setThinking(event.content);
239
+ }
240
+ }
241
+
242
+ const aiMsg: Message = {
243
+ id: Date.now() + 1,
244
+ session_id: currentSessionId,
245
+ role: "assistant",
246
+ content: accumulatedContent,
247
+ created_at: new Date().toISOString(),
248
+ };
249
+
250
+ setMessages((p) => [...p, aiMsg]);
251
+ } catch (e: any) {
252
+ setError(e.message);
253
+ } finally {
254
+ setIsLoading(false);
255
+ setStreamingContent("");
256
+ setThinking(null);
257
+ }
258
+ })();
259
+
260
+ return newHistory;
261
+ });
262
+ }, [providers, config.provider, currentModels, config.model, agentType]);
263
+
264
+ if (view === "session_picker") {
265
+ return (
266
+ <SessionPicker
267
+ currentDirSessions={currentDirSessions}
268
+ selectedIndex={selectedIdx}
269
+ onSelect={handleSessionSelect}
270
+ onNavigate={handleSessionNavigate}
271
+ />
272
+ );
273
+ }
274
+
275
+ if (view === "provider") {
276
+ return (
277
+ <Box flexDirection="column" paddingX={2} paddingY={1}>
278
+ <Text color="cyan" bold>⚙️ Select Provider</Text>
279
+ <Text dimColor>━━━━━━━━━━━━━━━━━━━━━━━━━━━━</Text>
280
+ <Box flexDirection="column" marginY={1}>
281
+ {providers.map((p, i) => (
282
+ <Text key={p} color={i === selectedIdx ? "green" : "white"} bold={i === selectedIdx}>
283
+ {i === selectedIdx ? "▸ " : " "}{p} {p === config.provider ? <Text dimColor>(current)</Text> : ""}
284
+ </Text>
285
+ ))}
286
+ </Box>
287
+ <Text dimColor>━━━━━━━━━━━━━━━━━━━━━━━━━━━━</Text>
288
+ <Text dimColor><Text color="gray">↑↓</Text> Navigate <Text color="gray">Enter</Text> Select <Text color="gray">q</Text> Back</Text>
289
+ </Box>
290
+ );
291
+ }
292
+
293
+ if (view === "model") {
294
+ return (
295
+ <Box flexDirection="column" paddingX={2} paddingY={1}>
296
+ <Text color="cyan" bold>🤖 Select Model <Text dimColor>({config.provider})</Text></Text>
297
+ <Text dimColor>━━━━━━━━━━━━━━━━━━━━━━━━━━━━</Text>
298
+ <Box flexDirection="column" marginY={1}>
299
+ {currentModels.map((m, i) => (
300
+ <Text key={m} color={i === selectedIdx ? "green" : "white"} bold={i === selectedIdx}>
301
+ {i === selectedIdx ? "▸ " : " "}{m} {m === config.model ? <Text dimColor>(current)</Text> : ""}
302
+ </Text>
303
+ ))}
304
+ </Box>
305
+ <Text dimColor>━━━━━━━━━━━━━━━━━━━━━━━━━━━━</Text>
306
+ <Text dimColor><Text color="gray">↑↓</Text> Navigate <Text color="gray">Enter</Text> Select <Text color="gray">q</Text> Back</Text>
307
+ </Box>
308
+ );
309
+ }
310
+
311
+ if (view === "agent") {
312
+ return (
313
+ <Box flexDirection="column" paddingX={2} paddingY={1}>
314
+ <Text color="cyan" bold>🕵️ Select Agent</Text>
315
+ <Text dimColor>━━━━━━━━━━━━━━━━━━━━━━━━━━━━</Text>
316
+ <Box flexDirection="column" marginY={1}>
317
+ {agentTypes.map((t, i) => (
318
+ <Text key={t} color={i === selectedIdx ? "green" : "white"} bold={i === selectedIdx}>
319
+ {i === selectedIdx ? "▸ " : " "}{t === "tool-loop" ? "Tool Loop (Standard)" : "Ralph Loop (Continuous)"} {t === agentType ? <Text dimColor>(current)</Text> : ""}
320
+ </Text>
321
+ ))}
322
+ </Box>
323
+ <Text dimColor>━━━━━━━━━━━━━━━━━━━━━━━━━━━━</Text>
324
+ <Text dimColor><Text color="gray">↑↓</Text> Navigate <Text color="gray">Enter</Text> Select <Text color="gray">q</Text> Back</Text>
325
+ </Box>
326
+ );
327
+ }
328
+
329
+ if (view === "history") {
330
+ const allSessions = [...currentDirSessions, ...otherDirSessions];
331
+ return (
332
+ <Box flexDirection="column" paddingX={2} paddingY={1}>
333
+ <Text color="cyan" bold>📚 Session History</Text>
334
+ <Text dimColor>━━━━━━━━━━━━━━━━━━━━━━━━━━━━</Text>
335
+
336
+ <Box flexDirection="column" marginY={1}>
337
+ <Text bold color="yellow">📂 Current Directory</Text>
338
+ {currentDirSessions.length === 0 ? (
339
+ <Text dimColor> No sessions</Text>
340
+ ) : (
341
+ currentDirSessions.map((s, i) => (
342
+ <Text key={s.id} color={i === selectedIdx ? "green" : "white"} bold={i === selectedIdx}>
343
+ {i === selectedIdx ? "▸ " : " "}💬 {s.name || "Session"} <Text dimColor>({s.message_count} msgs)</Text>
344
+ </Text>
345
+ ))
346
+ )}
347
+ </Box>
348
+
349
+ {otherDirSessions.length > 0 && (
350
+ <Box flexDirection="column" marginY={1}>
351
+ <Text bold color="blue">📁 Other Directories</Text>
352
+ {otherDirSessions.map((s, i) => {
353
+ const idx = currentDirSessions.length + i;
354
+ return (
355
+ <Text key={s.id} color={idx === selectedIdx ? "green" : "white"} bold={idx === selectedIdx}>
356
+ {idx === selectedIdx ? "▸ " : " "}📍 {s.path} <Text dimColor>({s.message_count} msgs)</Text>
357
+ </Text>
358
+ );
359
+ })}
360
+ </Box>
361
+ )}
362
+
363
+ <Text dimColor>━━━━━━━━━━━━━━━━━━━━━━━━━━━━</Text>
364
+ <Text dimColor><Text color="gray">↑↓</Text> Navigate <Text color="gray">Enter</Text> Load <Text color="gray">q</Text> Back</Text>
365
+ </Box>
366
+ );
367
+ }
368
+
369
+ return (
370
+ <Box flexDirection="column">
371
+ <Header provider={config.provider} model={config.model} />
372
+
373
+ <Static items={messages}>
374
+ {(msg) => (
375
+ <Box key={msg.id} paddingX={1}>
376
+ <MessageComponent
377
+ role={msg.role}
378
+ content={msg.content}
379
+ />
380
+ </Box>
381
+ )}
382
+ </Static>
383
+
384
+ {isLoading && (
385
+ <Box paddingX={1}>
386
+ <MessageComponent
387
+ role="assistant"
388
+ content={streamingContent}
389
+ thinking={thinking || undefined}
390
+ />
391
+ </Box>
392
+ )}
393
+
394
+ <Box flexDirection="column" paddingX={1}>
395
+ {error && <Text color="red">❌ Error: {error}</Text>}
396
+ <InputArea onSubmit={handleInput} isLoading={isLoading} />
397
+ </Box>
398
+ </Box>
399
+ );
400
+ };
package/src/index.tsx ADDED
@@ -0,0 +1,27 @@
1
+ import React from "react";
2
+ import { render } from "ink";
3
+ import { App } from "./app";
4
+ import dotenv from "dotenv";
5
+
6
+ dotenv.config();
7
+
8
+ let skipInitialMessages = false;
9
+
10
+ export const triggerRerender = () => {
11
+ skipInitialMessages = true;
12
+ console.clear();
13
+ rerenderFn?.(<App skipInitialLoad={true} />);
14
+ };
15
+
16
+ export const shouldSkipInitialMessages = () => {
17
+ const skip = skipInitialMessages;
18
+ skipInitialMessages = false;
19
+ return skip;
20
+ };
21
+
22
+ let rerenderFn: ((node: React.ReactNode) => void) | null = null;
23
+
24
+ const { rerender, waitUntilExit } = render(<App skipInitialLoad={false} />);
25
+ rerenderFn = rerender;
26
+
27
+ waitUntilExit();
@@ -0,0 +1,108 @@
1
+ import { ToolLoopAgent, stepCountIs } from "ai";
2
+ import { saveMessage } from "./db.js";
3
+ import { createMCPClient } from "@ai-sdk/mcp";
4
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
5
+ import { shellTool } from "../tools/shell.js";
6
+ import { getModel } from "./provider.js";
7
+ import { loadConfig } from "./config.js";
8
+ import { systemprompt } from "./prompt.js";
9
+ import { RalphLoopAgent, iterationCountIs } from 'ralph-loop-agent';
10
+
11
+ export type AgentMessage = {
12
+ role: "user" | "assistant";
13
+ content: string;
14
+ };
15
+
16
+ export type StreamEvent =
17
+ | { type: "text"; content: string }
18
+ | { type: "thinking"; content: string };
19
+
20
+ export async function* runAgentStream(prompt: string, history: AgentMessage[], agentType: "tool-loop" | "ralph-loop" = "tool-loop"): AsyncGenerator<StreamEvent> {
21
+ let fsClient: any = null;
22
+ let searchClient: any = null;
23
+
24
+ try {
25
+ const config = loadConfig();
26
+ const model = getModel(config.provider, config.model);
27
+
28
+ fsClient = await createMCPClient({
29
+ transport: new StdioClientTransport({
30
+ command: "npx",
31
+ args: ["-y", "@modelcontextprotocol/server-filesystem", process.cwd()],
32
+ }),
33
+ });
34
+
35
+ searchClient = await createMCPClient({
36
+ transport: new StdioClientTransport({
37
+ command: "uvx",
38
+ args: ["duckduckgo-mcp-server"],
39
+ }),
40
+ });
41
+
42
+ const fsTools = await fsClient.tools();
43
+ const searchTools = await searchClient.tools();
44
+
45
+ const tools = {
46
+ ...fsTools,
47
+ ...searchTools,
48
+ shell: shellTool,
49
+ };
50
+
51
+ const messages: Array<{ role: "user" | "assistant"; content: string }> = [
52
+ ...history.map((h) => ({ role: h.role, content: h.content })),
53
+ { role: "user" as const, content: prompt },
54
+ ];
55
+
56
+ let result;
57
+
58
+ if (agentType === "ralph-loop") {
59
+ const myAgent = new RalphLoopAgent({
60
+ model: model,
61
+ instructions: systemprompt,
62
+ tools: tools,
63
+ stopWhen: iterationCountIs(10),
64
+ verifyCompletion: async () => ({ complete: true }),
65
+ });
66
+ result = await myAgent.stream({ messages } as any);
67
+ } else {
68
+ const myAgent = new ToolLoopAgent({
69
+ model: model,
70
+ instructions: systemprompt,
71
+ tools: tools,
72
+ stopWhen: stepCountIs(100),
73
+ });
74
+ result = await myAgent.stream({ messages });
75
+ }
76
+
77
+ saveMessage("user", prompt);
78
+ let fullText = "";
79
+
80
+ for await (const part of result.fullStream) {
81
+ if (part.type === "text-delta") {
82
+ const content = part.text;
83
+ fullText += content;
84
+ yield { type: "text", content };
85
+ } else if (part.type === "tool-call") {
86
+ yield { type: "thinking", content: `Using tool: ${part.toolName}` };
87
+ }
88
+ }
89
+
90
+ saveMessage("assistant", fullText);
91
+ } catch (error: any) {
92
+ if (error.name === 'AbortError' ||
93
+ error.message?.includes('CancelledError') ||
94
+ error.message?.includes('KeyboardInterrupt') ||
95
+ error.code === 'ABORT_ERR') {
96
+ return;
97
+ }
98
+
99
+ yield { type: "text", content: `Error: ${error.message}` };
100
+ } finally {
101
+ try {
102
+ if (fsClient) await fsClient.close?.();
103
+ } catch {}
104
+ try {
105
+ if (searchClient) await searchClient.close?.();
106
+ } catch {}
107
+ }
108
+ }