@projectservan8n/cnapse 0.2.0 → 0.4.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/dist/Setup-Q32JPHGP.js +174 -0
- package/dist/chunk-COKO6V5J.js +50 -0
- package/dist/index.js +1203 -97
- package/package.json +4 -2
- package/src/agents/coder.ts +62 -0
- package/src/agents/computer.ts +61 -0
- package/src/agents/executor.ts +179 -0
- package/src/agents/filer.ts +56 -0
- package/src/agents/index.ts +12 -0
- package/src/agents/router.ts +160 -0
- package/src/agents/shell.ts +67 -0
- package/src/agents/types.ts +80 -0
- package/src/components/App.tsx +225 -8
- package/src/components/Header.tsx +11 -1
- package/src/components/HelpMenu.tsx +143 -0
- package/src/components/Setup.tsx +203 -0
- package/src/components/TaskProgress.tsx +68 -0
- package/src/index.tsx +14 -3
- package/src/lib/api.ts +2 -2
- package/src/lib/config.ts +21 -0
- package/src/lib/screen.ts +118 -0
- package/src/lib/tasks.ts +268 -0
- package/src/lib/vision.ts +254 -0
- package/src/services/telegram.ts +278 -0
- package/src/tools/clipboard.ts +55 -0
- package/src/tools/computer.ts +454 -0
- package/src/tools/filesystem.ts +272 -0
- package/src/tools/index.ts +35 -0
- package/src/tools/network.ts +204 -0
- package/src/tools/process.ts +194 -0
- package/src/tools/shell.ts +140 -0
- package/src/tools/vision.ts +65 -0
- package/src/types/screenshot-desktop.d.ts +10 -0
package/src/components/App.tsx
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
import React, { useState, useEffect } from 'react';
|
|
1
|
+
import React, { useState, useEffect, useRef } from 'react';
|
|
2
2
|
import { Box, Text, useApp, useInput } from 'ink';
|
|
3
3
|
import { Header } from './Header.js';
|
|
4
4
|
import { ChatMessage } from './ChatMessage.js';
|
|
5
5
|
import { ChatInput } from './ChatInput.js';
|
|
6
6
|
import { StatusBar } from './StatusBar.js';
|
|
7
|
+
import { HelpMenu } from './HelpMenu.js';
|
|
7
8
|
import { chat, Message } from '../lib/api.js';
|
|
9
|
+
import { getScreenDescription } from '../lib/screen.js';
|
|
10
|
+
import { describeScreen } from '../lib/vision.js';
|
|
11
|
+
import { getTelegramBot, TelegramMessage } from '../services/telegram.js';
|
|
12
|
+
import { parseTask, executeTask, formatTask, Task } from '../lib/tasks.js';
|
|
8
13
|
|
|
9
14
|
interface ChatMsg {
|
|
10
15
|
id: string;
|
|
@@ -20,7 +25,7 @@ export function App() {
|
|
|
20
25
|
{
|
|
21
26
|
id: '0',
|
|
22
27
|
role: 'system',
|
|
23
|
-
content: 'Welcome to C-napse! Type your message and press Enter.\n\nShortcuts:\n Ctrl+C - Exit\n /clear - Clear chat\n /help - Show help',
|
|
28
|
+
content: 'Welcome to C-napse! Type your message and press Enter.\n\nShortcuts:\n Ctrl+C - Exit\n Ctrl+W - Toggle screen watch\n /clear - Clear chat\n /help - Show help',
|
|
24
29
|
timestamp: new Date(),
|
|
25
30
|
},
|
|
26
31
|
]);
|
|
@@ -28,8 +33,37 @@ export function App() {
|
|
|
28
33
|
const [isProcessing, setIsProcessing] = useState(false);
|
|
29
34
|
const [status, setStatus] = useState('Ready');
|
|
30
35
|
const [error, setError] = useState<string | null>(null);
|
|
36
|
+
const [screenWatch, setScreenWatch] = useState(false);
|
|
37
|
+
const [showHelpMenu, setShowHelpMenu] = useState(false);
|
|
38
|
+
const [telegramEnabled, setTelegramEnabled] = useState(false);
|
|
39
|
+
const screenContextRef = useRef<string | null>(null);
|
|
40
|
+
|
|
41
|
+
// Screen watching effect
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (!screenWatch) {
|
|
44
|
+
screenContextRef.current = null;
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const checkScreen = async () => {
|
|
49
|
+
const desc = await getScreenDescription();
|
|
50
|
+
if (desc) {
|
|
51
|
+
screenContextRef.current = desc;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
checkScreen();
|
|
56
|
+
const interval = setInterval(checkScreen, 5000); // Check every 5 seconds
|
|
57
|
+
|
|
58
|
+
return () => clearInterval(interval);
|
|
59
|
+
}, [screenWatch]);
|
|
31
60
|
|
|
32
61
|
useInput((inputChar, key) => {
|
|
62
|
+
// If help menu is open, don't process other shortcuts
|
|
63
|
+
if (showHelpMenu) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
33
67
|
if (key.ctrl && inputChar === 'c') {
|
|
34
68
|
exit();
|
|
35
69
|
}
|
|
@@ -37,6 +71,31 @@ export function App() {
|
|
|
37
71
|
setMessages([messages[0]!]); // Keep welcome message
|
|
38
72
|
setError(null);
|
|
39
73
|
}
|
|
74
|
+
if (key.ctrl && inputChar === 'w') {
|
|
75
|
+
setScreenWatch((prev) => {
|
|
76
|
+
const newState = !prev;
|
|
77
|
+
addSystemMessage(
|
|
78
|
+
newState
|
|
79
|
+
? '🖥️ Screen watching enabled. AI will have context of your screen.'
|
|
80
|
+
: '🖥️ Screen watching disabled.'
|
|
81
|
+
);
|
|
82
|
+
return newState;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
if (key.ctrl && inputChar === 'h') {
|
|
86
|
+
setShowHelpMenu(true);
|
|
87
|
+
}
|
|
88
|
+
if (key.ctrl && inputChar === 't') {
|
|
89
|
+
setTelegramEnabled((prev) => {
|
|
90
|
+
const newState = !prev;
|
|
91
|
+
addSystemMessage(
|
|
92
|
+
newState
|
|
93
|
+
? '📱 Telegram bot enabled.'
|
|
94
|
+
: '📱 Telegram bot disabled.'
|
|
95
|
+
);
|
|
96
|
+
return newState;
|
|
97
|
+
});
|
|
98
|
+
}
|
|
40
99
|
});
|
|
41
100
|
|
|
42
101
|
const handleSubmit = async (value: string) => {
|
|
@@ -84,7 +143,13 @@ export function App() {
|
|
|
84
143
|
.slice(-10)
|
|
85
144
|
.map((m) => ({ role: m.role as 'user' | 'assistant', content: m.content }));
|
|
86
145
|
|
|
87
|
-
|
|
146
|
+
// Add screen context if watching
|
|
147
|
+
let finalInput = userInput;
|
|
148
|
+
if (screenWatch && screenContextRef.current) {
|
|
149
|
+
finalInput = `[Screen context: ${screenContextRef.current}]\n\n${userInput}`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
apiMessages.push({ role: 'user', content: finalInput });
|
|
88
153
|
|
|
89
154
|
const response = await chat(apiMessages);
|
|
90
155
|
|
|
@@ -116,6 +181,7 @@ export function App() {
|
|
|
116
181
|
const handleCommand = (cmd: string) => {
|
|
117
182
|
const parts = cmd.slice(1).split(' ');
|
|
118
183
|
const command = parts[0];
|
|
184
|
+
const args = parts.slice(1).join(' ');
|
|
119
185
|
|
|
120
186
|
switch (command) {
|
|
121
187
|
case 'clear':
|
|
@@ -123,12 +189,151 @@ export function App() {
|
|
|
123
189
|
addSystemMessage('Chat cleared.');
|
|
124
190
|
break;
|
|
125
191
|
case 'help':
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
192
|
+
setShowHelpMenu(true);
|
|
193
|
+
break;
|
|
194
|
+
case 'watch':
|
|
195
|
+
setScreenWatch((prev) => {
|
|
196
|
+
const newState = !prev;
|
|
197
|
+
addSystemMessage(
|
|
198
|
+
newState
|
|
199
|
+
? '🖥️ Screen watching enabled.'
|
|
200
|
+
: '🖥️ Screen watching disabled.'
|
|
201
|
+
);
|
|
202
|
+
return newState;
|
|
203
|
+
});
|
|
204
|
+
break;
|
|
205
|
+
case 'telegram':
|
|
206
|
+
handleTelegramToggle();
|
|
207
|
+
break;
|
|
208
|
+
case 'screen':
|
|
209
|
+
handleScreenCommand();
|
|
210
|
+
break;
|
|
211
|
+
case 'task':
|
|
212
|
+
if (args) {
|
|
213
|
+
handleTaskCommand(args);
|
|
214
|
+
} else {
|
|
215
|
+
addSystemMessage('Usage: /task <description>\nExample: /task open notepad and type hello');
|
|
216
|
+
}
|
|
217
|
+
break;
|
|
218
|
+
case 'config':
|
|
219
|
+
addSystemMessage('⚙️ Configuration:\n Provider: Use cnapse config\n Model: Use cnapse config set model <name>');
|
|
220
|
+
break;
|
|
221
|
+
case 'model':
|
|
222
|
+
addSystemMessage('🤖 Model selection coming soon.\nUse: cnapse config set model <name>');
|
|
223
|
+
break;
|
|
224
|
+
case 'provider':
|
|
225
|
+
addSystemMessage('🔌 Provider selection coming soon.\nUse: cnapse config set provider <name>');
|
|
226
|
+
break;
|
|
227
|
+
case 'quit':
|
|
228
|
+
case 'exit':
|
|
229
|
+
exit();
|
|
129
230
|
break;
|
|
130
231
|
default:
|
|
131
|
-
addSystemMessage(`Unknown command: ${command}
|
|
232
|
+
addSystemMessage(`Unknown command: ${command}\nType /help to see available commands.`);
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const handleHelpMenuSelect = (command: string) => {
|
|
237
|
+
// Execute the selected command
|
|
238
|
+
handleCommand(command);
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const handleScreenCommand = async () => {
|
|
242
|
+
addSystemMessage('📸 Taking screenshot and analyzing...');
|
|
243
|
+
setStatus('Analyzing screen...');
|
|
244
|
+
setIsProcessing(true);
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
const result = await describeScreen();
|
|
248
|
+
addSystemMessage(`🖥️ Screen Analysis:\n\n${result.description}`);
|
|
249
|
+
} catch (err) {
|
|
250
|
+
const errorMsg = err instanceof Error ? err.message : 'Vision analysis failed';
|
|
251
|
+
addSystemMessage(`❌ Screen capture failed: ${errorMsg}`);
|
|
252
|
+
} finally {
|
|
253
|
+
setIsProcessing(false);
|
|
254
|
+
setStatus('Ready');
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const handleTaskCommand = async (taskDescription: string) => {
|
|
259
|
+
addSystemMessage(`📋 Parsing task: ${taskDescription}`);
|
|
260
|
+
setStatus('Parsing task...');
|
|
261
|
+
setIsProcessing(true);
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
// Parse the task into steps
|
|
265
|
+
const task = await parseTask(taskDescription);
|
|
266
|
+
addSystemMessage(`📋 Task planned (${task.steps.length} steps):\n${formatTask(task)}`);
|
|
267
|
+
|
|
268
|
+
// Execute the task
|
|
269
|
+
addSystemMessage('🚀 Executing task...');
|
|
270
|
+
setStatus('Executing task...');
|
|
271
|
+
|
|
272
|
+
await executeTask(task, (updatedTask, currentStep) => {
|
|
273
|
+
// Update progress
|
|
274
|
+
if (currentStep.status === 'running') {
|
|
275
|
+
setStatus(`Running: ${currentStep.description}`);
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// Show final result
|
|
280
|
+
addSystemMessage(`\n${formatTask(task)}`);
|
|
281
|
+
|
|
282
|
+
if (task.status === 'completed') {
|
|
283
|
+
addSystemMessage('✅ Task completed successfully!');
|
|
284
|
+
} else {
|
|
285
|
+
addSystemMessage('❌ Task failed. Check the steps above for errors.');
|
|
286
|
+
}
|
|
287
|
+
} catch (err) {
|
|
288
|
+
const errorMsg = err instanceof Error ? err.message : 'Task failed';
|
|
289
|
+
addSystemMessage(`❌ Task error: ${errorMsg}`);
|
|
290
|
+
} finally {
|
|
291
|
+
setIsProcessing(false);
|
|
292
|
+
setStatus('Ready');
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const handleTelegramToggle = async () => {
|
|
297
|
+
const bot = getTelegramBot();
|
|
298
|
+
|
|
299
|
+
if (telegramEnabled) {
|
|
300
|
+
// Stop the bot
|
|
301
|
+
try {
|
|
302
|
+
await bot.stop();
|
|
303
|
+
setTelegramEnabled(false);
|
|
304
|
+
addSystemMessage('📱 Telegram bot stopped.');
|
|
305
|
+
} catch (err) {
|
|
306
|
+
const errorMsg = err instanceof Error ? err.message : 'Failed to stop bot';
|
|
307
|
+
addSystemMessage(`❌ Error stopping bot: ${errorMsg}`);
|
|
308
|
+
}
|
|
309
|
+
} else {
|
|
310
|
+
// Start the bot
|
|
311
|
+
addSystemMessage('📱 Starting Telegram bot...');
|
|
312
|
+
setStatus('Starting Telegram...');
|
|
313
|
+
|
|
314
|
+
try {
|
|
315
|
+
// Setup event handlers
|
|
316
|
+
bot.on('message', (msg: TelegramMessage) => {
|
|
317
|
+
addSystemMessage(`📱 Telegram [${msg.from}]: ${msg.text}`);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
bot.on('error', (error: Error) => {
|
|
321
|
+
addSystemMessage(`📱 Telegram error: ${error.message}`);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
await bot.start();
|
|
325
|
+
setTelegramEnabled(true);
|
|
326
|
+
addSystemMessage(
|
|
327
|
+
'📱 Telegram bot started!\n\n' +
|
|
328
|
+
'Open Telegram and send /start to your bot to connect.\n' +
|
|
329
|
+
'Commands: /screen, /describe, /run <cmd>, /status'
|
|
330
|
+
);
|
|
331
|
+
} catch (err) {
|
|
332
|
+
const errorMsg = err instanceof Error ? err.message : 'Failed to start bot';
|
|
333
|
+
addSystemMessage(`❌ Telegram error: ${errorMsg}`);
|
|
334
|
+
} finally {
|
|
335
|
+
setStatus('Ready');
|
|
336
|
+
}
|
|
132
337
|
}
|
|
133
338
|
};
|
|
134
339
|
|
|
@@ -147,9 +352,21 @@ export function App() {
|
|
|
147
352
|
// Only show last N messages that fit
|
|
148
353
|
const visibleMessages = messages.slice(-20);
|
|
149
354
|
|
|
355
|
+
// If help menu is open, show it as overlay
|
|
356
|
+
if (showHelpMenu) {
|
|
357
|
+
return (
|
|
358
|
+
<Box flexDirection="column" height="100%" alignItems="center" justifyContent="center">
|
|
359
|
+
<HelpMenu
|
|
360
|
+
onClose={() => setShowHelpMenu(false)}
|
|
361
|
+
onSelect={handleHelpMenuSelect}
|
|
362
|
+
/>
|
|
363
|
+
</Box>
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
150
367
|
return (
|
|
151
368
|
<Box flexDirection="column" height="100%">
|
|
152
|
-
<Header />
|
|
369
|
+
<Header screenWatch={screenWatch} telegramEnabled={telegramEnabled} />
|
|
153
370
|
|
|
154
371
|
<Box flexDirection="column" flexGrow={1} borderStyle="round" borderColor="gray" padding={1}>
|
|
155
372
|
<Text bold color="gray"> Chat </Text>
|
|
@@ -11,7 +11,12 @@ const ASCII_BANNER = `
|
|
|
11
11
|
╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═╝╚═╝ ╚══════╝╚══════╝
|
|
12
12
|
`.trim();
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
interface HeaderProps {
|
|
15
|
+
screenWatch?: boolean;
|
|
16
|
+
telegramEnabled?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function Header({ screenWatch = false, telegramEnabled = false }: HeaderProps) {
|
|
15
20
|
const config = getConfig();
|
|
16
21
|
|
|
17
22
|
return (
|
|
@@ -20,8 +25,13 @@ export function Header() {
|
|
|
20
25
|
<Box justifyContent="center">
|
|
21
26
|
<Text color="gray">
|
|
22
27
|
{config.provider} │ {config.model}
|
|
28
|
+
{screenWatch && <Text color="yellow"> │ 🖥️ Screen</Text>}
|
|
29
|
+
{telegramEnabled && <Text color="blue"> │ 📱 Telegram</Text>}
|
|
23
30
|
</Text>
|
|
24
31
|
</Box>
|
|
32
|
+
<Box justifyContent="center">
|
|
33
|
+
<Text color="gray" dimColor>Press Ctrl+H for help</Text>
|
|
34
|
+
</Box>
|
|
25
35
|
</Box>
|
|
26
36
|
);
|
|
27
37
|
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
|
|
4
|
+
interface MenuItem {
|
|
5
|
+
command: string;
|
|
6
|
+
shortcut?: string;
|
|
7
|
+
description: string;
|
|
8
|
+
category: 'navigation' | 'actions' | 'settings';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const MENU_ITEMS: MenuItem[] = [
|
|
12
|
+
// Navigation
|
|
13
|
+
{ command: '/help', shortcut: 'Ctrl+H', description: 'Show this help menu', category: 'navigation' },
|
|
14
|
+
{ command: '/clear', shortcut: 'Ctrl+L', description: 'Clear chat history', category: 'navigation' },
|
|
15
|
+
{ command: '/quit', shortcut: 'Ctrl+C', description: 'Exit C-napse', category: 'navigation' },
|
|
16
|
+
|
|
17
|
+
// Actions
|
|
18
|
+
{ command: '/screen', shortcut: 'Ctrl+S', description: 'Take screenshot and describe', category: 'actions' },
|
|
19
|
+
{ command: '/task', description: 'Run multi-step task', category: 'actions' },
|
|
20
|
+
{ command: '/telegram', shortcut: 'Ctrl+T', description: 'Toggle Telegram bot', category: 'actions' },
|
|
21
|
+
|
|
22
|
+
// Settings
|
|
23
|
+
{ command: '/config', description: 'Show/edit configuration', category: 'settings' },
|
|
24
|
+
{ command: '/watch', shortcut: 'Ctrl+W', description: 'Toggle screen watching', category: 'settings' },
|
|
25
|
+
{ command: '/model', description: 'Change AI model', category: 'settings' },
|
|
26
|
+
{ command: '/provider', description: 'Change AI provider', category: 'settings' },
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
interface HelpMenuProps {
|
|
30
|
+
onClose: () => void;
|
|
31
|
+
onSelect: (command: string) => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function HelpMenu({ onClose, onSelect }: HelpMenuProps) {
|
|
35
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
36
|
+
const [selectedCategory, setSelectedCategory] = useState<'all' | MenuItem['category']>('all');
|
|
37
|
+
|
|
38
|
+
const filteredItems = selectedCategory === 'all'
|
|
39
|
+
? MENU_ITEMS
|
|
40
|
+
: MENU_ITEMS.filter(item => item.category === selectedCategory);
|
|
41
|
+
|
|
42
|
+
useInput((input, key) => {
|
|
43
|
+
if (key.escape) {
|
|
44
|
+
onClose();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (key.upArrow) {
|
|
49
|
+
setSelectedIndex(prev => (prev > 0 ? prev - 1 : filteredItems.length - 1));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (key.downArrow) {
|
|
53
|
+
setSelectedIndex(prev => (prev < filteredItems.length - 1 ? prev + 1 : 0));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (key.leftArrow || key.rightArrow) {
|
|
57
|
+
const categories: Array<'all' | MenuItem['category']> = ['all', 'navigation', 'actions', 'settings'];
|
|
58
|
+
const currentIdx = categories.indexOf(selectedCategory);
|
|
59
|
+
if (key.leftArrow) {
|
|
60
|
+
setSelectedCategory(categories[currentIdx > 0 ? currentIdx - 1 : categories.length - 1]!);
|
|
61
|
+
} else {
|
|
62
|
+
setSelectedCategory(categories[currentIdx < categories.length - 1 ? currentIdx + 1 : 0]!);
|
|
63
|
+
}
|
|
64
|
+
setSelectedIndex(0);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (key.return) {
|
|
68
|
+
const item = filteredItems[selectedIndex];
|
|
69
|
+
if (item) {
|
|
70
|
+
onSelect(item.command);
|
|
71
|
+
onClose();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const categories: Array<{ key: 'all' | MenuItem['category']; label: string }> = [
|
|
77
|
+
{ key: 'all', label: 'All' },
|
|
78
|
+
{ key: 'navigation', label: 'Navigation' },
|
|
79
|
+
{ key: 'actions', label: 'Actions' },
|
|
80
|
+
{ key: 'settings', label: 'Settings' },
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<Box
|
|
85
|
+
flexDirection="column"
|
|
86
|
+
borderStyle="round"
|
|
87
|
+
borderColor="cyan"
|
|
88
|
+
padding={1}
|
|
89
|
+
width={60}
|
|
90
|
+
>
|
|
91
|
+
{/* Header */}
|
|
92
|
+
<Box justifyContent="center" marginBottom={1}>
|
|
93
|
+
<Text bold color="cyan">C-napse Help</Text>
|
|
94
|
+
</Box>
|
|
95
|
+
|
|
96
|
+
{/* Category Tabs */}
|
|
97
|
+
<Box justifyContent="center" marginBottom={1}>
|
|
98
|
+
{categories.map((cat, idx) => (
|
|
99
|
+
<React.Fragment key={cat.key}>
|
|
100
|
+
<Text
|
|
101
|
+
color={selectedCategory === cat.key ? 'cyan' : 'gray'}
|
|
102
|
+
bold={selectedCategory === cat.key}
|
|
103
|
+
>
|
|
104
|
+
{cat.label}
|
|
105
|
+
</Text>
|
|
106
|
+
{idx < categories.length - 1 && <Text color="gray"> │ </Text>}
|
|
107
|
+
</React.Fragment>
|
|
108
|
+
))}
|
|
109
|
+
</Box>
|
|
110
|
+
|
|
111
|
+
<Box marginBottom={1}>
|
|
112
|
+
<Text color="gray" dimColor>Use ←→ to switch tabs, ↑↓ to navigate, Enter to select</Text>
|
|
113
|
+
</Box>
|
|
114
|
+
|
|
115
|
+
{/* Menu Items */}
|
|
116
|
+
<Box flexDirection="column">
|
|
117
|
+
{filteredItems.map((item, index) => (
|
|
118
|
+
<Box key={item.command}>
|
|
119
|
+
<Text color={index === selectedIndex ? 'cyan' : 'white'}>
|
|
120
|
+
{index === selectedIndex ? '❯ ' : ' '}
|
|
121
|
+
</Text>
|
|
122
|
+
<Box width={12}>
|
|
123
|
+
<Text bold={index === selectedIndex} color={index === selectedIndex ? 'cyan' : 'white'}>
|
|
124
|
+
{item.command}
|
|
125
|
+
</Text>
|
|
126
|
+
</Box>
|
|
127
|
+
{item.shortcut && (
|
|
128
|
+
<Box width={10}>
|
|
129
|
+
<Text color="yellow" dimColor>{item.shortcut}</Text>
|
|
130
|
+
</Box>
|
|
131
|
+
)}
|
|
132
|
+
<Text color="gray">{item.description}</Text>
|
|
133
|
+
</Box>
|
|
134
|
+
))}
|
|
135
|
+
</Box>
|
|
136
|
+
|
|
137
|
+
{/* Footer */}
|
|
138
|
+
<Box marginTop={1} justifyContent="center">
|
|
139
|
+
<Text color="gray" dimColor>Press Esc to close</Text>
|
|
140
|
+
</Box>
|
|
141
|
+
</Box>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Box, Text, useInput, useApp } from 'ink';
|
|
3
|
+
import TextInput from 'ink-text-input';
|
|
4
|
+
import { setApiKey, setProvider, setModel, getConfig } from '../lib/config.js';
|
|
5
|
+
|
|
6
|
+
type Step = 'provider' | 'apiKey' | 'model' | 'done';
|
|
7
|
+
|
|
8
|
+
const PROVIDERS = [
|
|
9
|
+
{ id: 'ollama', name: 'Ollama', desc: 'Local AI (free, no API key needed)' },
|
|
10
|
+
{ id: 'openrouter', name: 'OpenRouter', desc: 'Many models, pay per use' },
|
|
11
|
+
{ id: 'anthropic', name: 'Anthropic', desc: 'Claude models' },
|
|
12
|
+
{ id: 'openai', name: 'OpenAI', desc: 'GPT models' },
|
|
13
|
+
] as const;
|
|
14
|
+
|
|
15
|
+
const DEFAULT_MODELS: Record<string, string[]> = {
|
|
16
|
+
ollama: ['qwen2.5:0.5b', 'qwen2.5:7b', 'llama3.2:3b', 'codellama:7b'],
|
|
17
|
+
openrouter: [
|
|
18
|
+
'qwen/qwen-2.5-coder-32b-instruct',
|
|
19
|
+
'anthropic/claude-3.5-sonnet',
|
|
20
|
+
'openai/gpt-4o',
|
|
21
|
+
'google/gemini-pro-1.5',
|
|
22
|
+
],
|
|
23
|
+
anthropic: [
|
|
24
|
+
'claude-3-5-sonnet-20241022',
|
|
25
|
+
'claude-3-opus-20240229',
|
|
26
|
+
'claude-3-haiku-20240307',
|
|
27
|
+
],
|
|
28
|
+
openai: ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'gpt-3.5-turbo'],
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export function Setup() {
|
|
32
|
+
const { exit } = useApp();
|
|
33
|
+
const [step, setStep] = useState<Step>('provider');
|
|
34
|
+
const [selectedProvider, setSelectedProvider] = useState(0);
|
|
35
|
+
const [selectedModel, setSelectedModel] = useState(0);
|
|
36
|
+
const [apiKey, setApiKeyInput] = useState('');
|
|
37
|
+
const [provider, setProviderState] = useState<string>('ollama');
|
|
38
|
+
|
|
39
|
+
useInput((input, key) => {
|
|
40
|
+
if (key.escape) {
|
|
41
|
+
exit();
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (step === 'provider') {
|
|
46
|
+
if (key.upArrow) {
|
|
47
|
+
setSelectedProvider((prev) => (prev > 0 ? prev - 1 : PROVIDERS.length - 1));
|
|
48
|
+
} else if (key.downArrow) {
|
|
49
|
+
setSelectedProvider((prev) => (prev < PROVIDERS.length - 1 ? prev + 1 : 0));
|
|
50
|
+
} else if (key.return) {
|
|
51
|
+
const selected = PROVIDERS[selectedProvider]!;
|
|
52
|
+
setProviderState(selected.id);
|
|
53
|
+
setProvider(selected.id as any);
|
|
54
|
+
|
|
55
|
+
if (selected.id === 'ollama') {
|
|
56
|
+
setStep('model');
|
|
57
|
+
} else {
|
|
58
|
+
setStep('apiKey');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
} else if (step === 'model') {
|
|
62
|
+
const models = DEFAULT_MODELS[provider] || [];
|
|
63
|
+
if (key.upArrow) {
|
|
64
|
+
setSelectedModel((prev) => (prev > 0 ? prev - 1 : models.length - 1));
|
|
65
|
+
} else if (key.downArrow) {
|
|
66
|
+
setSelectedModel((prev) => (prev < models.length - 1 ? prev + 1 : 0));
|
|
67
|
+
} else if (key.return) {
|
|
68
|
+
const model = models[selectedModel]!;
|
|
69
|
+
setModel(model);
|
|
70
|
+
setStep('done');
|
|
71
|
+
setTimeout(() => exit(), 1500);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const handleApiKeySubmit = (value: string) => {
|
|
77
|
+
if (value.trim()) {
|
|
78
|
+
setApiKey(provider as any, value.trim());
|
|
79
|
+
setApiKeyInput(value);
|
|
80
|
+
setStep('model');
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<Box flexDirection="column" padding={1}>
|
|
86
|
+
<Box marginBottom={1}>
|
|
87
|
+
<Text bold color="cyan">
|
|
88
|
+
🚀 C-napse Setup
|
|
89
|
+
</Text>
|
|
90
|
+
</Box>
|
|
91
|
+
|
|
92
|
+
{step === 'provider' && (
|
|
93
|
+
<Box flexDirection="column">
|
|
94
|
+
<Text bold>Select AI Provider:</Text>
|
|
95
|
+
<Text color="gray" dimColor>
|
|
96
|
+
(Use ↑↓ arrows, Enter to select)
|
|
97
|
+
</Text>
|
|
98
|
+
<Box marginTop={1} flexDirection="column">
|
|
99
|
+
{PROVIDERS.map((p, i) => (
|
|
100
|
+
<Box key={p.id}>
|
|
101
|
+
<Text color={i === selectedProvider ? 'cyan' : 'white'}>
|
|
102
|
+
{i === selectedProvider ? '❯ ' : ' '}
|
|
103
|
+
<Text bold={i === selectedProvider}>{p.name}</Text>
|
|
104
|
+
<Text color="gray"> - {p.desc}</Text>
|
|
105
|
+
</Text>
|
|
106
|
+
</Box>
|
|
107
|
+
))}
|
|
108
|
+
</Box>
|
|
109
|
+
</Box>
|
|
110
|
+
)}
|
|
111
|
+
|
|
112
|
+
{step === 'apiKey' && (
|
|
113
|
+
<Box flexDirection="column">
|
|
114
|
+
<Text>
|
|
115
|
+
<Text color="green">✓</Text> Provider: <Text bold>{provider}</Text>
|
|
116
|
+
</Text>
|
|
117
|
+
<Box marginTop={1} flexDirection="column">
|
|
118
|
+
<Text bold>Enter your {provider} API key:</Text>
|
|
119
|
+
<Text color="gray" dimColor>
|
|
120
|
+
(Paste with Ctrl+V or right-click, then Enter)
|
|
121
|
+
</Text>
|
|
122
|
+
<Box marginTop={1}>
|
|
123
|
+
<Text color="cyan">❯ </Text>
|
|
124
|
+
<TextInput
|
|
125
|
+
value={apiKey}
|
|
126
|
+
onChange={setApiKeyInput}
|
|
127
|
+
onSubmit={handleApiKeySubmit}
|
|
128
|
+
mask="*"
|
|
129
|
+
/>
|
|
130
|
+
</Box>
|
|
131
|
+
</Box>
|
|
132
|
+
</Box>
|
|
133
|
+
)}
|
|
134
|
+
|
|
135
|
+
{step === 'model' && (
|
|
136
|
+
<Box flexDirection="column">
|
|
137
|
+
<Text>
|
|
138
|
+
<Text color="green">✓</Text> Provider: <Text bold>{provider}</Text>
|
|
139
|
+
</Text>
|
|
140
|
+
{provider !== 'ollama' && (
|
|
141
|
+
<Text>
|
|
142
|
+
<Text color="green">✓</Text> API Key: <Text bold>saved</Text>
|
|
143
|
+
</Text>
|
|
144
|
+
)}
|
|
145
|
+
<Box marginTop={1} flexDirection="column">
|
|
146
|
+
<Text bold>Select Model:</Text>
|
|
147
|
+
<Text color="gray" dimColor>
|
|
148
|
+
(Use ↑↓ arrows, Enter to select)
|
|
149
|
+
</Text>
|
|
150
|
+
<Box marginTop={1} flexDirection="column">
|
|
151
|
+
{(DEFAULT_MODELS[provider] || []).map((model, i) => (
|
|
152
|
+
<Box key={model}>
|
|
153
|
+
<Text color={i === selectedModel ? 'cyan' : 'white'}>
|
|
154
|
+
{i === selectedModel ? '❯ ' : ' '}
|
|
155
|
+
<Text bold={i === selectedModel}>{model}</Text>
|
|
156
|
+
</Text>
|
|
157
|
+
</Box>
|
|
158
|
+
))}
|
|
159
|
+
</Box>
|
|
160
|
+
</Box>
|
|
161
|
+
</Box>
|
|
162
|
+
)}
|
|
163
|
+
|
|
164
|
+
{step === 'done' && (
|
|
165
|
+
<Box flexDirection="column">
|
|
166
|
+
<Text>
|
|
167
|
+
<Text color="green">✓</Text> Provider: <Text bold>{provider}</Text>
|
|
168
|
+
</Text>
|
|
169
|
+
{provider !== 'ollama' && (
|
|
170
|
+
<Text>
|
|
171
|
+
<Text color="green">✓</Text> API Key: <Text bold>saved</Text>
|
|
172
|
+
</Text>
|
|
173
|
+
)}
|
|
174
|
+
<Text>
|
|
175
|
+
<Text color="green">✓</Text> Model:{' '}
|
|
176
|
+
<Text bold>{DEFAULT_MODELS[provider]?.[selectedModel]}</Text>
|
|
177
|
+
</Text>
|
|
178
|
+
<Box marginTop={1}>
|
|
179
|
+
<Text color="green" bold>
|
|
180
|
+
✅ Setup complete! Run `cnapse` to start chatting.
|
|
181
|
+
</Text>
|
|
182
|
+
</Box>
|
|
183
|
+
{provider === 'ollama' && (
|
|
184
|
+
<Box marginTop={1} flexDirection="column">
|
|
185
|
+
<Text color="yellow">
|
|
186
|
+
Note: Make sure the model is downloaded. Run:
|
|
187
|
+
</Text>
|
|
188
|
+
<Text color="cyan">
|
|
189
|
+
{' '}ollama pull {DEFAULT_MODELS[provider]?.[selectedModel]}
|
|
190
|
+
</Text>
|
|
191
|
+
</Box>
|
|
192
|
+
)}
|
|
193
|
+
</Box>
|
|
194
|
+
)}
|
|
195
|
+
|
|
196
|
+
<Box marginTop={2}>
|
|
197
|
+
<Text color="gray" dimColor>
|
|
198
|
+
Press Esc to cancel
|
|
199
|
+
</Text>
|
|
200
|
+
</Box>
|
|
201
|
+
</Box>
|
|
202
|
+
);
|
|
203
|
+
}
|