@projectservan8n/cnapse 0.4.0 → 0.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@projectservan8n/cnapse",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Autonomous PC intelligence - AI assistant for desktop automation",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,372 +1,253 @@
1
- import React, { useState, useEffect, useRef } from 'react';
1
+ import React, { useState, useCallback } 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
7
  import { HelpMenu } from './HelpMenu.js';
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';
13
-
14
- interface ChatMsg {
15
- id: string;
16
- role: 'user' | 'assistant' | 'system';
17
- content: string;
18
- timestamp: Date;
19
- isStreaming?: boolean;
20
- }
8
+ import { ProviderSelector } from './ProviderSelector.js';
9
+ import { getConfig } from '../lib/config.js';
10
+ import { useChat, useVision, useTelegram, useTasks } from '../hooks/index.js';
11
+
12
+ type OverlayType = 'none' | 'help' | 'provider';
21
13
 
22
14
  export function App() {
23
15
  const { exit } = useApp();
24
- const [messages, setMessages] = useState<ChatMsg[]>([
25
- {
26
- id: '0',
27
- role: 'system',
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',
29
- timestamp: new Date(),
30
- },
31
- ]);
32
- const [input, setInput] = useState('');
33
- const [isProcessing, setIsProcessing] = useState(false);
34
- const [status, setStatus] = useState('Ready');
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
16
 
55
- checkScreen();
56
- const interval = setInterval(checkScreen, 5000); // Check every 5 seconds
17
+ // UI State
18
+ const [overlay, setOverlay] = useState<OverlayType>('none');
19
+ const [screenWatch, setScreenWatch] = useState(false);
20
+ const [status, setStatus] = useState('Ready');
57
21
 
58
- return () => clearInterval(interval);
59
- }, [screenWatch]);
22
+ // Feature hooks
23
+ const chat = useChat(screenWatch);
24
+ const vision = useVision();
25
+ const telegram = useTelegram((msg) => {
26
+ chat.addSystemMessage(`📱 Telegram [${msg.from}]: ${msg.text}`);
27
+ });
28
+ const tasks = useTasks((task, step) => {
29
+ if (step.status === 'running') {
30
+ setStatus(`Running: ${step.description}`);
31
+ }
32
+ });
60
33
 
34
+ // Keyboard shortcuts
61
35
  useInput((inputChar, key) => {
62
- // If help menu is open, don't process other shortcuts
63
- if (showHelpMenu) {
64
- return;
65
- }
36
+ if (overlay !== 'none') return;
66
37
 
67
- if (key.ctrl && inputChar === 'c') {
68
- exit();
69
- }
70
- if (key.ctrl && inputChar === 'l') {
71
- setMessages([messages[0]!]); // Keep welcome message
72
- setError(null);
73
- }
38
+ if (key.ctrl && inputChar === 'c') exit();
39
+ if (key.ctrl && inputChar === 'l') chat.clearMessages();
40
+ if (key.ctrl && inputChar === 'h') setOverlay('help');
41
+ if (key.ctrl && inputChar === 'p') setOverlay('provider');
74
42
  if (key.ctrl && inputChar === 'w') {
75
- setScreenWatch((prev) => {
43
+ setScreenWatch(prev => {
76
44
  const newState = !prev;
77
- addSystemMessage(
78
- newState
79
- ? '🖥️ Screen watching enabled. AI will have context of your screen.'
80
- : '🖥️ Screen watching disabled.'
45
+ chat.addSystemMessage(newState
46
+ ? '🖥️ Screen watching enabled.'
47
+ : '🖥️ Screen watching disabled.'
81
48
  );
82
49
  return newState;
83
50
  });
84
51
  }
85
- if (key.ctrl && inputChar === 'h') {
86
- setShowHelpMenu(true);
87
- }
88
52
  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
- });
53
+ handleTelegramToggle();
98
54
  }
99
55
  });
100
56
 
