@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.
- package/dist/Setup-Q32JPHGP.js +174 -0
- package/dist/chunk-COKO6V5J.js +50 -0
- package/dist/index.js +1684 -186
- 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 +222 -124
- package/src/components/Header.tsx +11 -1
- package/src/components/HelpMenu.tsx +144 -0
- package/src/components/ProviderSelector.tsx +176 -0
- package/src/components/Setup.tsx +203 -0
- package/src/components/TaskProgress.tsx +68 -0
- package/src/hooks/index.ts +15 -0
- package/src/hooks/useChat.ts +149 -0
- package/src/hooks/useTasks.ts +63 -0
- package/src/hooks/useTelegram.ts +91 -0
- package/src/hooks/useVision.ts +47 -0
- package/src/index.tsx +3 -50
- 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 +483 -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
|
@@ -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
|
|
117
|
-
|
|
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
|
+
}
|