@projectservan8n/cnapse 0.2.1 → 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.
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Telegram Hook - Remote PC control via Telegram bot
3
+ */
4
+
5
+ import { useState, useCallback, useEffect, useRef } from 'react';
6
+ import { getTelegramBot, TelegramMessage } from '../services/telegram.js';
7
+
8
+ export interface UseTelegramResult {
9
+ isEnabled: boolean;
10
+ isStarting: boolean;
11
+ error: string | null;
12
+ lastMessage: TelegramMessage | null;
13
+ toggle: () => Promise<void>;
14
+ start: () => Promise<void>;
15
+ stop: () => Promise<void>;
16
+ }
17
+
18
+ export function useTelegram(onMessage?: (msg: TelegramMessage) => void): UseTelegramResult {
19
+ const [isEnabled, setIsEnabled] = useState(false);
20
+ const [isStarting, setIsStarting] = useState(false);
21
+ const [error, setError] = useState<string | null>(null);
22
+ const [lastMessage, setLastMessage] = useState<TelegramMessage | null>(null);
23
+ const onMessageRef = useRef(onMessage);
24
+
25
+ // Keep callback ref updated
26
+ useEffect(() => {
27
+ onMessageRef.current = onMessage;
28
+ }, [onMessage]);
29
+
30
+ const start = useCallback(async () => {
31
+ if (isEnabled) return;
32
+
33
+ setIsStarting(true);
34
+ setError(null);
35
+
36
+ try {
37
+ const bot = getTelegramBot();
38
+
39
+ // Setup event handlers
40
+ bot.on('message', (msg: TelegramMessage) => {
41
+ setLastMessage(msg);
42
+ onMessageRef.current?.(msg);
43
+ });
44
+
45
+ bot.on('error', (err: Error) => {
46
+ setError(err.message);
47
+ });
48
+
49
+ await bot.start();
50
+ setIsEnabled(true);
51
+ } catch (err) {
52
+ const errorMsg = err instanceof Error ? err.message : 'Failed to start Telegram bot';
53
+ setError(errorMsg);
54
+ throw err;
55
+ } finally {
56
+ setIsStarting(false);
57
+ }
58
+ }, [isEnabled]);
59
+
60
+ const stop = useCallback(async () => {
61
+ if (!isEnabled) return;
62
+
63
+ try {
64
+ const bot = getTelegramBot();
65
+ await bot.stop();
66
+ setIsEnabled(false);
67
+ } catch (err) {
68
+ const errorMsg = err instanceof Error ? err.message : 'Failed to stop Telegram bot';
69
+ setError(errorMsg);
70
+ throw err;
71
+ }
72
+ }, [isEnabled]);
73
+
74
+ const toggle = useCallback(async () => {
75
+ if (isEnabled) {
76
+ await stop();
77
+ } else {
78
+ await start();
79
+ }
80
+ }, [isEnabled, start, stop]);
81
+
82
+ return {
83
+ isEnabled,
84
+ isStarting,
85
+ error,
86
+ lastMessage,
87
+ toggle,
88
+ start,
89
+ stop,
90
+ };
91
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Vision Hook - Screenshot capture and AI description
3
+ */
4
+
5
+ import { useState, useCallback } from 'react';
6
+ import { describeScreen } from '../lib/vision.js';
7
+
8
+ export interface UseVisionResult {
9
+ isAnalyzing: boolean;
10
+ lastDescription: string | null;
11
+ lastScreenshot: string | null;
12
+ error: string | null;
13
+ analyze: () => Promise<string>;
14
+ }
15
+
16
+ export function useVision(): UseVisionResult {
17
+ const [isAnalyzing, setIsAnalyzing] = useState(false);
18
+ const [lastDescription, setLastDescription] = useState<string | null>(null);
19
+ const [lastScreenshot, setLastScreenshot] = useState<string | null>(null);
20
+ const [error, setError] = useState<string | null>(null);
21
+
22
+ const analyze = useCallback(async (): Promise<string> => {
23
+ setIsAnalyzing(true);
24
+ setError(null);
25
+
26
+ try {
27
+ const result = await describeScreen();
28
+ setLastDescription(result.description);
29
+ setLastScreenshot(result.screenshot);
30
+ return result.description;
31
+ } catch (err) {
32
+ const errorMsg = err instanceof Error ? err.message : 'Vision analysis failed';
33
+ setError(errorMsg);
34
+ throw err;
35
+ } finally {
36
+ setIsAnalyzing(false);
37
+ }
38
+ }, []);
39
+
40
+ return {
41
+ isAnalyzing,
42
+ lastDescription,
43
+ lastScreenshot,
44
+ error,
45
+ analyze,
46
+ };
47
+ }
package/src/index.tsx CHANGED
@@ -112,56 +112,9 @@ Manual Setup:
112
112
  }