101
- const handleSubmit = async (value: string) => {
102
- if (!value.trim() || isProcessing) return;
103
-
104
- const userInput = value.trim();
105
- setInput('');
106
- setError(null);
107
-
108
- // Handle commands
109
- if (userInput.startsWith('/')) {
110
- handleCommand(userInput);
111
- return;
112
- }
113
-
114
- // Add user message
115
- const userMsg: ChatMsg = {
116
- id: Date.now().toString(),
117
- role: 'user',
118
- content: userInput,
119
- timestamp: new Date(),
120
- };
121
- setMessages((prev) => [...prev, userMsg]);
122
-
123
- // Add placeholder for assistant
124
- const assistantId = (Date.now() + 1).toString();
125
- setMessages((prev) => [
126
- ...prev,
127
- {
128
- id: assistantId,
129
- role: 'assistant',
130
- content: '',
131
- timestamp: new Date(),
132
- isStreaming: true,
133
- },
134
- ]);
135
-
136
- setIsProcessing(true);
137
- setStatus('Thinking...');
138
-
139
- try {
140
- // Build message history for API
141
- const apiMessages: Message[] = messages
142
- .filter((m) => m.role === 'user' || m.role === 'assistant')
143
- .slice(-10)
144
- .map((m) => ({ role: m.role as 'user' | 'assistant', content: m.content }));
145
-
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 });
153
-
154
- const response = await chat(apiMessages);
155
-
156
- // Update assistant message with response
157
- setMessages((prev) =>
158
- prev.map((m) =>
159
- m.id === assistantId
160
- ? { ...m, content: response.content || '(no response)', isStreaming: false }
161
- : m
162
- )
163
- );
164
- } catch (err) {
165
- const errorMsg = err instanceof Error ? err.message : 'Unknown error';
166
- setError(errorMsg);
167
- // Update assistant message with error
168
- setMessages((prev) =>
169
- prev.map((m) =>
170
- m.id === assistantId
171
- ? { ...m, content: `Error: ${errorMsg}`, isStreaming: false }
172
- : m
173
- )
174
- );
175
- } finally {
176
- setIsProcessing(false);
177
- setStatus('Ready');
178
- }
179
- };
180
-
181
- const handleCommand = (cmd: string) => {
57
+ // Command handlers
58
+ const handleCommand = useCallback(async (cmd: string) => {
182
59
  const parts = cmd.slice(1).split(' ');
183
60
  const command = parts[0];
184
61
  const args = parts.slice(1).join(' ');
185
62
 
186
63
  switch (command) {
187
64
  case 'clear':
188
- setMessages([messages[0]!]);
189
- addSystemMessage('Chat cleared.');
65
+ chat.clearMessages();
66
+ chat.addSystemMessage('Chat cleared.');
190
67
  break;
68
+
191
69
  case 'help':
192
- setShowHelpMenu(true);
70
+ setOverlay('help');
71
+ break;
72
+
73
+ case 'provider':
74
+ case 'model':
75
+ setOverlay('provider');
193
76
  break;
77
+
78
+ case 'config': {
79
+ const config = getConfig();
80
+ chat.addSystemMessage(
81
+ `⚙️ Configuration:\n` +
82
+ ` Provider: ${config.provider}\n` +
83
+ ` Model: ${config.model}\n` +
84
+ ` Ollama: ${config.ollamaHost}\n\n` +
85
+ `Use /provider to change`
86
+ );
87
+ break;
88
+ }
89
+
90
+ case 'screen':
91
+ await handleScreenCommand();
92
+ break;
93
+
194
94
  case 'watch':
195
- setScreenWatch((prev) => {
95
+ setScreenWatch(prev => {
196
96
  const newState = !prev;
197
- addSystemMessage(
198
- newState
199
- ? '🖥️ Screen watching enabled.'
200
- : '🖥️ Screen watching disabled.'
97
+ chat.addSystemMessage(newState
98
+ ? '🖥️ Screen watching enabled.'
99
+ : '🖥️ Screen watching disabled.'
201
100
  );
202
101
  return newState;
203
102
  });
204
103
  break;
104
+
205
105
  case 'telegram':
206
- handleTelegramToggle();
207
- break;
208
- case 'screen':
209
- handleScreenCommand();
106
+ await handleTelegramToggle();
210
107
  break;
108
+
211
109
  case 'task':
212
110
  if (args) {
213
- handleTaskCommand(args);
111
+ await handleTaskCommand(args);
214
112
  } else {
215
- addSystemMessage('Usage: /task <description>\nExample: /task open notepad and type hello');
113
+ chat.addSystemMessage('Usage: /task <description>\nExample: /task open notepad and type hello');
216
114
  }
217
115
  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>');
116
+
117
+ case 'memory':
118
+ if (args === 'clear') {
119
+ tasks.clearMemory();
120
+ chat.addSystemMessage('🧠 Task memory cleared.');
121
+ } else {
122
+ const stats = tasks.getMemoryStats();
123
+ chat.addSystemMessage(
124
+ `🧠 Task Memory:\n\n` +
125
+ ` Learned patterns: ${stats.patternCount}\n` +
126
+ ` Total successful uses: ${stats.totalUses}\n\n` +
127
+ (stats.topPatterns.length > 0
128
+ ? ` Top patterns:\n${stats.topPatterns.map(p => ` • ${p}`).join('\n')}\n\n`
129
+ : ' No patterns learned yet.\n\n') +
130
+ `The more you use /task, the smarter it gets!\n` +
131
+ `Use /memory clear to reset.`
132
+ );
133
+ }
226
134
  break;
135
+
227
136
  case 'quit':
228
137
  case 'exit':
229
138
  exit();
230
139
  break;
140
+
231
141
  default:
232
- addSystemMessage(`Unknown command: ${command}\nType /help to see available commands.`);
142
+ chat.addSystemMessage(`Unknown command: ${command}\nType /help for commands`);
233
143
  }
234
- };
144
+ }, [chat, exit]);
235
145
 
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);
146
+ // Screen command
147
+ const handleScreenCommand = useCallback(async () => {
148
+ chat.addSystemMessage('📸 Analyzing screen...');
149
+ setStatus('Analyzing...');
245
150
 
246
151
  try {
247
- const result = await describeScreen();
248
- addSystemMessage(`🖥️ Screen Analysis:\n\n${result.description}`);
152
+ const description = await vision.analyze();
153
+ chat.addSystemMessage(`🖥️ Screen:\n\n${description}`);
249
154
  } catch (err) {
250
- const errorMsg = err instanceof Error ? err.message : 'Vision analysis failed';
251
- addSystemMessage(`❌ Screen capture failed: ${errorMsg}`);
155
+ chat.addSystemMessage(`❌ ${vision.error || 'Vision failed'}`);
252
156
  } finally {
253
- setIsProcessing(false);
254
157
  setStatus('Ready');
255
158
  }
256
- };
159
+ }, [chat, vision]);
257
160
 
258
- const handleTaskCommand = async (taskDescription: string) => {
259
- addSystemMessage(`📋 Parsing task: ${taskDescription}`);
161
+ // Telegram toggle
162
+ const handleTelegramToggle = useCallback(async () => {
163
+ if (telegram.isEnabled) {
164
+ await telegram.stop();
165
+ chat.addSystemMessage('📱 Telegram stopped.');
166
+ } else {
167
+ chat.addSystemMessage('📱 Starting Telegram...');
168
+ setStatus('Starting Telegram...');
169
+ try {
170
+ await telegram.start();
171
+ chat.addSystemMessage(
172
+ '📱 Telegram started!\n' +
173
+ 'Send /start to your bot to connect.\n' +
174
+ 'Commands: /screen, /describe, /run, /status'
175
+ );
176
+ } catch {
177
+ chat.addSystemMessage(`❌ ${telegram.error || 'Telegram failed'}`);
178
+ } finally {
179
+ setStatus('Ready');
180
+ }
181
+ }
182
+ }, [chat, telegram]);
183
+
184
+ // Task command
185
+ const handleTaskCommand = useCallback(async (description: string) => {
186
+ chat.addSystemMessage(`📋 Parsing: ${description}`);
260
187
  setStatus('Parsing task...');
261
- setIsProcessing(true);
262
188
 
263
189
  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}`);
190
+ const task = await tasks.run(description);
191
+ chat.addSystemMessage(`\n${tasks.format(task)}`);
192
+ chat.addSystemMessage(task.status === 'completed'
193
+ ? '✅ Task completed!'
194
+ : '❌ Task failed.'
195
+ );
196
+ } catch {
197
+ chat.addSystemMessage(`❌ ${tasks.error || 'Task failed'}`);
290
198
  } finally {
291
- setIsProcessing(false);
292
199
  setStatus('Ready');
293
200
  }
294
- };
201
+ }, [chat, tasks]);
295
202
 
296
- const handleTelegramToggle = async () => {
297
- const bot = getTelegramBot();
203
+ // Submit handler
204
+ const handleSubmit = useCallback(async (value: string) => {
205
+ if (!value.trim()) return;
298
206
 
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
- }
207
+ if (value.startsWith('/')) {
208
+ await handleCommand(value);
309
209
  } 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
- });
210
+ setStatus('Thinking...');
211
+ await chat.sendMessage(value);
212
+ setStatus('Ready');
213
+ }
214
+ }, [chat, handleCommand]);
319
215
 
320
- bot.on('error', (error: Error) => {
321
- addSystemMessage(`📱 Telegram error: ${error.message}`);
322
- });
216
+ // Provider selection callback
217
+ const handleProviderSelect = useCallback((provider: string, model: string) => {
218
+ chat.addSystemMessage(`✅ Updated: ${provider} / ${model}`);
219
+ }, [chat]);
323
220
 
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
- }
337
- }
338
- };
339
-
340
- const addSystemMessage = (content: string) => {
341
- setMessages((prev) => [
342
- ...prev,
343
- {
344
- id: Date.now().toString(),
345
- role: 'system',
346
- content,
347
- timestamp: new Date(),
348
- },
349
- ]);
350
- };
351
-
352
- // Only show last N messages that fit
353
- const visibleMessages = messages.slice(-20);
354
-
355
- // If help menu is open, show it as overlay
356
- if (showHelpMenu) {
221
+ // Render overlays
222
+ if (overlay === 'help') {
357
223
  return (
358
224
  <Box flexDirection="column" height="100%" alignItems="center" justifyContent="center">
359
225
  <HelpMenu
360
- onClose={() => setShowHelpMenu(false)}
361
- onSelect={handleHelpMenuSelect}
226
+ onClose={() => setOverlay('none')}
227
+ onSelect={(cmd) => { setOverlay('none'); handleCommand(cmd); }}
362
228
  />
363
229
  </Box>
364
230
  );
365
231
  }
366
232
 
233
+ if (overlay === 'provider') {
234
+ return (
235
+ <Box flexDirection="column" height="100%" alignItems="center" justifyContent="center">
236
+ <ProviderSelector
237
+ onClose={() => setOverlay('none')}
238
+ onSelect={handleProviderSelect}
239
+ />
240
+ </Box>
241
+ );
242
+ }
243
+
244
+ // Main UI
245
+ const visibleMessages = chat.messages.slice(-20);
246
+ const isProcessing = chat.isProcessing || vision.isAnalyzing || tasks.isRunning || telegram.isStarting;
247
+
367
248
  return (
368
249
  <Box flexDirection="column" height="100%">
369
- <Header screenWatch={screenWatch} telegramEnabled={telegramEnabled} />
250
+ <Header screenWatch={screenWatch} telegramEnabled={telegram.isEnabled} />
370
251
 
371
252
  <Box flexDirection="column" flexGrow={1} borderStyle="round" borderColor="gray" padding={1}>
372
253
  <Text bold color="gray"> Chat </Text>
@@ -381,15 +262,15 @@ export function App() {
381
262
  ))}
382
263
  </Box>
383
264
 
384
- {error && (
265
+ {chat.error && (
385
266
  <Box marginY={1}>
386
- <Text color="red">Error: {error}</Text>
267
+ <Text color="red">Error: {chat.error}</Text>
387
268
  </Box>
388
269
  )}
389
270
 
390
271
  <ChatInput
391
- value={input}
392
- onChange={setInput}
272
+ value=""
273
+ onChange={() => {}}
393
274
  onSubmit={handleSubmit}
394
275
  isProcessing={isProcessing}
395
276
  />
@@ -18,12 +18,13 @@ const MENU_ITEMS: MenuItem[] = [
18
18
  { command: '/screen', shortcut: 'Ctrl+S', description: 'Take screenshot and describe', category: 'actions' },
19
19
  { command: '/task', description: 'Run multi-step task', category: 'actions' },
20
20
  { command: '/telegram', shortcut: 'Ctrl+T', description: 'Toggle Telegram bot', category: 'actions' },
21
+ { command: '/memory', description: 'View/clear learned task patterns', category: 'actions' },
21
22
 
22
23
  // Settings
23
24
  { command: '/config', description: 'Show/edit configuration', category: 'settings' },
24
25
  { command: '/watch', shortcut: 'Ctrl+W', description: 'Toggle screen watching', category: 'settings' },
25
26
  { command: '/model', description: 'Change AI model', category: 'settings' },
26
- { command: '/provider', description: 'Change AI provider', category: 'settings' },
27
+ { command: '/provider', shortcut: 'Ctrl+P', description: 'Change AI provider', category: 'settings' },
27
28
  ];
28
29
 
29
30
  interface HelpMenuProps {