@projectservan8n/cnapse 0.6.3 → 0.8.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.
@@ -1,352 +0,0 @@
1
- /**
2
- * Interactive Configuration UI
3
- * - Select provider
4
- * - Enter API key (for non-Ollama)
5
- * - Select model from recommended list
6
- * - For Ollama: runs the model to ensure it's ready
7
- */
8
-
9
- import React, { useState, useEffect } from 'react';
10
- import { Box, Text, useInput, useApp } from 'ink';
11
- import TextInput from 'ink-text-input';
12
- import Spinner from 'ink-spinner';
13
- import { getConfig, setProvider, setModel, setApiKey } from '../lib/config.js';
14
- import { exec } from 'child_process';
15
- import { promisify } from 'util';
16
-
17
- const execAsync = promisify(exec);
18
-
19
- type Step = 'provider' | 'apiKey' | 'model' | 'ollamaCheck' | 'done';
20
-
21
- interface ProviderConfig {
22
- id: 'ollama' | 'openrouter' | 'anthropic' | 'openai';
23
- name: string;
24
- description: string;
25
- needsApiKey: boolean;
26
- models: Array<{
27
- id: string;
28
- name: string;
29
- description: string;
30
- recommended?: boolean;
31
- }>;
32
- }
33
-
34
- const PROVIDERS: ProviderConfig[] = [
35
- {
36
- id: 'ollama',
37
- name: 'Ollama',
38
- description: 'Local AI - Free, private, runs on your PC',
39
- needsApiKey: false,
40
- models: [
41
- { id: 'qwen2.5:0.5b', name: 'Qwen 2.5 0.5B', description: 'Ultra fast, good for tasks', recommended: true },
42
- { id: 'qwen2.5:1.5b', name: 'Qwen 2.5 1.5B', description: 'Fast, better quality' },
43
- { id: 'qwen2.5:7b', name: 'Qwen 2.5 7B', description: 'Best quality, needs 8GB+ RAM' },
44
- { id: 'llama3.2:1b', name: 'Llama 3.2 1B', description: 'Fast, good general use' },
45
- { id: 'llama3.2:3b', name: 'Llama 3.2 3B', description: 'Balanced speed/quality' },
46
- { id: 'codellama:7b', name: 'Code Llama 7B', description: 'Best for coding tasks' },
47
- { id: 'llava:7b', name: 'LLaVA 7B', description: 'Vision model - can see images' },
48
- ],
49
- },
50
- {
51
- id: 'openrouter',
52
- name: 'OpenRouter',
53
- description: 'Many models, pay-per-use, great value',
54
- needsApiKey: true,
55
- models: [
56
- { id: 'openai/gpt-5-nano', name: 'GPT-5 Nano', description: 'Best budget vision! $0.05/1M in', recommended: true },
57
- { id: 'google/gemini-2.5-flash-lite', name: 'Gemini 2.5 Flash Lite', description: 'Fast reasoning, $0.10/1M in' },
58
- { id: 'qwen/qwen-2.5-coder-32b-instruct', name: 'Qwen 2.5 Coder 32B', description: 'Best for coding, $0.07/1M' },
59
- { id: 'meta-llama/llama-3.3-70b-instruct', name: 'Llama 3.3 70B', description: 'Powerful, $0.10/1M' },
60
- { id: 'deepseek/deepseek-chat', name: 'DeepSeek V3', description: 'Cheap, $0.14/1M' },
61
- ],
62
- },
63
- {
64
- id: 'anthropic',
65
- name: 'Anthropic',
66
- description: 'Claude models - Best for complex reasoning',
67
- needsApiKey: true,
68
- models: [
69
- { id: 'claude-3-5-sonnet-20241022', name: 'Claude 3.5 Sonnet', description: 'Best balance of speed/quality', recommended: true },
70
- { id: 'claude-3-opus-20240229', name: 'Claude 3 Opus', description: 'Most capable, slower' },
71
- { id: 'claude-3-haiku-20240307', name: 'Claude 3 Haiku', description: 'Fastest, cheapest' },
72
- ],
73
- },
74
- {
75
- id: 'openai',
76
- name: 'OpenAI',
77
- description: 'GPT models - Well-known, reliable',
78
- needsApiKey: true,
79
- models: [
80
- { id: 'gpt-4o', name: 'GPT-4o', description: 'Latest, multimodal', recommended: true },
81
- { id: 'gpt-4o-mini', name: 'GPT-4o Mini', description: 'Fast and cheap' },
82
- { id: 'gpt-4-turbo', name: 'GPT-4 Turbo', description: 'Previous best' },
83
- { id: 'gpt-3.5-turbo', name: 'GPT-3.5 Turbo', description: 'Legacy, very cheap' },
84
- ],
85
- },
86
- ];
87
-
88
- export function ConfigUI() {
89
- const { exit } = useApp();
90
- const config = getConfig();
91
-
92
- const [step, setStep] = useState<Step>('provider');
93
- const [providerIndex, setProviderIndex] = useState(() => {
94
- const idx = PROVIDERS.findIndex(p => p.id === config.provider);
95
- return idx >= 0 ? idx : 0;
96
- });
97
- const [modelIndex, setModelIndex] = useState(0);
98
- const [apiKeyInput, setApiKeyInput] = useState('');
99
- const [selectedProvider, setSelectedProvider] = useState<ProviderConfig | null>(null);
100
- const [ollamaStatus, setOllamaStatus] = useState<'checking' | 'pulling' | 'running' | 'ready' | 'error'>('checking');
101
- const [ollamaMessage, setOllamaMessage] = useState('');
102
-
103
- // Handle keyboard input
104
- useInput((input, key) => {
105
- if (key.escape) {
106
- exit();
107
- return;
108
- }
109
-
110
- if (step === 'provider') {
111
- if (key.upArrow) {
112
- setProviderIndex(prev => (prev > 0 ? prev - 1 : PROVIDERS.length - 1));
113
- } else if (key.downArrow) {
114
- setProviderIndex(prev => (prev < PROVIDERS.length - 1 ? prev + 1 : 0));
115
- } else if (key.return) {
116
- const provider = PROVIDERS[providerIndex]!;
117
- setSelectedProvider(provider);
118
- setProvider(provider.id);
119
-
120
- // Find recommended model index
121
- const recommendedIdx = provider.models.findIndex(m => m.recommended);
122
- setModelIndex(recommendedIdx >= 0 ? recommendedIdx : 0);
123
-
124
- if (provider.needsApiKey) {
125
- const apiKeyProvider = provider.id as 'openrouter' | 'anthropic' | 'openai';
126
- if (!config.apiKeys[apiKeyProvider]) {
127
- setStep('apiKey');
128
- } else {
129
- setStep('model');
130
- }
131
- } else {
132
- setStep('model');
133
- }
134
- }
135
- } else if (step === 'model' && selectedProvider) {
136
- if (key.upArrow) {
137
- setModelIndex(prev => (prev > 0 ? prev - 1 : selectedProvider.models.length - 1));
138
- } else if (key.downArrow) {
139
- setModelIndex(prev => (prev < selectedProvider.models.length - 1 ? prev + 1 : 0));
140
- } else if (key.return) {
141
- const model = selectedProvider.models[modelIndex]!;
142
- setModel(model.id);
143
-
144
- if (selectedProvider.id === 'ollama') {
145
- setStep('ollamaCheck');
146
- } else {
147
- setStep('done');
148
- setTimeout(() => exit(), 2000);
149
- }
150
- } else if (key.leftArrow || input === 'b') {
151
- setStep('provider');
152
- }
153
- }
154
- });
155
-
156
- // Handle API key submission
157
- const handleApiKeySubmit = (value: string) => {
158
- if (value.trim() && selectedProvider) {
159
- setApiKey(selectedProvider.id as 'openrouter' | 'anthropic' | 'openai', value.trim());
160
- setStep('model');
161
- }
162
- };
163
-
164
- // Ollama model check and run
165
- useEffect(() => {
166
- if (step !== 'ollamaCheck' || !selectedProvider) return;
167
-
168
- const modelId = selectedProvider.models[modelIndex]!.id;
169
-
170
- async function checkAndRunOllama() {
171
- try {
172
- // Check if Ollama is running
173
- setOllamaStatus('checking');
174
- setOllamaMessage('Checking Ollama...');
175
-
176
- try {
177
- await execAsync('ollama list', { timeout: 5000 });
178
- } catch {
179
- setOllamaStatus('error');
180
- setOllamaMessage('Ollama not found. Install from https://ollama.ai');
181
- setTimeout(() => exit(), 3000);
182
- return;
183
- }
184
-
185
- // Check if model exists
186
- const { stdout } = await execAsync('ollama list');
187
- const modelName = modelId.split(':')[0];
188
- const hasModel = stdout.toLowerCase().includes(modelName!.toLowerCase());
189
-
190
- if (!hasModel) {
191
- setOllamaStatus('pulling');
192
- setOllamaMessage(`Downloading ${modelId}... (this may take a few minutes)`);
193
-
194
- // Pull the model
195
- await execAsync(`ollama pull ${modelId}`, { timeout: 600000 }); // 10 min timeout
196
- }
197
-
198
- // Run the model to load it into memory
199
- setOllamaStatus('running');
200
- setOllamaMessage(`Starting ${modelId}...`);
201
-
202
- // Send a simple request to load the model
203
- await execAsync(`ollama run ${modelId} "Hello" --nowordwrap`, { timeout: 120000 });
204
-
205
- setOllamaStatus('ready');
206
- setOllamaMessage(`${modelId} is ready!`);
207
- setStep('done');
208
- setTimeout(() => exit(), 2000);
209
-
210
- } catch (err) {
211
- setOllamaStatus('error');
212
- setOllamaMessage(`Error: ${err instanceof Error ? err.message : 'Unknown error'}`);
213
- setTimeout(() => exit(), 3000);
214
- }
215
- }
216
-
217
- checkAndRunOllama();
218
- }, [step, selectedProvider, modelIndex, exit]);
219
-
220
- return (
221
- <Box flexDirection="column" padding={1}>
222
- <Box marginBottom={1}>
223
- <Text bold color="cyan">C-napse Configuration</Text>
224
- </Box>
225
-
226
- {/* Provider Selection */}
227
- {step === 'provider' && (
228
- <Box flexDirection="column">
229
- <Text bold>Select AI Provider:</Text>
230
- <Text color="gray" dimColor>(Use arrows, Enter to select, Esc to cancel)</Text>
231
- <Box marginTop={1} flexDirection="column">
232
- {PROVIDERS.map((p, i) => {
233
- const isSelected = i === providerIndex;
234
- const isCurrent = p.id === config.provider;
235
- return (
236
- <Box key={p.id} flexDirection="column">
237
- <Text color={isSelected ? 'cyan' : 'white'}>
238
- {isSelected ? '❯ ' : ' '}
239
- <Text bold={isSelected}>{p.name}</Text>
240
- {isCurrent && <Text color="green"> (current)</Text>}
241
- {p.needsApiKey && p.id !== 'ollama' && config.apiKeys[p.id as 'openrouter' | 'anthropic' | 'openai'] && <Text color="yellow"> (key saved)</Text>}
242
- </Text>
243
- {isSelected && (
244
- <Text color="gray"> {p.description}</Text>
245
- )}
246
- </Box>
247
- );
248
- })}
249
- </Box>
250
- </Box>
251
- )}
252
-
253
- {/* API Key Input */}
254
- {step === 'apiKey' && selectedProvider && (
255
- <Box flexDirection="column">
256
- <Text><Text color="green">✓</Text> Provider: <Text bold>{selectedProvider.name}</Text></Text>
257
- <Box marginTop={1} flexDirection="column">
258
- <Text bold>Enter your {selectedProvider.name} API key:</Text>
259
- <Text color="gray" dimColor>
260
- {selectedProvider.id === 'openrouter' && 'Get key at: https://openrouter.ai/keys'}
261
- {selectedProvider.id === 'anthropic' && 'Get key at: https://console.anthropic.com'}
262
- {selectedProvider.id === 'openai' && 'Get key at: https://platform.openai.com/api-keys'}
263
- </Text>
264
- <Box marginTop={1}>
265
- <Text color="cyan">❯ </Text>
266
- <TextInput
267
- value={apiKeyInput}
268
- onChange={setApiKeyInput}
269
- onSubmit={handleApiKeySubmit}
270
- mask="*"
271
- />
272
- </Box>
273
- </Box>
274
- </Box>
275
- )}
276
-
277
- {/* Model Selection */}
278
- {step === 'model' && selectedProvider && (
279
- <Box flexDirection="column">
280
- <Text><Text color="green">✓</Text> Provider: <Text bold>{selectedProvider.name}</Text></Text>
281
- {selectedProvider.needsApiKey && (
282
- <Text><Text color="green">✓</Text> API Key: <Text bold>configured</Text></Text>
283
- )}
284
- <Box marginTop={1} flexDirection="column">
285
- <Text bold>Select Model:</Text>
286
- <Text color="gray" dimColor>(Arrows to navigate, Enter to select, B to go back)</Text>
287
- <Box marginTop={1} flexDirection="column">
288
- {selectedProvider.models.map((model, i) => {
289
- const isSelected = i === modelIndex;
290
- const isCurrent = model.id === config.model && selectedProvider.id === config.provider;
291
- return (
292
- <Box key={model.id} flexDirection="column">
293
- <Text color={isSelected ? 'cyan' : 'white'}>
294
- {isSelected ? '❯ ' : ' '}
295
- <Text bold={isSelected}>{model.name}</Text>
296
- {model.recommended && <Text color="yellow"> *</Text>}
297
- {isCurrent && <Text color="green"> (current)</Text>}
298
- </Text>
299
- {isSelected && (
300
- <Text color="gray"> {model.description}</Text>
301
- )}
302
- </Box>
303
- );
304
- })}
305
- </Box>
306
- <Box marginTop={1}>
307
- <Text color="gray" dimColor>* = Recommended for C-napse</Text>
308
- </Box>
309
- </Box>
310
- </Box>
311
- )}
312
-
313
- {/* Ollama Check */}
314
- {step === 'ollamaCheck' && selectedProvider && (
315
- <Box flexDirection="column">
316
- <Text><Text color="green">✓</Text> Provider: <Text bold>{selectedProvider.name}</Text></Text>
317
- <Text><Text color="green">✓</Text> Model: <Text bold>{selectedProvider.models[modelIndex]?.name}</Text></Text>
318
- <Box marginTop={1}>
319
- {ollamaStatus === 'error' ? (
320
- <Text color="red">✗ {ollamaMessage}</Text>
321
- ) : ollamaStatus === 'ready' ? (
322
- <Text color="green">✓ {ollamaMessage}</Text>
323
- ) : (
324
- <Text>
325
- <Text color="cyan"><Spinner type="dots" /></Text>
326
- {' '}{ollamaMessage}
327
- </Text>
328
- )}
329
- </Box>
330
- </Box>
331
- )}
332
-
333
- {/* Done */}
334
- {step === 'done' && selectedProvider && (
335
- <Box flexDirection="column">
336
- <Text><Text color="green">✓</Text> Provider: <Text bold>{selectedProvider.name}</Text></Text>
337
- {selectedProvider.needsApiKey && (
338
- <Text><Text color="green">✓</Text> API Key: <Text bold>configured</Text></Text>
339
- )}
340
- <Text><Text color="green">✓</Text> Model: <Text bold>{selectedProvider.models[modelIndex]?.name}</Text></Text>
341
- <Box marginTop={1}>
342
- <Text color="green" bold>Configuration saved! Run `cnapse` to start.</Text>
343
- </Box>
344
- </Box>
345
- )}
346
-
347
- <Box marginTop={2}>
348
- <Text color="gray" dimColor>Press Esc to cancel</Text>
349
- </Box>
350
- </Box>
351
- );
352
- }
@@ -1,202 +0,0 @@
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
- 'openai/gpt-5-nano', // $0.05/1M - Best budget + vision!
19
- 'google/gemini-2.5-flash-lite', // $0.10/1M - Fast reasoning
20
- 'qwen/qwen-2.5-coder-32b-instruct', // $0.07/1M - Best for coding
21
- 'meta-llama/llama-3.3-70b-instruct', // $0.10/1M
22
- ],
23
- anthropic: [
24
- 'claude-3-5-sonnet-20241022',
25
- 'claude-3-haiku-20240307',
26
- ],
27
- openai: ['gpt-4o-mini', 'gpt-3.5-turbo'],
28
- };
29
-
30
- export function Setup() {
31
- const { exit } = useApp();
32
- const [step, setStep] = useState<Step>('provider');
33
- const [selectedProvider, setSelectedProvider] = useState(0);
34
- const [selectedModel, setSelectedModel] = useState(0);
35
- const [apiKey, setApiKeyInput] = useState('');
36
- const [provider, setProviderState] = useState<string>('ollama');
37
-
38
- useInput((input, key) => {
39
- if (key.escape) {
40
- exit();
41
- return;
42
- }
43
-
44
- if (step === 'provider') {
45
- if (key.upArrow) {
46
- setSelectedProvider((prev) => (prev > 0 ? prev - 1 : PROVIDERS.length - 1));
47
- } else if (key.downArrow) {
48
- setSelectedProvider((prev) => (prev < PROVIDERS.length - 1 ? prev + 1 : 0));
49
- } else if (key.return) {
50
- const selected = PROVIDERS[selectedProvider]!;
51
- setProviderState(selected.id);
52
- setProvider(selected.id as any);
53
-
54
- if (selected.id === 'ollama') {
55
- setStep('model');
56
- } else {
57
- setStep('apiKey');
58
- }
59
- }
60
- } else if (step === 'model') {
61
- const models = DEFAULT_MODELS[provider] || [];
62
- if (key.upArrow) {
63
- setSelectedModel((prev) => (prev > 0 ? prev - 1 : models.length - 1));
64
- } else if (key.downArrow) {
65
- setSelectedModel((prev) => (prev < models.length - 1 ? prev + 1 : 0));
66
- } else if (key.return) {
67
- const model = models[selectedModel]!;
68
- setModel(model);
69
- setStep('done');
70
- setTimeout(() => exit(), 1500);
71
- }
72
- }
73
- });
74
-
75
- const handleApiKeySubmit = (value: string) => {
76
- if (value.trim()) {
77
- setApiKey(provider as any, value.trim());
78
- setApiKeyInput(value);
79
- setStep('model');
80
- }
81
- };
82
-
83
- return (
84
- <Box flexDirection="column" padding={1}>
85
- <Box marginBottom={1}>
86
- <Text bold color="cyan">
87
- 🚀 C-napse Setup
88
- </Text>
89
- </Box>
90
-
91
- {step === 'provider' && (
92
- <Box flexDirection="column">
93
- <Text bold>Select AI Provider:</Text>
94
- <Text color="gray" dimColor>
95
- (Use ↑↓ arrows, Enter to select)
96
- </Text>
97
- <Box marginTop={1} flexDirection="column">
98
- {PROVIDERS.map((p, i) => (
99
- <Box key={p.id}>
100
- <Text color={i === selectedProvider ? 'cyan' : 'white'}>
101
- {i === selectedProvider ? '❯ ' : ' '}
102
- <Text bold={i === selectedProvider}>{p.name}</Text>
103
- <Text color="gray"> - {p.desc}</Text>
104
- </Text>
105
- </Box>
106
- ))}
107
- </Box>
108
- </Box>
109
- )}
110
-
111
- {step === 'apiKey' && (
112
- <Box flexDirection="column">
113
- <Text>
114
- <Text color="green">✓</Text> Provider: <Text bold>{provider}</Text>
115
- </Text>
116
- <Box marginTop={1} flexDirection="column">
117
- <Text bold>Enter your {provider} API key:</Text>
118
- <Text color="gray" dimColor>
119
- (Paste with Ctrl+V or right-click, then Enter)
120
- </Text>
121
- <Box marginTop={1}>
122
- <Text color="cyan">❯ </Text>
123
- <TextInput
124
- value={apiKey}
125
- onChange={setApiKeyInput}
126
- onSubmit={handleApiKeySubmit}
127
- mask="*"
128
- />
129
- </Box>
130
- </Box>
131
- </Box>
132
- )}
133
-
134
- {step === 'model' && (
135
- <Box flexDirection="column">
136
- <Text>
137
- <Text color="green">✓</Text> Provider: <Text bold>{provider}</Text>
138
- </Text>
139
- {provider !== 'ollama' && (
140
- <Text>
141
- <Text color="green">✓</Text> API Key: <Text bold>saved</Text>
142
- </Text>
143
- )}
144
- <Box marginTop={1} flexDirection="column">
145
- <Text bold>Select Model:</Text>
146
- <Text color="gray" dimColor>
147
- (Use ↑↓ arrows, Enter to select)
148
- </Text>
149
- <Box marginTop={1} flexDirection="column">
150
- {(DEFAULT_MODELS[provider] || []).map((model, i) => (
151
- <Box key={model}>
152
- <Text color={i === selectedModel ? 'cyan' : 'white'}>
153
- {i === selectedModel ? '❯ ' : ' '}
154
- <Text bold={i === selectedModel}>{model}</Text>
155
- </Text>
156
- </Box>
157
- ))}
158
- </Box>
159
- </Box>
160
- </Box>
161
- )}
162
-
163
- {step === 'done' && (
164
- <Box flexDirection="column">
165
- <Text>
166
- <Text color="green">✓</Text> Provider: <Text bold>{provider}</Text>
167
- </Text>
168
- {provider !== 'ollama' && (
169
- <Text>
170
- <Text color="green">✓</Text> API Key: <Text bold>saved</Text>
171
- </Text>
172
- )}
173
- <Text>
174
- <Text color="green">✓</Text> Model:{' '}
175
- <Text bold>{DEFAULT_MODELS[provider]?.[selectedModel]}</Text>
176
- </Text>
177
- <Box marginTop={1}>
178
- <Text color="green" bold>
179
- ✅ Setup complete! Run `cnapse` to start chatting.
180
- </Text>
181
- </Box>
182
- {provider === 'ollama' && (
183
- <Box marginTop={1} flexDirection="column">
184
- <Text color="yellow">
185
- Note: Make sure the model is downloaded. Run:
186
- </Text>
187
- <Text color="cyan">
188
- {' '}ollama pull {DEFAULT_MODELS[provider]?.[selectedModel]}
189
- </Text>
190
- </Box>
191
- )}
192
- </Box>
193
- )}
194
-
195
- <Box marginTop={2}>
196
- <Text color="gray" dimColor>
197
- Press Esc to cancel
198
- </Text>
199
- </Box>
200
- </Box>
201
- );
202
- }
package/src/lib/screen.ts DELETED
@@ -1,118 +0,0 @@
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
- }