@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.
- package/dist/ProviderSelector-MXRZFAOB.js +6 -0
- package/dist/chunk-OPX7FFL6.js +391 -0
- package/dist/index.js +882 -525
- package/package.json +17 -16
- package/src/agents/executor.ts +20 -13
- package/src/index.tsx +32 -6
- package/src/lib/tasks.ts +451 -74
- package/src/services/browser.ts +669 -0
- package/src/tools/index.ts +0 -1
- package/dist/ConfigUI-I2CJVODT.js +0 -305
- package/dist/Setup-KGYXCA7Y.js +0 -177
- package/dist/chunk-COKO6V5J.js +0 -50
- package/src/components/ConfigUI.tsx +0 -352
- package/src/components/Setup.tsx +0 -202
- package/src/lib/screen.ts +0 -118
- package/src/tools/vision.ts +0 -65
|
@@ -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
|
-
}
|
package/src/components/Setup.tsx
DELETED
|
@@ -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
|
-
}
|