113
113
 
114
114
  case 'init': {
115
- // Interactive setup
116
- const readline = await import('readline');
117
- const rl = readline.createInterface({
118
- input: process.stdin,
119
- output: process.stdout,
120
- });
121
-
122
- const question = (q: string): Promise<string> =>
123
- new Promise((resolve) => rl.question(q, resolve));
124
-
125
- console.log('\n🚀 C-napse Setup\n');
126
-
127
- console.log('Select a provider:');
128
- console.log(' 1. ollama - Local AI (free, requires Ollama installed)');
129
- console.log(' 2. openrouter - OpenRouter API (pay per use, many models)');
130
- console.log(' 3. anthropic - Anthropic Claude (pay per use)');
131
- console.log(' 4. openai - OpenAI GPT (pay per use)');
132
- console.log('');
133
-
134
- const providerChoice = await question('Enter choice (1-4) [1]: ');
135
- const providers = ['ollama', 'openrouter', 'anthropic', 'openai'] as const;
136
- const providerIndex = parseInt(providerChoice || '1') - 1;
137
- const provider = providers[providerIndex] || 'ollama';
138
-
139
- setProvider(provider);
140
- console.log(`✓ Provider set to: ${provider}`);
141
-
142
- if (provider !== 'ollama') {
143
- const apiKey = await question(`\nEnter your ${provider} API key: `);
144
- if (apiKey) {
145
- setApiKey(provider as any, apiKey);
146
- console.log(`✓ API key saved`);
147
- }
148
- }
149
-
150
- // Set default model based on provider
151
- const defaultModels: Record<string, string> = {
152
- ollama: 'qwen2.5:0.5b',
153
- openrouter: 'qwen/qwen-2.5-coder-32b-instruct',
154
- anthropic: 'claude-3-5-sonnet-20241022',
155
- openai: 'gpt-4o',
156
- };
157
-
158
- const model = await question(`\nModel [${defaultModels[provider]}]: `);
159
- setModel(model || defaultModels[provider]!);
160
- console.log(`✓ Model set to: ${model || defaultModels[provider]}`);
161
-
162
- rl.close();
163
-
164
- console.log('\n✅ Setup complete! Run `cnapse` to start chatting.\n');
115
+ // Interactive setup with Ink UI
116
+ const { Setup } = await import('./components/Setup.js');
117
+ render(<Setup />);
165
118
  process.exit(0);
166
119
  }
167
120
 
package/src/lib/api.ts CHANGED
@@ -18,11 +18,11 @@ When responding:
18
18
  - Use markdown formatting for code blocks
19
19
  - If asked to do something, explain what you'll do first`;
20
20
 
21
- export async function chat(messages: Message[]): Promise<ChatResponse> {
21
+ export async function chat(messages: Message[], systemPrompt?: string): Promise<ChatResponse> {
22
22
  const config = getConfig();
23
23
 
24
24
  const allMessages: Message[] = [
25
- { role: 'system', content: SYSTEM_PROMPT },
25
+ { role: 'system', content: systemPrompt || SYSTEM_PROMPT },
26
26
  ...messages,
27
27
  ];
28
28
 
package/src/lib/config.ts CHANGED
@@ -7,12 +7,17 @@ interface ConfigSchema {
7
7
  openrouter?: string;
8
8
  anthropic?: string;
9
9
  openai?: string;
10
+ telegram?: string;
10
11
  };
11
12
  ollamaHost: string;
12
13
  openrouter: {
13
14
  siteUrl: string;
14
15
  appName: string;
15
16
  };
17
+ telegram: {
18
+ chatId?: number;
19
+ enabled: boolean;
20
+ };
16
21
  }
17
22
 
18
23
  const config = new Conf<ConfigSchema>({
@@ -26,6 +31,9 @@ const config = new Conf<ConfigSchema>({
26
31
  siteUrl: 'https://github.com/projectservan8n/C-napse',
27
32
  appName: 'C-napse',
28
33
  },
34
+ telegram: {
35
+ enabled: false,
36
+ },
29
37
  },
30
38
  });
31
39
 
@@ -36,6 +44,7 @@ export function getConfig() {
36
44
  apiKeys: config.get('apiKeys'),
37
45
  ollamaHost: config.get('ollamaHost'),
38
46
  openrouter: config.get('openrouter'),
47
+ telegram: config.get('telegram'),
39
48
  };
40
49
  }
41
50
 
@@ -57,4 +66,16 @@ export function getApiKey(provider: keyof ConfigSchema['apiKeys']): string | und
57
66
  return config.get('apiKeys')[provider];
58
67
  }
59
68
 
