@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,176 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import { getConfig, setProvider, setModel } from '../lib/config.js';
|
|
4
|
+
|
|
5
|
+
interface ProviderSelectorProps {
|
|
6
|
+
onClose: () => void;
|
|
7
|
+
onSelect: (provider: string, model: string) => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface ProviderOption {
|
|
11
|
+
id: 'ollama' | 'openrouter' | 'anthropic' | 'openai';
|
|
12
|
+
name: string;
|
|
13
|
+
description: string;
|
|
14
|
+
defaultModel: string;
|
|
15
|
+
models: string[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const PROVIDERS: ProviderOption[] = [
|
|
19
|
+
{
|
|
20
|
+
id: 'ollama',
|
|
21
|
+
name: 'Ollama',
|
|
22
|
+
description: 'Local AI - Free, private, no API key',
|
|
23
|
+
defaultModel: 'qwen2.5:0.5b',
|
|
24
|
+
models: ['qwen2.5:0.5b', 'qwen2.5:1.5b', 'qwen2.5:7b', 'llama3.2:1b', 'llama3.2:3b', 'mistral:7b', 'codellama:7b', 'llava:7b'],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: 'openrouter',
|
|
28
|
+
name: 'OpenRouter',
|
|
29
|
+
description: 'Many models, pay-per-use',
|
|
30
|
+
defaultModel: 'qwen/qwen-2.5-coder-32b-instruct',
|
|
31
|
+
models: [
|
|
32
|
+
'qwen/qwen-2.5-coder-32b-instruct',
|
|
33
|
+
'anthropic/claude-3.5-sonnet',
|
|
34
|
+
'openai/gpt-4o',
|
|
35
|
+
'openai/gpt-4o-mini',
|
|
36
|
+
'google/gemini-pro-1.5',
|
|
37
|
+
'meta-llama/llama-3.1-70b-instruct',
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: 'anthropic',
|
|
42
|
+
name: 'Anthropic',
|
|
43
|
+
description: 'Claude models - Best for coding',
|
|
44
|
+
defaultModel: 'claude-3-5-sonnet-20241022',
|
|
45
|
+
models: ['claude-3-5-sonnet-20241022', 'claude-3-opus-20240229', 'claude-3-haiku-20240307'],
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: 'openai',
|
|
49
|
+
name: 'OpenAI',
|
|
50
|
+
description: 'GPT models',
|
|
51
|
+
defaultModel: 'gpt-4o',
|
|
52
|
+
models: ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'gpt-3.5-turbo'],
|
|
53
|
+
},
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
type SelectionMode = 'provider' | 'model';
|
|
57
|
+
|
|
58
|
+
export function ProviderSelector({ onClose, onSelect }: ProviderSelectorProps) {
|
|
59
|
+
const config = getConfig();
|
|
60
|
+
const [mode, setMode] = useState<SelectionMode>('provider');
|
|
61
|
+
const [providerIndex, setProviderIndex] = useState(() => {
|
|
62
|
+
const idx = PROVIDERS.findIndex(p => p.id === config.provider);
|
|
63
|
+
return idx >= 0 ? idx : 0;
|
|
64
|
+
});
|
|
65
|
+
const [modelIndex, setModelIndex] = useState(0);
|
|
66
|
+
const [selectedProvider, setSelectedProvider] = useState<ProviderOption | null>(null);
|
|
67
|
+
|
|
68
|
+
useInput((input, key) => {
|
|
69
|
+
if (key.escape) {
|
|
70
|
+
onClose();
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (mode === 'provider') {
|
|
75
|
+
if (key.upArrow) {
|
|
76
|
+
setProviderIndex(prev => (prev > 0 ? prev - 1 : PROVIDERS.length - 1));
|
|
77
|
+
} else if (key.downArrow) {
|
|
78
|
+
setProviderIndex(prev => (prev < PROVIDERS.length - 1 ? prev + 1 : 0));
|
|
79
|
+
} else if (key.return) {
|
|
80
|
+
const provider = PROVIDERS[providerIndex]!;
|
|
81
|
+
setSelectedProvider(provider);
|
|
82
|
+
// Find current model index if it exists in the provider's models
|
|
83
|
+
const currentModelIdx = provider.models.findIndex(m => m === config.model);
|
|
84
|
+
setModelIndex(currentModelIdx >= 0 ? currentModelIdx : 0);
|
|
85
|
+
setMode('model');
|
|
86
|
+
}
|
|
87
|
+
} else if (mode === 'model' && selectedProvider) {
|
|
88
|
+
if (key.upArrow) {
|
|
89
|
+
setModelIndex(prev => (prev > 0 ? prev - 1 : selectedProvider.models.length - 1));
|
|
90
|
+
} else if (key.downArrow) {
|
|
91
|
+
setModelIndex(prev => (prev < selectedProvider.models.length - 1 ? prev + 1 : 0));
|
|
92
|
+
} else if (key.return) {
|
|
93
|
+
const model = selectedProvider.models[modelIndex]!;
|
|
94
|
+
// Save to config
|
|
95
|
+
setProvider(selectedProvider.id);
|
|
96
|
+
setModel(model);
|
|
97
|
+
onSelect(selectedProvider.id, model);
|
|
98
|
+
onClose();
|
|
99
|
+
} else if (key.leftArrow || input === 'b') {
|
|
100
|
+
setMode('provider');
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
if (mode === 'provider') {
|
|
106
|
+
return (
|
|
107
|
+
<Box flexDirection="column" borderStyle="round" borderColor="cyan" padding={1}>
|
|
108
|
+
<Box marginBottom={1}>
|
|
109
|
+
<Text bold color="cyan">Select Provider</Text>
|
|
110
|
+
</Box>
|
|
111
|
+
<Box marginBottom={1}>
|
|
112
|
+
<Text color="gray">Use arrows to navigate, Enter to select, Esc to cancel</Text>
|
|
113
|
+
</Box>
|
|
114
|
+
|
|
115
|
+
{PROVIDERS.map((provider, index) => {
|
|
116
|
+
const isSelected = index === providerIndex;
|
|
117
|
+
const isCurrent = provider.id === config.provider;
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<Box key={provider.id} marginY={0}>
|
|
121
|
+
<Text color={isSelected ? 'cyan' : 'white'}>
|
|
122
|
+
{isSelected ? '❯ ' : ' '}
|
|
123
|
+
{provider.name}
|
|
124
|
+
{isCurrent && <Text color="green"> (current)</Text>}
|
|
125
|
+
</Text>
|
|
126
|
+
{isSelected && (
|
|
127
|
+
<Text color="gray"> - {provider.description}</Text>
|
|
128
|
+
)}
|
|
129
|
+
</Box>
|
|
130
|
+
);
|
|
131
|
+
})}
|
|
132
|
+
|
|
133
|
+
<Box marginTop={1} borderStyle="single" borderColor="gray" paddingX={1}>
|
|
134
|
+
<Text color="gray">
|
|
135
|
+
Current: {config.provider} / {config.model}
|
|
136
|
+
</Text>
|
|
137
|
+
</Box>
|
|
138
|
+
</Box>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Model selection mode
|
|
143
|
+
return (
|
|
144
|
+
<Box flexDirection="column" borderStyle="round" borderColor="cyan" padding={1}>
|
|
145
|
+
<Box marginBottom={1}>
|
|
146
|
+
<Text bold color="cyan">Select Model for {selectedProvider?.name}</Text>
|
|
147
|
+
</Box>
|
|
148
|
+
<Box marginBottom={1}>
|
|
149
|
+
<Text color="gray">Arrows to navigate, Enter to select, B/Left to go back</Text>
|
|
150
|
+
</Box>
|
|
151
|
+
|
|
152
|
+
{selectedProvider?.models.map((model, index) => {
|
|
153
|
+
const isSelected = index === modelIndex;
|
|
154
|
+
const isCurrent = model === config.model && selectedProvider.id === config.provider;
|
|
155
|
+
const isDefault = model === selectedProvider.defaultModel;
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<Box key={model} marginY={0}>
|
|
159
|
+
<Text color={isSelected ? 'cyan' : 'white'}>
|
|
160
|
+
{isSelected ? '❯ ' : ' '}
|
|
161
|
+
{model}
|
|
162
|
+
{isCurrent && <Text color="green"> (current)</Text>}
|
|
163
|
+
{isDefault && !isCurrent && <Text color="yellow"> (default)</Text>}
|
|
164
|
+
</Text>
|
|
165
|
+
</Box>
|
|
166
|
+
);
|
|
167
|
+
})}
|
|
168
|
+
|
|
169
|
+
<Box marginTop={1} borderStyle="single" borderColor="gray" paddingX={1}>
|
|
170
|
+
<Text color="gray">
|
|
171
|
+
Provider: {selectedProvider?.name}
|
|
172
|
+
</Text>
|
|
173
|
+
</Box>
|
|
174
|
+
</Box>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
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
|
+
'qwen/qwen-2.5-coder-32b-instruct',
|
|
19
|
+
'anthropic/claude-3.5-sonnet',
|
|
20
|
+
'openai/gpt-4o',
|
|
21
|
+
'google/gemini-pro-1.5',
|
|
22
|
+
],
|
|
23
|
+
anthropic: [
|
|
24
|
+
'claude-3-5-sonnet-20241022',
|
|
25
|
+
'claude-3-opus-20240229',
|
|
26
|
+
'claude-3-haiku-20240307',
|
|
27
|
+
],
|
|
28
|
+
openai: ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'gpt-3.5-turbo'],
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export function Setup() {
|
|
32
|
+
const { exit } = useApp();
|
|
33
|
+
const [step, setStep] = useState<Step>('provider');
|
|
34
|
+
const [selectedProvider, setSelectedProvider] = useState(0);
|
|
35
|
+
const [selectedModel, setSelectedModel] = useState(0);
|
|
36
|
+
const [apiKey, setApiKeyInput] = useState('');
|
|
37
|
+
const [provider, setProviderState] = useState<string>('ollama');
|
|
38
|
+
|
|
39
|
+
useInput((input, key) => {
|
|
40
|
+
if (key.escape) {
|
|
41
|
+
exit();
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (step === 'provider') {
|
|
46
|
+
if (key.upArrow) {
|
|
47
|
+
setSelectedProvider((prev) => (prev > 0 ? prev - 1 : PROVIDERS.length - 1));
|
|
48
|
+
} else if (key.downArrow) {
|
|
49
|
+
setSelectedProvider((prev) => (prev < PROVIDERS.length - 1 ? prev + 1 : 0));
|
|
50
|
+
} else if (key.return) {
|
|
51
|
+
const selected = PROVIDERS[selectedProvider]!;
|
|
52
|
+
setProviderState(selected.id);
|
|
53
|
+
setProvider(selected.id as any);
|
|
54
|
+
|
|
55
|
+
if (selected.id === 'ollama') {
|
|
56
|
+
setStep('model');
|
|
57
|
+
} else {
|
|
58
|
+
setStep('apiKey');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
} else if (step === 'model') {
|
|
62
|
+
const models = DEFAULT_MODELS[provider] || [];
|
|
63
|
+
if (key.upArrow) {
|
|
64
|
+
setSelectedModel((prev) => (prev > 0 ? prev - 1 : models.length - 1));
|
|
65
|
+
} else if (key.downArrow) {
|
|
66
|
+
setSelectedModel((prev) => (prev < models.length - 1 ? prev + 1 : 0));
|
|
67
|
+
} else if (key.return) {
|
|
68
|
+
const model = models[selectedModel]!;
|
|
69
|
+
setModel(model);
|
|
70
|
+
setStep('done');
|
|
71
|
+
setTimeout(() => exit(), 1500);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const handleApiKeySubmit = (value: string) => {
|
|
77
|
+
if (value.trim()) {
|
|
78
|
+
setApiKey(provider as any, value.trim());
|
|
79
|
+
setApiKeyInput(value);
|
|
80
|
+
setStep('model');
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<Box flexDirection="column" padding={1}>
|
|
86
|
+
<Box marginBottom={1}>
|
|
87
|
+
<Text bold color="cyan">
|
|
88
|
+
🚀 C-napse Setup
|
|
89
|
+
</Text>
|
|
90
|
+
</Box>
|
|
91
|
+
|
|
92
|
+
{step === 'provider' && (
|
|
93
|
+
<Box flexDirection="column">
|
|
94
|
+
<Text bold>Select AI Provider:</Text>
|
|
95
|
+
<Text color="gray" dimColor>
|
|
96
|
+
(Use ↑↓ arrows, Enter to select)
|
|
97
|
+
</Text>
|
|
98
|
+
<Box marginTop={1} flexDirection="column">
|
|
99
|
+
{PROVIDERS.map((p, i) => (
|
|
100
|
+
<Box key={p.id}>
|
|
101
|
+
<Text color={i === selectedProvider ? 'cyan' : 'white'}>
|
|
102
|
+
{i === selectedProvider ? '❯ ' : ' '}
|
|
103
|
+
<Text bold={i === selectedProvider}>{p.name}</Text>
|
|
104
|
+
<Text color="gray"> - {p.desc}</Text>
|
|
105
|
+
</Text>
|
|
106
|
+
</Box>
|
|
107
|
+
))}
|
|
108
|
+
</Box>
|
|
109
|
+
</Box>
|
|
110
|
+
)}
|
|
111
|
+
|
|
112
|
+
{step === 'apiKey' && (
|
|
113
|
+
<Box flexDirection="column">
|
|
114
|
+
<Text>
|
|
115
|
+
<Text color="green">✓</Text> Provider: <Text bold>{provider}</Text>
|
|
116
|
+
</Text>
|
|
117
|
+
<Box marginTop={1} flexDirection="column">
|
|
118
|
+
<Text bold>Enter your {provider} API key:</Text>
|
|
119
|
+
<Text color="gray" dimColor>
|
|
120
|
+
(Paste with Ctrl+V or right-click, then Enter)
|
|
121
|
+
</Text>
|
|
122
|
+
<Box marginTop={1}>
|
|
123
|
+
<Text color="cyan">❯ </Text>
|
|
124
|
+
<TextInput
|
|
125
|
+
value={apiKey}
|
|
126
|
+
onChange={setApiKeyInput}
|
|
127
|
+
onSubmit={handleApiKeySubmit}
|
|
128
|
+
mask="*"
|
|
129
|
+
/>
|
|
130
|
+
</Box>
|
|
131
|
+
</Box>
|
|
132
|
+
</Box>
|
|
133
|
+
)}
|
|
134
|
+
|
|
135
|
+
{step === 'model' && (
|
|
136
|
+
<Box flexDirection="column">
|
|
137
|
+
<Text>
|
|
138
|
+
<Text color="green">✓</Text> Provider: <Text bold>{provider}</Text>
|
|
139
|
+
</Text>
|
|
140
|
+
{provider !== 'ollama' && (
|
|
141
|
+
<Text>
|
|
142
|
+
<Text color="green">✓</Text> API Key: <Text bold>saved</Text>
|
|
143
|
+
</Text>
|
|
144
|
+
)}
|
|
145
|
+
<Box marginTop={1} flexDirection="column">
|
|
146
|
+
<Text bold>Select Model:</Text>
|
|
147
|
+
<Text color="gray" dimColor>
|
|
148
|
+
(Use ↑↓ arrows, Enter to select)
|
|
149
|
+
</Text>
|
|
150
|
+
<Box marginTop={1} flexDirection="column">
|
|
151
|
+
{(DEFAULT_MODELS[provider] || []).map((model, i) => (
|
|
152
|
+
<Box key={model}>
|
|
153
|
+
<Text color={i === selectedModel ? 'cyan' : 'white'}>
|
|
154
|
+
{i === selectedModel ? '❯ ' : ' '}
|
|
155
|
+
<Text bold={i === selectedModel}>{model}</Text>
|
|
156
|
+
</Text>
|
|
157
|
+
</Box>
|
|
158
|
+
))}
|
|
159
|
+
</Box>
|
|
160
|
+
</Box>
|
|
161
|
+
</Box>
|
|
162
|
+
)}
|
|
163
|
+
|
|
164
|
+
{step === 'done' && (
|
|
165
|
+
<Box flexDirection="column">
|
|
166
|
+
<Text>
|
|
167
|
+
<Text color="green">✓</Text> Provider: <Text bold>{provider}</Text>
|
|
168
|
+
</Text>
|
|
169
|
+
{provider !== 'ollama' && (
|
|
170
|
+
<Text>
|
|
171
|
+
<Text color="green">✓</Text> API Key: <Text bold>saved</Text>
|
|
172
|
+
</Text>
|
|
173
|
+
)}
|
|
174
|
+
<Text>
|
|
175
|
+
<Text color="green">✓</Text> Model:{' '}
|
|
176
|
+
<Text bold>{DEFAULT_MODELS[provider]?.[selectedModel]}</Text>
|
|
177
|
+
</Text>
|
|
178
|
+
<Box marginTop={1}>
|
|
179
|
+
<Text color="green" bold>
|
|
180
|
+
✅ Setup complete! Run `cnapse` to start chatting.
|
|
181
|
+
</Text>
|
|
182
|
+
</Box>
|
|
183
|
+
{provider === 'ollama' && (
|
|
184
|
+
<Box marginTop={1} flexDirection="column">
|
|
185
|
+
<Text color="yellow">
|
|
186
|
+
Note: Make sure the model is downloaded. Run:
|
|
187
|
+
</Text>
|
|
188
|
+
<Text color="cyan">
|
|
189
|
+
{' '}ollama pull {DEFAULT_MODELS[provider]?.[selectedModel]}
|
|
190
|
+
</Text>
|
|
191
|
+
</Box>
|
|
192
|
+
)}
|
|
193
|
+
</Box>
|
|
194
|
+
)}
|
|
195
|
+
|
|
196
|
+
<Box marginTop={2}>
|
|
197
|
+
<Text color="gray" dimColor>
|
|
198
|
+
Press Esc to cancel
|
|
199
|
+
</Text>
|
|
200
|
+
</Box>
|
|
201
|
+
</Box>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import type { Task, TaskStep } from '../lib/tasks.js';
|
|
4
|
+
|
|
5
|
+
interface TaskProgressProps {
|
|
6
|
+
task: Task;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const statusEmoji: Record<Task['status'], string> = {
|
|
10
|
+
pending: '⏳',
|
|
11
|
+
running: '🔄',
|
|
12
|
+
completed: '✅',
|
|
13
|
+
failed: '❌',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const stepStatusEmoji: Record<TaskStep['status'], string> = {
|
|
17
|
+
pending: '○',
|
|
18
|
+
running: '◐',
|
|
19
|
+
completed: '●',
|
|
20
|
+
failed: '✗',
|
|
21
|
+
skipped: '◌',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const stepStatusColor: Record<TaskStep['status'], string> = {
|
|
25
|
+
pending: 'gray',
|
|
26
|
+
running: 'yellow',
|
|
27
|
+
completed: 'green',
|
|
28
|
+
failed: 'red',
|
|
29
|
+
skipped: 'gray',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export function TaskProgress({ task }: TaskProgressProps) {
|
|
33
|
+
return (
|
|
34
|
+
<Box flexDirection="column" borderStyle="round" borderColor="cyan" padding={1} marginY={1}>
|
|
35
|
+
<Box marginBottom={1}>
|
|
36
|
+
<Text bold>
|
|
37
|
+
{statusEmoji[task.status]} Task: {task.description}
|
|
38
|
+
</Text>
|
|
39
|
+
</Box>
|
|
40
|
+
|
|
41
|
+
{task.steps.map((step, index) => (
|
|
42
|
+
<Box key={step.id} marginLeft={2}>
|
|
43
|
+
<Text color={stepStatusColor[step.status]}>
|
|
44
|
+
{stepStatusEmoji[step.status]} {step.description}
|
|
45
|
+
</Text>
|
|
46
|
+
{step.result && (
|
|
47
|
+
<Text color="gray" dimColor> → {step.result}</Text>
|
|
48
|
+
)}
|
|
49
|
+
{step.error && (
|
|
50
|
+
<Text color="red"> (Error: {step.error})</Text>
|
|
51
|
+
)}
|
|
52
|
+
</Box>
|
|
53
|
+
))}
|
|
54
|
+
|
|
55
|
+
{task.status === 'completed' && (
|
|
56
|
+
<Box marginTop={1}>
|
|
57
|
+
<Text color="green">✓ Task completed</Text>
|
|
58
|
+
</Box>
|
|
59
|
+
)}
|
|
60
|
+
|
|
61
|
+
{task.status === 'failed' && (
|
|
62
|
+
<Box marginTop={1}>
|
|
63
|
+
<Text color="red">✗ Task failed</Text>
|
|
64
|
+
</Box>
|
|
65
|
+
)}
|
|
66
|
+
</Box>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom hooks for C-napse
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { useChat } from './useChat.js';
|
|
6
|
+
export type { ChatMessage, UseChatResult } from './useChat.js';
|
|
7
|
+
|
|
8
|
+
export { useVision } from './useVision.js';
|
|
9
|
+
export type { UseVisionResult } from './useVision.js';
|
|
10
|
+
|
|
11
|
+
export { useTelegram } from './useTelegram.js';
|
|
12
|
+
export type { UseTelegramResult } from './useTelegram.js';
|
|
13
|
+
|
|
14
|
+
export { useTasks } from './useTasks.js';
|
|
15
|
+
export type { UseTasksResult } from './useTasks.js';
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat Hook - AI conversation management
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
6
|
+
import { chat, Message } from '../lib/api.js';
|
|
7
|
+
import { getScreenDescription } from '../lib/screen.js';
|
|
8
|
+
|
|
9
|
+
export interface ChatMessage {
|
|
10
|
+
id: string;
|
|
11
|
+
role: 'user' | 'assistant' | 'system';
|
|
12
|
+
content: string;
|
|
13
|
+
timestamp: Date;
|
|
14
|
+
isStreaming?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface UseChatResult {
|
|
18
|
+
messages: ChatMessage[];
|
|
19
|
+
isProcessing: boolean;
|
|
20
|
+
error: string | null;
|
|
21
|
+
sendMessage: (content: string) => Promise<void>;
|
|
22
|
+
addSystemMessage: (content: string) => void;
|
|
23
|
+
clearMessages: () => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const WELCOME_MESSAGE: ChatMessage = {
|
|
27
|
+
id: '0',
|
|
28
|
+
role: 'system',
|
|
29
|
+
content: 'Welcome to C-napse! Type your message and press Enter.\n\nShortcuts: Ctrl+H for help, Ctrl+P for provider',
|
|
30
|
+
timestamp: new Date(),
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export function useChat(screenWatch: boolean = false): UseChatResult {
|
|
34
|
+
const [messages, setMessages] = useState<ChatMessage[]>([WELCOME_MESSAGE]);
|
|
35
|
+
const [isProcessing, setIsProcessing] = useState(false);
|
|
36
|
+
const [error, setError] = useState<string | null>(null);
|
|
37
|
+
const screenContextRef = useRef<string | null>(null);
|
|
38
|
+
|
|
39
|
+
// Screen watching effect
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (!screenWatch) {
|
|
42
|
+
screenContextRef.current = null;
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const checkScreen = async () => {
|
|
47
|
+
const desc = await getScreenDescription();
|
|
48
|
+
if (desc) {
|
|
49
|
+
screenContextRef.current = desc;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
checkScreen();
|
|
54
|
+
const interval = setInterval(checkScreen, 5000);
|
|
55
|
+
return () => clearInterval(interval);
|
|
56
|
+
}, [screenWatch]);
|
|
57
|
+
|
|
58
|
+
const addSystemMessage = useCallback((content: string) => {
|
|
59
|
+
setMessages(prev => [
|
|
60
|
+
...prev,
|
|
61
|
+
{
|
|
62
|
+
id: Date.now().toString(),
|
|
63
|
+
role: 'system',
|
|
64
|
+
content,
|
|
65
|
+
timestamp: new Date(),
|
|
66
|
+
},
|
|
67
|
+
]);
|
|
68
|
+
}, []);
|
|
69
|
+
|
|
70
|
+
const sendMessage = useCallback(async (content: string) => {
|
|
71
|
+
if (!content.trim() || isProcessing) return;
|
|
72
|
+
|
|
73
|
+
setError(null);
|
|
74
|
+
|
|
75
|
+
// Add user message
|
|
76
|
+
const userMsg: ChatMessage = {
|
|
77
|
+
id: Date.now().toString(),
|
|
78
|
+
role: 'user',
|
|
79
|
+
content,
|
|
80
|
+
timestamp: new Date(),
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// Add assistant placeholder
|
|
84
|
+
const assistantId = (Date.now() + 1).toString();
|
|
85
|
+
const assistantMsg: ChatMessage = {
|
|
86
|
+
id: assistantId,
|
|
87
|
+
role: 'assistant',
|
|
88
|
+
content: '',
|
|
89
|
+
timestamp: new Date(),
|
|
90
|
+
isStreaming: true,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
setMessages(prev => [...prev, userMsg, assistantMsg]);
|
|
94
|
+
setIsProcessing(true);
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
// Build message history
|
|
98
|
+
const apiMessages: Message[] = messages
|
|
99
|
+
.filter(m => m.role === 'user' || m.role === 'assistant')
|
|
100
|
+
.slice(-10)
|
|
101
|
+
.map(m => ({ role: m.role as 'user' | 'assistant', content: m.content }));
|
|
102
|
+
|
|
103
|
+
// Add screen context if watching
|
|
104
|
+
let finalContent = content;
|
|
105
|
+
if (screenWatch && screenContextRef.current) {
|
|
106
|
+
finalContent = `[Screen context: ${screenContextRef.current}]\n\n${content}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
apiMessages.push({ role: 'user', content: finalContent });
|
|
110
|
+
|
|
111
|
+
const response = await chat(apiMessages);
|
|
112
|
+
|
|
113
|
+
// Update assistant message
|
|
114
|
+
setMessages(prev =>
|
|
115
|
+
prev.map(m =>
|
|
116
|
+
m.id === assistantId
|
|
117
|
+
? { ...m, content: response.content || '(no response)', isStreaming: false }
|
|
118
|
+
: m
|
|
119
|
+
)
|
|
120
|
+
);
|
|
121
|
+
} catch (err) {
|
|
122
|
+
const errorMsg = err instanceof Error ? err.message : 'Unknown error';
|
|
123
|
+
setError(errorMsg);
|
|
124
|
+
setMessages(prev =>
|
|
125
|
+
prev.map(m =>
|
|
126
|
+
m.id === assistantId
|
|
127
|
+
? { ...m, content: `Error: ${errorMsg}`, isStreaming: false }
|
|
128
|
+
: m
|
|
129
|
+
)
|
|
130
|
+
);
|
|
131
|
+
} finally {
|
|
132
|
+
setIsProcessing(false);
|
|
133
|
+
}
|
|
134
|
+
}, [messages, isProcessing, screenWatch]);
|
|
135
|
+
|
|
136
|
+
const clearMessages = useCallback(() => {
|
|
137
|
+
setMessages([WELCOME_MESSAGE]);
|
|
138
|
+
setError(null);
|
|
139
|
+
}, []);
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
messages,
|
|
143
|
+
isProcessing,
|
|
144
|
+
error,
|
|
145
|
+
sendMessage,
|
|
146
|
+
addSystemMessage,
|
|
147
|
+
clearMessages,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tasks Hook - Multi-step task automation
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { useState, useCallback } from 'react';
|
|
6
|
+
import { parseTask, executeTask, formatTask, getTaskMemoryStats, clearTaskMemory, Task, TaskStep } from '../lib/tasks.js';
|
|
7
|
+
|
|
8
|
+
export interface UseTasksResult {
|
|
9
|
+
isRunning: boolean;
|
|
10
|
+
currentTask: Task | null;
|
|
11
|
+
currentStep: TaskStep | null;
|
|
12
|
+
error: string | null;
|
|
13
|
+
run: (description: string) => Promise<Task>;
|
|
14
|
+
format: (task: Task) => string;
|
|
15
|
+
getMemoryStats: () => { patternCount: number; totalUses: number; topPatterns: string[] };
|
|
16
|
+
clearMemory: () => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function useTasks(onProgress?: (task: Task, step: TaskStep) => void): UseTasksResult {
|
|
20
|
+
const [isRunning, setIsRunning] = useState(false);
|
|
21
|
+
const [currentTask, setCurrentTask] = useState<Task | null>(null);
|
|
22
|
+
const [currentStep, setCurrentStep] = useState<TaskStep | null>(null);
|
|
23
|
+
const [error, setError] = useState<string | null>(null);
|
|
24
|
+
|
|
25
|
+
const run = useCallback(async (description: string): Promise<Task> => {
|
|
26
|
+
setIsRunning(true);
|
|
27
|
+
setError(null);
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
// Parse the task
|
|
31
|
+
const task = await parseTask(description);
|
|
32
|
+
setCurrentTask(task);
|
|
33
|
+
|
|
34
|
+
// Execute with progress callback
|
|
35
|
+
const result = await executeTask(task, (updatedTask, step) => {
|
|
36
|
+
setCurrentTask({ ...updatedTask });
|
|
37
|
+
setCurrentStep(step);
|
|
38
|
+
onProgress?.(updatedTask, step);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
setCurrentTask(result);
|
|
42
|
+
return result;
|
|
43
|
+
} catch (err) {
|
|
44
|
+
const errorMsg = err instanceof Error ? err.message : 'Task failed';
|
|
45
|
+
setError(errorMsg);
|
|
46
|
+
throw err;
|
|
47
|
+
} finally {
|
|
48
|
+
setIsRunning(false);
|
|
49
|
+
setCurrentStep(null);
|
|
50
|
+
}
|
|
51
|
+
}, [onProgress]);
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
isRunning,
|
|
55
|
+
currentTask,
|
|
56
|
+
currentStep,
|
|
57
|
+
error,
|
|
58
|
+
run,
|
|
59
|
+
format: formatTask,
|
|
60
|
+
getMemoryStats: getTaskMemoryStats,
|
|
61
|
+
clearMemory: clearTaskMemory,
|
|
62
|
+
};
|
|
63
|
+
}
|