@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 +53 -0
- package/bin/axe.js +2 -0
- package/package.json +60 -0
- package/src/app.tsx +400 -0
- package/src/index.tsx +27 -0
- package/src/lib/agent.ts +108 -0
- package/src/lib/config.ts +76 -0
- package/src/lib/db.ts +135 -0
- package/src/lib/filesystem.ts +83 -0
- package/src/lib/prompt.ts +38 -0
- package/src/lib/provider.ts +71 -0
- package/src/tools/shell.ts +31 -0
- package/src/ui/autocomplete.tsx +22 -0
- package/src/ui/header.tsx +25 -0
- package/src/ui/input-area.tsx +121 -0
- package/src/ui/layout.tsx +28 -0
- package/src/ui/message.tsx +38 -0
- package/src/ui/session-picker.tsx +116 -0
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
|
+

|
|
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
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();
|
package/src/lib/agent.ts
ADDED
|
@@ -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
|
+
}
|