69
+ export function setTelegramChatId(chatId: number) {
70
+ const telegram = config.get('telegram');
71
+ telegram.chatId = chatId;
72
+ config.set('telegram', telegram);
73
+ }
74
+
75
+ export function setTelegramEnabled(enabled: boolean) {
76
+ const telegram = config.get('telegram');
77
+ telegram.enabled = enabled;
78
+ config.set('telegram', telegram);
79
+ }
80
+
60
81
  export { config };
@@ -0,0 +1,118 @@
1
+ import { exec } from 'child_process';
2
+ import { promisify } from 'util';
3
+ import { tmpdir } from 'os';
4
+ import { join } from 'path';
5
+ import { readFile, unlink } from 'fs/promises';
6
+
7
+ const execAsync = promisify(exec);
8
+
9
+ let lastScreenHash: string | null = null;
10
+ let isCapturing = false;
11
+
12
+ /**
13
+ * Capture screenshot and return base64 encoded image
14
+ * Uses platform-specific tools
15
+ */
16
+ export async function captureScreen(): Promise<string | null> {
17
+ if (isCapturing) return null;
18
+ isCapturing = true;
19
+
20
+ const tempFile = join(tmpdir(), `cnapse-screen-${Date.now()}.png`);
21
+
22
+ try {
23
+ const platform = process.platform;
24
+
25
+ if (platform === 'win32') {
26
+ // Windows: Use PowerShell to capture screen
27
+ await execAsync(`
28
+ Add-Type -AssemblyName System.Windows.Forms
29
+ $screen = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds
30
+ $bitmap = New-Object System.Drawing.Bitmap($screen.Width, $screen.Height)
31
+ $graphics = [System.Drawing.Graphics]::FromImage($bitmap)
32
+ $graphics.CopyFromScreen($screen.Location, [System.Drawing.Point]::Empty, $screen.Size)
33
+ $bitmap.Save("${tempFile.replace(/\\/g, '\\\\')}")
34
+ $graphics.Dispose()
35
+ $bitmap.Dispose()
36
+ `, { shell: 'powershell.exe' });
37
+ } else if (platform === 'darwin') {
38
+ // macOS: Use screencapture
39
+ await execAsync(`screencapture -x "${tempFile}"`);
40
+ } else {
41
+ // Linux: Try various tools
42
+ try {
43
+ await execAsync(`gnome-screenshot -f "${tempFile}" 2>/dev/null || scrot "${tempFile}" 2>/dev/null || import -window root "${tempFile}"`);
44
+ } catch {
45
+ return null;
46
+ }
47
+ }
48
+
49
+ // Read the file and convert to base64
50
+ const imageBuffer = await readFile(tempFile);
51
+ const base64 = imageBuffer.toString('base64');
52
+
53
+ // Clean up
54
+ await unlink(tempFile).catch(() => {});
55
+
56
+ return base64;
57
+ } catch (error) {
58
+ return null;
59
+ } finally {
60
+ isCapturing = false;
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Simple hash function for change detection
66
+ */
67
+ function simpleHash(str: string): string {
68
+ let hash = 0;
69
+ for (let i = 0; i < str.length; i += 100) {
70
+ const char = str.charCodeAt(i);
71
+ hash = ((hash << 5) - hash) + char;
72
+ hash = hash & hash;
73
+ }
74
+ return hash.toString(16);
75
+ }
76
+
77
+ /**
78
+ * Check if screen has changed since last capture
79
+ */
80
+ export async function checkScreenChange(): Promise<{ changed: boolean; image: string | null }> {
81
+ const image = await captureScreen();
82
+
83
+ if (!image) {
84
+ return { changed: false, image: null };
85
+ }
86
+
87
+ const currentHash = simpleHash(image);
88
+ const changed = lastScreenHash !== null && lastScreenHash !== currentHash;
89
+ lastScreenHash = currentHash;
90
+
91
+ return { changed, image };
92
+ }
93
+
94
+ /**
95
+ * Get screen description for context (simplified - just dimensions)
96
+ */
97
+ export async function getScreenDescription(): Promise<string | null> {
98
+ try {
99
+ const platform = process.platform;
100
+
101
+ if (platform === 'win32') {
102
+ const { stdout } = await execAsync(`
103
+ Add-Type -AssemblyName System.Windows.Forms
104
+ $screen = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds
105
+ Write-Output "$($screen.Width)x$($screen.Height)"
106
+ `, { shell: 'powershell.exe' });
107
+ return `Screen ${stdout.trim()} captured`;
108
+ } else if (platform === 'darwin') {
109
+ const { stdout } = await execAsync(`system_profiler SPDisplaysDataType | grep Resolution | head -1`);
110
+ return `Screen ${stdout.trim()}`;
111
+ } else {
112
+ const { stdout } = await execAsync(`xdpyinfo | grep dimensions | awk '{print $2}'`);
113
+ return `Screen ${stdout.trim()} captured`;
114
+ }
115
+ } catch {
116
+ return null;
117
+ }
118
+ }