@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
package/dist/index.js
CHANGED
|
@@ -1,55 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
getApiKey,
|
|
4
|
+
getConfig,
|
|
5
|
+
setApiKey,
|
|
6
|
+
setModel,
|
|
7
|
+
setProvider
|
|
8
|
+
} from "./chunk-COKO6V5J.js";
|
|
2
9
|
|
|
3
10
|
// src/index.tsx
|
|
4
11
|
import { render } from "ink";
|
|
5
12
|
|
|
6
13
|
// src/components/App.tsx
|
|
7
|
-
import { useState } from "react";
|
|
8
|
-
import { Box as
|
|
14
|
+
import { useState as useState7, useCallback as useCallback5 } from "react";
|
|
15
|
+
import { Box as Box7, Text as Text7, useApp, useInput as useInput3 } from "ink";
|
|
9
16
|
|
|
10
17
|
// src/components/Header.tsx
|
|
11
18
|
import { Box, Text } from "ink";
|
|
12
|
-
|
|
13
|
-
// src/lib/config.ts
|
|
14
|
-
import Conf from "conf";
|
|
15
|
-
var config = new Conf({
|
|
16
|
-
projectName: "cnapse",
|
|
17
|
-
defaults: {
|
|
18
|
-
provider: "ollama",
|
|
19
|
-
model: "qwen2.5:0.5b",
|
|
20
|
-
apiKeys: {},
|
|
21
|
-
ollamaHost: "http://localhost:11434",
|
|
22
|
-
openrouter: {
|
|
23
|
-
siteUrl: "https://github.com/projectservan8n/C-napse",
|
|
24
|
-
appName: "C-napse"
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
});
|
|
28
|
-
function getConfig() {
|
|
29
|
-
return {
|
|
30
|
-
provider: config.get("provider"),
|
|
31
|
-
model: config.get("model"),
|
|
32
|
-
apiKeys: config.get("apiKeys"),
|
|
33
|
-
ollamaHost: config.get("ollamaHost"),
|
|
34
|
-
openrouter: config.get("openrouter")
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
function setProvider(provider) {
|
|
38
|
-
config.set("provider", provider);
|
|
39
|
-
}
|
|
40
|
-
function setModel(model) {
|
|
41
|
-
config.set("model", model);
|
|
42
|
-
}
|
|
43
|
-
function setApiKey(provider, key) {
|
|
44
|
-
const keys = config.get("apiKeys");
|
|
45
|
-
keys[provider] = key;
|
|
46
|
-
config.set("apiKeys", keys);
|
|
47
|
-
}
|
|
48
|
-
function getApiKey(provider) {
|
|
49
|
-
return config.get("apiKeys")[provider];
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// src/components/Header.tsx
|
|
53
19
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
54
20
|
var ASCII_BANNER = `
|
|
55
21
|
\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557
|
|
@@ -59,15 +25,18 @@ var ASCII_BANNER = `
|
|
|
59
25
|
\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557
|
|
60
26
|
\u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D
|
|
61
27
|
`.trim();
|
|
62
|
-
function Header() {
|
|
63
|
-
const
|
|
28
|
+
function Header({ screenWatch = false, telegramEnabled = false }) {
|
|
29
|
+
const config = getConfig();
|
|
64
30
|
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
|
|
65
31
|
/* @__PURE__ */ jsx(Text, { color: "cyan", children: ASCII_BANNER }),
|
|
66
32
|
/* @__PURE__ */ jsx(Box, { justifyContent: "center", children: /* @__PURE__ */ jsxs(Text, { color: "gray", children: [
|
|
67
|
-
|
|
33
|
+
config.provider,
|
|
68
34
|
" \u2502 ",
|
|
69
|
-
|
|
70
|
-
|
|
35
|
+
config.model,
|
|
36
|
+
screenWatch && /* @__PURE__ */ jsx(Text, { color: "yellow", children: " \u2502 \u{1F5A5}\uFE0F Screen" }),
|
|
37
|
+
telegramEnabled && /* @__PURE__ */ jsx(Text, { color: "blue", children: " \u2502 \u{1F4F1} Telegram" })
|
|
38
|
+
] }) }),
|
|
39
|
+
/* @__PURE__ */ jsx(Box, { justifyContent: "center", children: /* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: "Press Ctrl+H for help" }) })
|
|
71
40
|
] });
|
|
72
41
|
}
|
|
73
42
|
|
|
@@ -129,6 +98,236 @@ function StatusBar({ status }) {
|
|
|
129
98
|
return /* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsx4(Text4, { backgroundColor: "gray", color: "white", children: ` ${status} \u2502 Ctrl+C: Exit \u2502 Enter: Send ` }) });
|
|
130
99
|
}
|
|
131
100
|
|
|
101
|
+
// src/components/HelpMenu.tsx
|
|
102
|
+
import React, { useState } from "react";
|
|
103
|
+
import { Box as Box5, Text as Text5, useInput } from "ink";
|
|
104
|
+
import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
105
|
+
var MENU_ITEMS = [
|
|
106
|
+
// Navigation
|
|
107
|
+
{ command: "/help", shortcut: "Ctrl+H", description: "Show this help menu", category: "navigation" },
|
|
108
|
+
{ command: "/clear", shortcut: "Ctrl+L", description: "Clear chat history", category: "navigation" },
|
|
109
|
+
{ command: "/quit", shortcut: "Ctrl+C", description: "Exit C-napse", category: "navigation" },
|
|
110
|
+
// Actions
|
|
111
|
+
{ command: "/screen", shortcut: "Ctrl+S", description: "Take screenshot and describe", category: "actions" },
|
|
112
|
+
{ command: "/task", description: "Run multi-step task", category: "actions" },
|
|
113
|
+
{ command: "/telegram", shortcut: "Ctrl+T", description: "Toggle Telegram bot", category: "actions" },
|
|
114
|
+
{ command: "/memory", description: "View/clear learned task patterns", category: "actions" },
|
|
115
|
+
// Settings
|
|
116
|
+
{ command: "/config", description: "Show/edit configuration", category: "settings" },
|
|
117
|
+
{ command: "/watch", shortcut: "Ctrl+W", description: "Toggle screen watching", category: "settings" },
|
|
118
|
+
{ command: "/model", description: "Change AI model", category: "settings" },
|
|
119
|
+
{ command: "/provider", shortcut: "Ctrl+P", description: "Change AI provider", category: "settings" }
|
|
120
|
+
];
|
|
121
|
+
function HelpMenu({ onClose, onSelect }) {
|
|
122
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
123
|
+
const [selectedCategory, setSelectedCategory] = useState("all");
|
|
124
|
+
const filteredItems = selectedCategory === "all" ? MENU_ITEMS : MENU_ITEMS.filter((item) => item.category === selectedCategory);
|
|
125
|
+
useInput((input, key) => {
|
|
126
|
+
if (key.escape) {
|
|
127
|
+
onClose();
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (key.upArrow) {
|
|
131
|
+
setSelectedIndex((prev) => prev > 0 ? prev - 1 : filteredItems.length - 1);
|
|
132
|
+
}
|
|
133
|
+
if (key.downArrow) {
|
|
134
|
+
setSelectedIndex((prev) => prev < filteredItems.length - 1 ? prev + 1 : 0);
|
|
135
|
+
}
|
|
136
|
+
if (key.leftArrow || key.rightArrow) {
|
|
137
|
+
const categories2 = ["all", "navigation", "actions", "settings"];
|
|
138
|
+
const currentIdx = categories2.indexOf(selectedCategory);
|
|
139
|
+
if (key.leftArrow) {
|
|
140
|
+
setSelectedCategory(categories2[currentIdx > 0 ? currentIdx - 1 : categories2.length - 1]);
|
|
141
|
+
} else {
|
|
142
|
+
setSelectedCategory(categories2[currentIdx < categories2.length - 1 ? currentIdx + 1 : 0]);
|
|
143
|
+
}
|
|
144
|
+
setSelectedIndex(0);
|
|
145
|
+
}
|
|
146
|
+
if (key.return) {
|
|
147
|
+
const item = filteredItems[selectedIndex];
|
|
148
|
+
if (item) {
|
|
149
|
+
onSelect(item.command);
|
|
150
|
+
onClose();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
const categories = [
|
|
155
|
+
{ key: "all", label: "All" },
|
|
156
|
+
{ key: "navigation", label: "Navigation" },
|
|
157
|
+
{ key: "actions", label: "Actions" },
|
|
158
|
+
{ key: "settings", label: "Settings" }
|
|
159
|
+
];
|
|
160
|
+
return /* @__PURE__ */ jsxs4(
|
|
161
|
+
Box5,
|
|
162
|
+
{
|
|
163
|
+
flexDirection: "column",
|
|
164
|
+
borderStyle: "round",
|
|
165
|
+
borderColor: "cyan",
|
|
166
|
+
padding: 1,
|
|
167
|
+
width: 60,
|
|
168
|
+
children: [
|
|
169
|
+
/* @__PURE__ */ jsx5(Box5, { justifyContent: "center", marginBottom: 1, children: /* @__PURE__ */ jsx5(Text5, { bold: true, color: "cyan", children: "C-napse Help" }) }),
|
|
170
|
+
/* @__PURE__ */ jsx5(Box5, { justifyContent: "center", marginBottom: 1, children: categories.map((cat, idx) => /* @__PURE__ */ jsxs4(React.Fragment, { children: [
|
|
171
|
+
/* @__PURE__ */ jsx5(
|
|
172
|
+
Text5,
|
|
173
|
+
{
|
|
174
|
+
color: selectedCategory === cat.key ? "cyan" : "gray",
|
|
175
|
+
bold: selectedCategory === cat.key,
|
|
176
|
+
children: cat.label
|
|
177
|
+
}
|
|
178
|
+
),
|
|
179
|
+
idx < categories.length - 1 && /* @__PURE__ */ jsx5(Text5, { color: "gray", children: " \u2502 " })
|
|
180
|
+
] }, cat.key)) }),
|
|
181
|
+
/* @__PURE__ */ jsx5(Box5, { marginBottom: 1, children: /* @__PURE__ */ jsx5(Text5, { color: "gray", dimColor: true, children: "Use \u2190\u2192 to switch tabs, \u2191\u2193 to navigate, Enter to select" }) }),
|
|
182
|
+
/* @__PURE__ */ jsx5(Box5, { flexDirection: "column", children: filteredItems.map((item, index) => /* @__PURE__ */ jsxs4(Box5, { children: [
|
|
183
|
+
/* @__PURE__ */ jsx5(Text5, { color: index === selectedIndex ? "cyan" : "white", children: index === selectedIndex ? "\u276F " : " " }),
|
|
184
|
+
/* @__PURE__ */ jsx5(Box5, { width: 12, children: /* @__PURE__ */ jsx5(Text5, { bold: index === selectedIndex, color: index === selectedIndex ? "cyan" : "white", children: item.command }) }),
|
|
185
|
+
item.shortcut && /* @__PURE__ */ jsx5(Box5, { width: 10, children: /* @__PURE__ */ jsx5(Text5, { color: "yellow", dimColor: true, children: item.shortcut }) }),
|
|
186
|
+
/* @__PURE__ */ jsx5(Text5, { color: "gray", children: item.description })
|
|
187
|
+
] }, item.command)) }),
|
|
188
|
+
/* @__PURE__ */ jsx5(Box5, { marginTop: 1, justifyContent: "center", children: /* @__PURE__ */ jsx5(Text5, { color: "gray", dimColor: true, children: "Press Esc to close" }) })
|
|
189
|
+
]
|
|
190
|
+
}
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// src/components/ProviderSelector.tsx
|
|
195
|
+
import { useState as useState2 } from "react";
|
|
196
|
+
import { Box as Box6, Text as Text6, useInput as useInput2 } from "ink";
|
|
197
|
+
import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
198
|
+
var PROVIDERS = [
|
|
199
|
+
{
|
|
200
|
+
id: "ollama",
|
|
201
|
+
name: "Ollama",
|
|
202
|
+
description: "Local AI - Free, private, no API key",
|
|
203
|
+
defaultModel: "qwen2.5:0.5b",
|
|
204
|
+
models: ["qwen2.5:0.5b", "qwen2.5:1.5b", "qwen2.5:7b", "llama3.2:1b", "llama3.2:3b", "mistral:7b", "codellama:7b", "llava:7b"]
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
id: "openrouter",
|
|
208
|
+
name: "OpenRouter",
|
|
209
|
+
description: "Many models, pay-per-use",
|
|
210
|
+
defaultModel: "qwen/qwen-2.5-coder-32b-instruct",
|
|
211
|
+
models: [
|
|
212
|
+
"qwen/qwen-2.5-coder-32b-instruct",
|
|
213
|
+
"anthropic/claude-3.5-sonnet",
|
|
214
|
+
"openai/gpt-4o",
|
|
215
|
+
"openai/gpt-4o-mini",
|
|
216
|
+
"google/gemini-pro-1.5",
|
|
217
|
+
"meta-llama/llama-3.1-70b-instruct"
|
|
218
|
+
]
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
id: "anthropic",
|
|
222
|
+
name: "Anthropic",
|
|
223
|
+
description: "Claude models - Best for coding",
|
|
224
|
+
defaultModel: "claude-3-5-sonnet-20241022",
|
|
225
|
+
models: ["claude-3-5-sonnet-20241022", "claude-3-opus-20240229", "claude-3-haiku-20240307"]
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
id: "openai",
|
|
229
|
+
name: "OpenAI",
|
|
230
|
+
description: "GPT models",
|
|
231
|
+
defaultModel: "gpt-4o",
|
|
232
|
+
models: ["gpt-4o", "gpt-4o-mini", "gpt-4-turbo", "gpt-3.5-turbo"]
|
|
233
|
+
}
|
|
234
|
+
];
|
|
235
|
+
function ProviderSelector({ onClose, onSelect }) {
|
|
236
|
+
const config = getConfig();
|
|
237
|
+
const [mode, setMode] = useState2("provider");
|
|
238
|
+
const [providerIndex, setProviderIndex] = useState2(() => {
|
|
239
|
+
const idx = PROVIDERS.findIndex((p) => p.id === config.provider);
|
|
240
|
+
return idx >= 0 ? idx : 0;
|
|
241
|
+
});
|
|
242
|
+
const [modelIndex, setModelIndex] = useState2(0);
|
|
243
|
+
const [selectedProvider, setSelectedProvider] = useState2(null);
|
|
244
|
+
useInput2((input, key) => {
|
|
245
|
+
if (key.escape) {
|
|
246
|
+
onClose();
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (mode === "provider") {
|
|
250
|
+
if (key.upArrow) {
|
|
251
|
+
setProviderIndex((prev) => prev > 0 ? prev - 1 : PROVIDERS.length - 1);
|
|
252
|
+
} else if (key.downArrow) {
|
|
253
|
+
setProviderIndex((prev) => prev < PROVIDERS.length - 1 ? prev + 1 : 0);
|
|
254
|
+
} else if (key.return) {
|
|
255
|
+
const provider = PROVIDERS[providerIndex];
|
|
256
|
+
setSelectedProvider(provider);
|
|
257
|
+
const currentModelIdx = provider.models.findIndex((m) => m === config.model);
|
|
258
|
+
setModelIndex(currentModelIdx >= 0 ? currentModelIdx : 0);
|
|
259
|
+
setMode("model");
|
|
260
|
+
}
|
|
261
|
+
} else if (mode === "model" && selectedProvider) {
|
|
262
|
+
if (key.upArrow) {
|
|
263
|
+
setModelIndex((prev) => prev > 0 ? prev - 1 : selectedProvider.models.length - 1);
|
|
264
|
+
} else if (key.downArrow) {
|
|
265
|
+
setModelIndex((prev) => prev < selectedProvider.models.length - 1 ? prev + 1 : 0);
|
|
266
|
+
} else if (key.return) {
|
|
267
|
+
const model = selectedProvider.models[modelIndex];
|
|
268
|
+
setProvider(selectedProvider.id);
|
|
269
|
+
setModel(model);
|
|
270
|
+
onSelect(selectedProvider.id, model);
|
|
271
|
+
onClose();
|
|
272
|
+
} else if (key.leftArrow || input === "b") {
|
|
273
|
+
setMode("provider");
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
if (mode === "provider") {
|
|
278
|
+
return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", padding: 1, children: [
|
|
279
|
+
/* @__PURE__ */ jsx6(Box6, { marginBottom: 1, children: /* @__PURE__ */ jsx6(Text6, { bold: true, color: "cyan", children: "Select Provider" }) }),
|
|
280
|
+
/* @__PURE__ */ jsx6(Box6, { marginBottom: 1, children: /* @__PURE__ */ jsx6(Text6, { color: "gray", children: "Use arrows to navigate, Enter to select, Esc to cancel" }) }),
|
|
281
|
+
PROVIDERS.map((provider, index) => {
|
|
282
|
+
const isSelected = index === providerIndex;
|
|
283
|
+
const isCurrent = provider.id === config.provider;
|
|
284
|
+
return /* @__PURE__ */ jsxs5(Box6, { marginY: 0, children: [
|
|
285
|
+
/* @__PURE__ */ jsxs5(Text6, { color: isSelected ? "cyan" : "white", children: [
|
|
286
|
+
isSelected ? "\u276F " : " ",
|
|
287
|
+
provider.name,
|
|
288
|
+
isCurrent && /* @__PURE__ */ jsx6(Text6, { color: "green", children: " (current)" })
|
|
289
|
+
] }),
|
|
290
|
+
isSelected && /* @__PURE__ */ jsxs5(Text6, { color: "gray", children: [
|
|
291
|
+
" - ",
|
|
292
|
+
provider.description
|
|
293
|
+
] })
|
|
294
|
+
] }, provider.id);
|
|
295
|
+
}),
|
|
296
|
+
/* @__PURE__ */ jsx6(Box6, { marginTop: 1, borderStyle: "single", borderColor: "gray", paddingX: 1, children: /* @__PURE__ */ jsxs5(Text6, { color: "gray", children: [
|
|
297
|
+
"Current: ",
|
|
298
|
+
config.provider,
|
|
299
|
+
" / ",
|
|
300
|
+
config.model
|
|
301
|
+
] }) })
|
|
302
|
+
] });
|
|
303
|
+
}
|
|
304
|
+
return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", padding: 1, children: [
|
|
305
|
+
/* @__PURE__ */ jsx6(Box6, { marginBottom: 1, children: /* @__PURE__ */ jsxs5(Text6, { bold: true, color: "cyan", children: [
|
|
306
|
+
"Select Model for ",
|
|
307
|
+
selectedProvider?.name
|
|
308
|
+
] }) }),
|
|
309
|
+
/* @__PURE__ */ jsx6(Box6, { marginBottom: 1, children: /* @__PURE__ */ jsx6(Text6, { color: "gray", children: "Arrows to navigate, Enter to select, B/Left to go back" }) }),
|
|
310
|
+
selectedProvider?.models.map((model, index) => {
|
|
311
|
+
const isSelected = index === modelIndex;
|
|
312
|
+
const isCurrent = model === config.model && selectedProvider.id === config.provider;
|
|
313
|
+
const isDefault = model === selectedProvider.defaultModel;
|
|
314
|
+
return /* @__PURE__ */ jsx6(Box6, { marginY: 0, children: /* @__PURE__ */ jsxs5(Text6, { color: isSelected ? "cyan" : "white", children: [
|
|
315
|
+
isSelected ? "\u276F " : " ",
|
|
316
|
+
model,
|
|
317
|
+
isCurrent && /* @__PURE__ */ jsx6(Text6, { color: "green", children: " (current)" }),
|
|
318
|
+
isDefault && !isCurrent && /* @__PURE__ */ jsx6(Text6, { color: "yellow", children: " (default)" })
|
|
319
|
+
] }) }, model);
|
|
320
|
+
}),
|
|
321
|
+
/* @__PURE__ */ jsx6(Box6, { marginTop: 1, borderStyle: "single", borderColor: "gray", paddingX: 1, children: /* @__PURE__ */ jsxs5(Text6, { color: "gray", children: [
|
|
322
|
+
"Provider: ",
|
|
323
|
+
selectedProvider?.name
|
|
324
|
+
] }) })
|
|
325
|
+
] });
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// src/hooks/useChat.ts
|
|
329
|
+
import { useState as useState3, useCallback, useRef, useEffect } from "react";
|
|
330
|
+
|
|
132
331
|
// src/lib/api.ts
|
|
133
332
|
var SYSTEM_PROMPT = `You are C-napse, a helpful AI assistant for PC automation running on the user's desktop.
|
|
134
333
|
You can help with coding, file management, shell commands, and more. Be concise and helpful.
|
|
@@ -137,23 +336,23 @@ When responding:
|
|
|
137
336
|
- Be direct and practical
|
|
138
337
|
- Use markdown formatting for code blocks
|
|
139
338
|
- If asked to do something, explain what you'll do first`;
|
|
140
|
-
async function chat(messages) {
|
|
141
|
-
const
|
|
339
|
+
async function chat(messages, systemPrompt) {
|
|
340
|
+
const config = getConfig();
|
|
142
341
|
const allMessages = [
|
|
143
|
-
{ role: "system", content: SYSTEM_PROMPT },
|
|
342
|
+
{ role: "system", content: systemPrompt || SYSTEM_PROMPT },
|
|
144
343
|
...messages
|
|
145
344
|
];
|
|
146
|
-
switch (
|
|
345
|
+
switch (config.provider) {
|
|
147
346
|
case "openrouter":
|
|
148
|
-
return chatOpenRouter(allMessages,
|
|
347
|
+
return chatOpenRouter(allMessages, config.model);
|
|
149
348
|
case "ollama":
|
|
150
|
-
return chatOllama(allMessages,
|
|
349
|
+
return chatOllama(allMessages, config.model);
|
|
151
350
|
case "anthropic":
|
|
152
|
-
return chatAnthropic(allMessages,
|
|
351
|
+
return chatAnthropic(allMessages, config.model);
|
|
153
352
|
case "openai":
|
|
154
|
-
return chatOpenAI(allMessages,
|
|
353
|
+
return chatOpenAI(allMessages, config.model);
|
|
155
354
|
default:
|
|
156
|
-
throw new Error(`Unknown provider: ${
|
|
355
|
+
throw new Error(`Unknown provider: ${config.provider}`);
|
|
157
356
|
}
|
|
158
357
|
}
|
|
159
358
|
async function chatOpenRouter(messages, model) {
|
|
@@ -161,14 +360,14 @@ async function chatOpenRouter(messages, model) {
|
|
|
161
360
|
if (!apiKey) {
|
|
162
361
|
throw new Error("OpenRouter API key not configured. Run: cnapse auth openrouter <key>");
|
|
163
362
|
}
|
|
164
|
-
const
|
|
363
|
+
const config = getConfig();
|
|
165
364
|
const response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
|
|
166
365
|
method: "POST",
|
|
167
366
|
headers: {
|
|
168
367
|
"Authorization": `Bearer ${apiKey}`,
|
|
169
368
|
"Content-Type": "application/json",
|
|
170
|
-
"HTTP-Referer":
|
|
171
|
-
"X-Title":
|
|
369
|
+
"HTTP-Referer": config.openrouter.siteUrl,
|
|
370
|
+
"X-Title": config.openrouter.appName
|
|
172
371
|
},
|
|
173
372
|
body: JSON.stringify({
|
|
174
373
|
model,
|
|
@@ -186,8 +385,8 @@ async function chatOpenRouter(messages, model) {
|
|
|
186
385
|
return { content, model };
|
|
187
386
|
}
|
|
188
387
|
async function chatOllama(messages, model) {
|
|
189
|
-
const
|
|
190
|
-
const response = await fetch(`${
|
|
388
|
+
const config = getConfig();
|
|
389
|
+
const response = await fetch(`${config.ollamaHost}/api/chat`, {
|
|
191
390
|
method: "POST",
|
|
192
391
|
headers: { "Content-Type": "application/json" },
|
|
193
392
|
body: JSON.stringify({
|
|
@@ -260,71 +459,106 @@ async function chatOpenAI(messages, model) {
|
|
|
260
459
|
return { content, model };
|
|
261
460
|
}
|
|
262
461
|
|
|
263
|
-
// src/
|
|
264
|
-
import {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
462
|
+
// src/lib/screen.ts
|
|
463
|
+
import { exec } from "child_process";
|
|
464
|
+
import { promisify } from "util";
|
|
465
|
+
var execAsync = promisify(exec);
|
|
466
|
+
async function getScreenDescription() {
|
|
467
|
+
try {
|
|
468
|
+
const platform = process.platform;
|
|
469
|
+
if (platform === "win32") {
|
|
470
|
+
const { stdout } = await execAsync(`
|
|
471
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
472
|
+
$screen = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds
|
|
473
|
+
Write-Output "$($screen.Width)x$($screen.Height)"
|
|
474
|
+
`, { shell: "powershell.exe" });
|
|
475
|
+
return `Screen ${stdout.trim()} captured`;
|
|
476
|
+
} else if (platform === "darwin") {
|
|
477
|
+
const { stdout } = await execAsync(`system_profiler SPDisplaysDataType | grep Resolution | head -1`);
|
|
478
|
+
return `Screen ${stdout.trim()}`;
|
|
479
|
+
} else {
|
|
480
|
+
const { stdout } = await execAsync(`xdpyinfo | grep dimensions | awk '{print $2}'`);
|
|
481
|
+
return `Screen ${stdout.trim()} captured`;
|
|
282
482
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
483
|
+
} catch {
|
|
484
|
+
return null;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// src/hooks/useChat.ts
|
|
489
|
+
var WELCOME_MESSAGE = {
|
|
490
|
+
id: "0",
|
|
491
|
+
role: "system",
|
|
492
|
+
content: "Welcome to C-napse! Type your message and press Enter.\n\nShortcuts: Ctrl+H for help, Ctrl+P for provider",
|
|
493
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
494
|
+
};
|
|
495
|
+
function useChat(screenWatch = false) {
|
|
496
|
+
const [messages, setMessages] = useState3([WELCOME_MESSAGE]);
|
|
497
|
+
const [isProcessing, setIsProcessing] = useState3(false);
|
|
498
|
+
const [error, setError] = useState3(null);
|
|
499
|
+
const screenContextRef = useRef(null);
|
|
500
|
+
useEffect(() => {
|
|
501
|
+
if (!screenWatch) {
|
|
502
|
+
screenContextRef.current = null;
|
|
295
503
|
return;
|
|
296
504
|
}
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
505
|
+
const checkScreen = async () => {
|
|
506
|
+
const desc = await getScreenDescription();
|
|
507
|
+
if (desc) {
|
|
508
|
+
screenContextRef.current = desc;
|
|
509
|
+
}
|
|
302
510
|
};
|
|
303
|
-
|
|
304
|
-
const
|
|
511
|
+
checkScreen();
|
|
512
|
+
const interval = setInterval(checkScreen, 5e3);
|
|
513
|
+
return () => clearInterval(interval);
|
|
514
|
+
}, [screenWatch]);
|
|
515
|
+
const addSystemMessage = useCallback((content) => {
|
|
305
516
|
setMessages((prev) => [
|
|
306
517
|
...prev,
|
|
307
518
|
{
|
|
308
|
-
id:
|
|
309
|
-
role: "
|
|
310
|
-
content
|
|
311
|
-
timestamp: /* @__PURE__ */ new Date()
|
|
312
|
-
isStreaming: true
|
|
519
|
+
id: Date.now().toString(),
|
|
520
|
+
role: "system",
|
|
521
|
+
content,
|
|
522
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
313
523
|
}
|
|
314
524
|
]);
|
|
525
|
+
}, []);
|
|
526
|
+
const sendMessage = useCallback(async (content) => {
|
|
527
|
+
if (!content.trim() || isProcessing) return;
|
|
528
|
+
setError(null);
|
|
529
|
+
const userMsg = {
|
|
530
|
+
id: Date.now().toString(),
|
|
531
|
+
role: "user",
|
|
532
|
+
content,
|
|
533
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
534
|
+
};
|
|
535
|
+
const assistantId = (Date.now() + 1).toString();
|
|
536
|
+
const assistantMsg = {
|
|
537
|
+
id: assistantId,
|
|
538
|
+
role: "assistant",
|
|
539
|
+
content: "",
|
|
540
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
541
|
+
isStreaming: true
|
|
542
|
+
};
|
|
543
|
+
setMessages((prev) => [...prev, userMsg, assistantMsg]);
|
|
315
544
|
setIsProcessing(true);
|
|
316
|
-
setStatus("Thinking...");
|
|
317
545
|
try {
|
|
318
546
|
const apiMessages = messages.filter((m) => m.role === "user" || m.role === "assistant").slice(-10).map((m) => ({ role: m.role, content: m.content }));
|
|
319
|
-
|
|
547
|
+
let finalContent = content;
|
|
548
|
+
if (screenWatch && screenContextRef.current) {
|
|
549
|
+
finalContent = `[Screen context: ${screenContextRef.current}]
|
|
550
|
+
|
|
551
|
+
${content}`;
|
|
552
|
+
}
|
|
553
|
+
apiMessages.push({ role: "user", content: finalContent });
|
|
320
554
|
const response = await chat(apiMessages);
|
|
321
555
|
setMessages(
|
|
322
556
|
(prev) => prev.map(
|
|
323
557
|
(m) => m.id === assistantId ? { ...m, content: response.content || "(no response)", isStreaming: false } : m
|
|
324
558
|
)
|
|
325
559
|
);
|
|
326
|
-
} catch (
|
|
327
|
-
const errorMsg =
|
|
560
|
+
} catch (err2) {
|
|
561
|
+
const errorMsg = err2 instanceof Error ? err2.message : "Unknown error";
|
|
328
562
|
setError(errorMsg);
|
|
329
563
|
setMessages(
|
|
330
564
|
(prev) => prev.map(
|
|
@@ -333,43 +567,1343 @@ function App() {
|
|
|
333
567
|
);
|
|
334
568
|
} finally {
|
|
335
569
|
setIsProcessing(false);
|
|
336
|
-
setStatus("Ready");
|
|
337
570
|
}
|
|
571
|
+
}, [messages, isProcessing, screenWatch]);
|
|
572
|
+
const clearMessages = useCallback(() => {
|
|
573
|
+
setMessages([WELCOME_MESSAGE]);
|
|
574
|
+
setError(null);
|
|
575
|
+
}, []);
|
|
576
|
+
return {
|
|
577
|
+
messages,
|
|
578
|
+
isProcessing,
|
|
579
|
+
error,
|
|
580
|
+
sendMessage,
|
|
581
|
+
addSystemMessage,
|
|
582
|
+
clearMessages
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// src/hooks/useVision.ts
|
|
587
|
+
import { useState as useState4, useCallback as useCallback2 } from "react";
|
|
588
|
+
|
|
589
|
+
// src/lib/vision.ts
|
|
590
|
+
async function describeScreen() {
|
|
591
|
+
const screenshot = await captureScreenshot();
|
|
592
|
+
if (!screenshot) {
|
|
593
|
+
throw new Error("Failed to capture screenshot");
|
|
594
|
+
}
|
|
595
|
+
const config = getConfig();
|
|
596
|
+
const description = await analyzeWithVision(screenshot, config.provider);
|
|
597
|
+
return { description, screenshot };
|
|
598
|
+
}
|
|
599
|
+
async function captureScreenshot() {
|
|
600
|
+
try {
|
|
601
|
+
const screenshotDesktop = await import("screenshot-desktop");
|
|
602
|
+
const buffer = await screenshotDesktop.default({ format: "png" });
|
|
603
|
+
return buffer.toString("base64");
|
|
604
|
+
} catch {
|
|
605
|
+
return captureScreenFallback();
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
async function captureScreenFallback() {
|
|
609
|
+
const { exec: exec5 } = await import("child_process");
|
|
610
|
+
const { promisify: promisify5 } = await import("util");
|
|
611
|
+
const { tmpdir } = await import("os");
|
|
612
|
+
const { join: join2 } = await import("path");
|
|
613
|
+
const { readFile, unlink } = await import("fs/promises");
|
|
614
|
+
const execAsync5 = promisify5(exec5);
|
|
615
|
+
const tempFile = join2(tmpdir(), `cnapse-screen-${Date.now()}.png`);
|
|
616
|
+
try {
|
|
617
|
+
const platform = process.platform;
|
|
618
|
+
if (platform === "win32") {
|
|
619
|
+
await execAsync5(`
|
|
620
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
621
|
+
$screen = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds
|
|
622
|
+
$bitmap = New-Object System.Drawing.Bitmap($screen.Width, $screen.Height)
|
|
623
|
+
$graphics = [System.Drawing.Graphics]::FromImage($bitmap)
|
|
624
|
+
$graphics.CopyFromScreen($screen.Location, [System.Drawing.Point]::Empty, $screen.Size)
|
|
625
|
+
$bitmap.Save("${tempFile.replace(/\\/g, "\\\\")}")
|
|
626
|
+
$graphics.Dispose()
|
|
627
|
+
$bitmap.Dispose()
|
|
628
|
+
`, { shell: "powershell.exe" });
|
|
629
|
+
} else if (platform === "darwin") {
|
|
630
|
+
await execAsync5(`screencapture -x "${tempFile}"`);
|
|
631
|
+
} else {
|
|
632
|
+
await execAsync5(`gnome-screenshot -f "${tempFile}" 2>/dev/null || scrot "${tempFile}" 2>/dev/null || import -window root "${tempFile}"`);
|
|
633
|
+
}
|
|
634
|
+
const imageBuffer = await readFile(tempFile);
|
|
635
|
+
await unlink(tempFile).catch(() => {
|
|
636
|
+
});
|
|
637
|
+
return imageBuffer.toString("base64");
|
|
638
|
+
} catch {
|
|
639
|
+
return null;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
async function analyzeWithVision(base64Image, provider) {
|
|
643
|
+
const prompt = `Look at this screenshot and describe:
|
|
644
|
+
1. What application or window is visible
|
|
645
|
+
2. Key UI elements you can see (buttons, text fields, menus)
|
|
646
|
+
3. What the user appears to be doing or could do next
|
|
647
|
+
4. Any notable content or state
|
|
648
|
+
|
|
649
|
+
Be concise but helpful.`;
|
|
650
|
+
switch (provider) {
|
|
651
|
+
case "ollama":
|
|
652
|
+
return analyzeWithOllama(base64Image, prompt);
|
|
653
|
+
case "openrouter":
|
|
654
|
+
return analyzeWithOpenRouter(base64Image, prompt);
|
|
655
|
+
case "anthropic":
|
|
656
|
+
return analyzeWithAnthropic(base64Image, prompt);
|
|
657
|
+
case "openai":
|
|
658
|
+
return analyzeWithOpenAI(base64Image, prompt);
|
|
659
|
+
default:
|
|
660
|
+
throw new Error(`Vision not supported for provider: ${provider}`);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
async function analyzeWithOllama(base64Image, prompt) {
|
|
664
|
+
const config = getConfig();
|
|
665
|
+
const ollamaHost = config.ollamaHost || "http://localhost:11434";
|
|
666
|
+
const visionModels = ["llava", "llama3.2-vision", "bakllava", "llava-llama3"];
|
|
667
|
+
const model = visionModels.find((m) => config.model.includes(m)) || "llava";
|
|
668
|
+
const response = await fetch(`${ollamaHost}/api/generate`, {
|
|
669
|
+
method: "POST",
|
|
670
|
+
headers: { "Content-Type": "application/json" },
|
|
671
|
+
body: JSON.stringify({
|
|
672
|
+
model,
|
|
673
|
+
prompt,
|
|
674
|
+
images: [base64Image],
|
|
675
|
+
stream: false
|
|
676
|
+
})
|
|
677
|
+
});
|
|
678
|
+
if (!response.ok) {
|
|
679
|
+
const text = await response.text();
|
|
680
|
+
throw new Error(`Ollama vision error: ${text}`);
|
|
681
|
+
}
|
|
682
|
+
const data = await response.json();
|
|
683
|
+
return data.response || "Unable to analyze image";
|
|
684
|
+
}
|
|
685
|
+
async function analyzeWithOpenRouter(base64Image, prompt) {
|
|
686
|
+
const apiKey = getApiKey("openrouter");
|
|
687
|
+
if (!apiKey) throw new Error("OpenRouter API key not configured");
|
|
688
|
+
const model = "anthropic/claude-3-5-sonnet";
|
|
689
|
+
const response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
|
|
690
|
+
method: "POST",
|
|
691
|
+
headers: {
|
|
692
|
+
"Authorization": `Bearer ${apiKey}`,
|
|
693
|
+
"Content-Type": "application/json",
|
|
694
|
+
"HTTP-Referer": "https://c-napse.up.railway.app",
|
|
695
|
+
"X-Title": "C-napse"
|
|
696
|
+
},
|
|
697
|
+
body: JSON.stringify({
|
|
698
|
+
model,
|
|
699
|
+
messages: [
|
|
700
|
+
{
|
|
701
|
+
role: "user",
|
|
702
|
+
content: [
|
|
703
|
+
{ type: "text", text: prompt },
|
|
704
|
+
{
|
|
705
|
+
type: "image_url",
|
|
706
|
+
image_url: { url: `data:image/png;base64,${base64Image}` }
|
|
707
|
+
}
|
|
708
|
+
]
|
|
709
|
+
}
|
|
710
|
+
],
|
|
711
|
+
max_tokens: 1e3
|
|
712
|
+
})
|
|
713
|
+
});
|
|
714
|
+
if (!response.ok) {
|
|
715
|
+
const text = await response.text();
|
|
716
|
+
throw new Error(`OpenRouter vision error: ${text}`);
|
|
717
|
+
}
|
|
718
|
+
const data = await response.json();
|
|
719
|
+
return data.choices?.[0]?.message?.content || "Unable to analyze image";
|
|
720
|
+
}
|
|
721
|
+
async function analyzeWithAnthropic(base64Image, prompt) {
|
|
722
|
+
const apiKey = getApiKey("anthropic");
|
|
723
|
+
if (!apiKey) throw new Error("Anthropic API key not configured");
|
|
724
|
+
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
725
|
+
method: "POST",
|
|
726
|
+
headers: {
|
|
727
|
+
"x-api-key": apiKey,
|
|
728
|
+
"anthropic-version": "2023-06-01",
|
|
729
|
+
"Content-Type": "application/json"
|
|
730
|
+
},
|
|
731
|
+
body: JSON.stringify({
|
|
732
|
+
model: "claude-3-5-sonnet-20241022",
|
|
733
|
+
max_tokens: 1e3,
|
|
734
|
+
messages: [
|
|
735
|
+
{
|
|
736
|
+
role: "user",
|
|
737
|
+
content: [
|
|
738
|
+
{
|
|
739
|
+
type: "image",
|
|
740
|
+
source: {
|
|
741
|
+
type: "base64",
|
|
742
|
+
media_type: "image/png",
|
|
743
|
+
data: base64Image
|
|
744
|
+
}
|
|
745
|
+
},
|
|
746
|
+
{ type: "text", text: prompt }
|
|
747
|
+
]
|
|
748
|
+
}
|
|
749
|
+
]
|
|
750
|
+
})
|
|
751
|
+
});
|
|
752
|
+
if (!response.ok) {
|
|
753
|
+
const text = await response.text();
|
|
754
|
+
throw new Error(`Anthropic vision error: ${text}`);
|
|
755
|
+
}
|
|
756
|
+
const data = await response.json();
|
|
757
|
+
return data.content?.[0]?.text || "Unable to analyze image";
|
|
758
|
+
}
|
|
759
|
+
async function analyzeWithOpenAI(base64Image, prompt) {
|
|
760
|
+
const apiKey = getApiKey("openai");
|
|
761
|
+
if (!apiKey) throw new Error("OpenAI API key not configured");
|
|
762
|
+
const response = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
763
|
+
method: "POST",
|
|
764
|
+
headers: {
|
|
765
|
+
"Authorization": `Bearer ${apiKey}`,
|
|
766
|
+
"Content-Type": "application/json"
|
|
767
|
+
},
|
|
768
|
+
body: JSON.stringify({
|
|
769
|
+
model: "gpt-4-vision-preview",
|
|
770
|
+
messages: [
|
|
771
|
+
{
|
|
772
|
+
role: "user",
|
|
773
|
+
content: [
|
|
774
|
+
{ type: "text", text: prompt },
|
|
775
|
+
{
|
|
776
|
+
type: "image_url",
|
|
777
|
+
image_url: { url: `data:image/png;base64,${base64Image}` }
|
|
778
|
+
}
|
|
779
|
+
]
|
|
780
|
+
}
|
|
781
|
+
],
|
|
782
|
+
max_tokens: 1e3
|
|
783
|
+
})
|
|
784
|
+
});
|
|
785
|
+
if (!response.ok) {
|
|
786
|
+
const text = await response.text();
|
|
787
|
+
throw new Error(`OpenAI vision error: ${text}`);
|
|
788
|
+
}
|
|
789
|
+
const data = await response.json();
|
|
790
|
+
return data.choices?.[0]?.message?.content || "Unable to analyze image";
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// src/hooks/useVision.ts
|
|
794
|
+
function useVision() {
|
|
795
|
+
const [isAnalyzing, setIsAnalyzing] = useState4(false);
|
|
796
|
+
const [lastDescription, setLastDescription] = useState4(null);
|
|
797
|
+
const [lastScreenshot, setLastScreenshot] = useState4(null);
|
|
798
|
+
const [error, setError] = useState4(null);
|
|
799
|
+
const analyze = useCallback2(async () => {
|
|
800
|
+
setIsAnalyzing(true);
|
|
801
|
+
setError(null);
|
|
802
|
+
try {
|
|
803
|
+
const result = await describeScreen();
|
|
804
|
+
setLastDescription(result.description);
|
|
805
|
+
setLastScreenshot(result.screenshot);
|
|
806
|
+
return result.description;
|
|
807
|
+
} catch (err2) {
|
|
808
|
+
const errorMsg = err2 instanceof Error ? err2.message : "Vision analysis failed";
|
|
809
|
+
setError(errorMsg);
|
|
810
|
+
throw err2;
|
|
811
|
+
} finally {
|
|
812
|
+
setIsAnalyzing(false);
|
|
813
|
+
}
|
|
814
|
+
}, []);
|
|
815
|
+
return {
|
|
816
|
+
isAnalyzing,
|
|
817
|
+
lastDescription,
|
|
818
|
+
lastScreenshot,
|
|
819
|
+
error,
|
|
820
|
+
analyze
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// src/hooks/useTelegram.ts
|
|
825
|
+
import { useState as useState5, useCallback as useCallback3, useEffect as useEffect2, useRef as useRef2 } from "react";
|
|
826
|
+
|
|
827
|
+
// src/services/telegram.ts
|
|
828
|
+
import { EventEmitter } from "events";
|
|
829
|
+
|
|
830
|
+
// src/tools/shell.ts
|
|
831
|
+
import { exec as exec4 } from "child_process";
|
|
832
|
+
import { promisify as promisify4 } from "util";
|
|
833
|
+
|
|
834
|
+
// src/tools/clipboard.ts
|
|
835
|
+
import clipboardy from "clipboardy";
|
|
836
|
+
|
|
837
|
+
// src/tools/process.ts
|
|
838
|
+
import { exec as exec2 } from "child_process";
|
|
839
|
+
import { promisify as promisify2 } from "util";
|
|
840
|
+
var execAsync2 = promisify2(exec2);
|
|
841
|
+
|
|
842
|
+
// src/tools/computer.ts
|
|
843
|
+
import { exec as exec3 } from "child_process";
|
|
844
|
+
import { promisify as promisify3 } from "util";
|
|
845
|
+
var execAsync3 = promisify3(exec3);
|
|
846
|
+
async function clickMouse(button = "left") {
|
|
847
|
+
try {
|
|
848
|
+
if (process.platform === "win32") {
|
|
849
|
+
const script = `
|
|
850
|
+
Add-Type -MemberDefinition @"
|
|
851
|
+
[DllImport("user32.dll",CharSet=CharSet.Auto,CallingConvention=CallingConvention.StdCall)]
|
|
852
|
+
public static extern void mouse_event(long dwFlags, long dx, long dy, long cButtons, long dwExtraInfo);
|
|
853
|
+
"@ -Name Mouse -Namespace Win32
|
|
854
|
+
${button === "left" ? "[Win32.Mouse]::mouse_event(0x02, 0, 0, 0, 0); [Win32.Mouse]::mouse_event(0x04, 0, 0, 0, 0)" : button === "right" ? "[Win32.Mouse]::mouse_event(0x08, 0, 0, 0, 0); [Win32.Mouse]::mouse_event(0x10, 0, 0, 0, 0)" : "[Win32.Mouse]::mouse_event(0x20, 0, 0, 0, 0); [Win32.Mouse]::mouse_event(0x40, 0, 0, 0, 0)"}`;
|
|
855
|
+
await execAsync3(`powershell -Command "${script.replace(/\n/g, " ")}"`, { shell: "cmd.exe" });
|
|
856
|
+
} else if (process.platform === "darwin") {
|
|
857
|
+
await execAsync3(`cliclick c:.`);
|
|
858
|
+
} else {
|
|
859
|
+
const btn = button === "left" ? "1" : button === "right" ? "3" : "2";
|
|
860
|
+
await execAsync3(`xdotool click ${btn}`);
|
|
861
|
+
}
|
|
862
|
+
return ok(`Clicked ${button} button`);
|
|
863
|
+
} catch (error) {
|
|
864
|
+
return err(`Failed to click: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
async function typeText(text) {
|
|
868
|
+
try {
|
|
869
|
+
if (process.platform === "win32") {
|
|
870
|
+
const escapedText = text.replace(/'/g, "''").replace(/[+^%~(){}[\]]/g, "{$&}");
|
|
871
|
+
await execAsync3(`powershell -Command "Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.SendKeys]::SendWait('${escapedText}')"`, { shell: "cmd.exe" });
|
|
872
|
+
} else if (process.platform === "darwin") {
|
|
873
|
+
const escaped = text.replace(/'/g, "'\\''");
|
|
874
|
+
await execAsync3(`osascript -e 'tell application "System Events" to keystroke "${escaped}"'`);
|
|
875
|
+
} else {
|
|
876
|
+
const escaped = text.replace(/'/g, "'\\''");
|
|
877
|
+
await execAsync3(`xdotool type '${escaped}'`);
|
|
878
|
+
}
|
|
879
|
+
return ok(`Typed: ${text}`);
|
|
880
|
+
} catch (error) {
|
|
881
|
+
return err(`Failed to type: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
async function pressKey(key) {
|
|
885
|
+
try {
|
|
886
|
+
if (process.platform === "win32") {
|
|
887
|
+
const winKeyMap = {
|
|
888
|
+
"enter": "{ENTER}",
|
|
889
|
+
"return": "{ENTER}",
|
|
890
|
+
"escape": "{ESC}",
|
|
891
|
+
"esc": "{ESC}",
|
|
892
|
+
"tab": "{TAB}",
|
|
893
|
+
"space": " ",
|
|
894
|
+
"backspace": "{BACKSPACE}",
|
|
895
|
+
"delete": "{DELETE}",
|
|
896
|
+
"up": "{UP}",
|
|
897
|
+
"down": "{DOWN}",
|
|
898
|
+
"left": "{LEFT}",
|
|
899
|
+
"right": "{RIGHT}",
|
|
900
|
+
"home": "{HOME}",
|
|
901
|
+
"end": "{END}",
|
|
902
|
+
"pageup": "{PGUP}",
|
|
903
|
+
"pagedown": "{PGDN}",
|
|
904
|
+
"f1": "{F1}",
|
|
905
|
+
"f2": "{F2}",
|
|
906
|
+
"f3": "{F3}",
|
|
907
|
+
"f4": "{F4}",
|
|
908
|
+
"f5": "{F5}",
|
|
909
|
+
"f6": "{F6}",
|
|
910
|
+
"f7": "{F7}",
|
|
911
|
+
"f8": "{F8}",
|
|
912
|
+
"f9": "{F9}",
|
|
913
|
+
"f10": "{F10}",
|
|
914
|
+
"f11": "{F11}",
|
|
915
|
+
"f12": "{F12}"
|
|
916
|
+
};
|
|
917
|
+
const winKey = winKeyMap[key.toLowerCase()] || key;
|
|
918
|
+
await execAsync3(`powershell -Command "Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.SendKeys]::SendWait('${winKey}')"`, { shell: "cmd.exe" });
|
|
919
|
+
} else if (process.platform === "darwin") {
|
|
920
|
+
const macKeyMap = {
|
|
921
|
+
"return": 36,
|
|
922
|
+
"enter": 36,
|
|
923
|
+
"escape": 53,
|
|
924
|
+
"esc": 53,
|
|
925
|
+
"tab": 48,
|
|
926
|
+
"space": 49,
|
|
927
|
+
"backspace": 51,
|
|
928
|
+
"delete": 117,
|
|
929
|
+
"up": 126,
|
|
930
|
+
"down": 125,
|
|
931
|
+
"left": 123,
|
|
932
|
+
"right": 124
|
|
933
|
+
};
|
|
934
|
+
const keyCode = macKeyMap[key.toLowerCase()];
|
|
935
|
+
if (keyCode) {
|
|
936
|
+
await execAsync3(`osascript -e 'tell application "System Events" to key code ${keyCode}'`);
|
|
937
|
+
} else {
|
|
938
|
+
await execAsync3(`osascript -e 'tell application "System Events" to keystroke "${key}"'`);
|
|
939
|
+
}
|
|
940
|
+
} else {
|
|
941
|
+
await execAsync3(`xdotool key ${key}`);
|
|
942
|
+
}
|
|
943
|
+
return ok(`Pressed: ${key}`);
|
|
944
|
+
} catch (error) {
|
|
945
|
+
return err(`Failed to press key: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
async function keyCombo(keys) {
|
|
949
|
+
try {
|
|
950
|
+
if (process.platform === "win32") {
|
|
951
|
+
const hasWin = keys.some((k) => k.toLowerCase() === "meta" || k.toLowerCase() === "win");
|
|
952
|
+
const hasR = keys.some((k) => k.toLowerCase() === "r");
|
|
953
|
+
if (hasWin && hasR) {
|
|
954
|
+
await execAsync3(`powershell -Command "$shell = New-Object -ComObject WScript.Shell; $shell.Run('explorer shell:::{2559a1f3-21d7-11d4-bdaf-00c04f60b9f0}')"`, { shell: "cmd.exe" });
|
|
955
|
+
return ok(`Pressed: ${keys.join("+")}`);
|
|
956
|
+
}
|
|
957
|
+
const modifierMap = {
|
|
958
|
+
"control": "^",
|
|
959
|
+
"ctrl": "^",
|
|
960
|
+
"alt": "%",
|
|
961
|
+
"shift": "+"
|
|
962
|
+
};
|
|
963
|
+
let combo = "";
|
|
964
|
+
const regularKeys = [];
|
|
965
|
+
for (const key of keys) {
|
|
966
|
+
const lower = key.toLowerCase();
|
|
967
|
+
if (modifierMap[lower]) {
|
|
968
|
+
combo += modifierMap[lower];
|
|
969
|
+
} else if (lower !== "meta" && lower !== "win") {
|
|
970
|
+
regularKeys.push(key.toLowerCase());
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
combo += regularKeys.join("");
|
|
974
|
+
await execAsync3(`powershell -Command "Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.SendKeys]::SendWait('${combo}')"`, { shell: "cmd.exe" });
|
|
975
|
+
} else if (process.platform === "darwin") {
|
|
976
|
+
const modifiers = keys.filter((k) => ["control", "ctrl", "alt", "shift", "command", "meta"].includes(k.toLowerCase()));
|
|
977
|
+
const regular = keys.filter((k) => !["control", "ctrl", "alt", "shift", "command", "meta"].includes(k.toLowerCase()));
|
|
978
|
+
let cmd = 'tell application "System Events" to keystroke "' + regular.join("") + '"';
|
|
979
|
+
if (modifiers.length > 0) {
|
|
980
|
+
const modMap = {
|
|
981
|
+
"control": "control down",
|
|
982
|
+
"ctrl": "control down",
|
|
983
|
+
"alt": "option down",
|
|
984
|
+
"shift": "shift down",
|
|
985
|
+
"command": "command down",
|
|
986
|
+
"meta": "command down"
|
|
987
|
+
};
|
|
988
|
+
cmd += " using {" + modifiers.map((m) => modMap[m.toLowerCase()]).join(", ") + "}";
|
|
989
|
+
}
|
|
990
|
+
await execAsync3(`osascript -e '${cmd}'`);
|
|
991
|
+
} else {
|
|
992
|
+
await execAsync3(`xdotool key ${keys.join("+")}`);
|
|
993
|
+
}
|
|
994
|
+
return ok(`Pressed: ${keys.join("+")}`);
|
|
995
|
+
} catch (error) {
|
|
996
|
+
return err(`Failed to press combo: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
async function focusWindow(title) {
|
|
1000
|
+
try {
|
|
1001
|
+
if (process.platform === "win32") {
|
|
1002
|
+
const escaped = title.replace(/'/g, "''");
|
|
1003
|
+
await execAsync3(`powershell -Command "$wshell = New-Object -ComObject wscript.shell; $wshell.AppActivate('${escaped}')"`, { shell: "cmd.exe" });
|
|
1004
|
+
} else if (process.platform === "darwin") {
|
|
1005
|
+
await execAsync3(`osascript -e 'tell application "${title}" to activate'`);
|
|
1006
|
+
} else {
|
|
1007
|
+
await execAsync3(`wmctrl -a "${title}"`);
|
|
1008
|
+
}
|
|
1009
|
+
return ok(`Focused window: ${title}`);
|
|
1010
|
+
} catch (error) {
|
|
1011
|
+
return err(`Failed to focus window: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// src/tools/index.ts
|
|
1016
|
+
function ok(output) {
|
|
1017
|
+
return { success: true, output };
|
|
1018
|
+
}
|
|
1019
|
+
function err(error) {
|
|
1020
|
+
return { success: false, output: "", error };
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// src/tools/shell.ts
|
|
1024
|
+
var execAsync4 = promisify4(exec4);
|
|
1025
|
+
async function runCommand(cmd, timeout = 3e4) {
|
|
1026
|
+
try {
|
|
1027
|
+
const isWindows = process.platform === "win32";
|
|
1028
|
+
const shell = isWindows ? "cmd.exe" : "/bin/sh";
|
|
1029
|
+
const shellArg = isWindows ? "/C" : "-c";
|
|
1030
|
+
const { stdout, stderr } = await execAsync4(cmd, {
|
|
1031
|
+
shell,
|
|
1032
|
+
timeout,
|
|
1033
|
+
maxBuffer: 10 * 1024 * 1024
|
|
1034
|
+
// 10MB
|
|
1035
|
+
});
|
|
1036
|
+
if (stderr && stderr.trim()) {
|
|
1037
|
+
return ok(`${stdout}
|
|
1038
|
+
[stderr]: ${stderr}`);
|
|
1039
|
+
}
|
|
1040
|
+
return ok(stdout || "(no output)");
|
|
1041
|
+
} catch (error) {
|
|
1042
|
+
if (error.killed) {
|
|
1043
|
+
return err(`Command timed out after ${timeout}ms`);
|
|
1044
|
+
}
|
|
1045
|
+
const stderr = error.stderr || "";
|
|
1046
|
+
const stdout = error.stdout || "";
|
|
1047
|
+
return {
|
|
1048
|
+
success: false,
|
|
1049
|
+
output: stdout,
|
|
1050
|
+
error: `Exit code: ${error.code || -1}
|
|
1051
|
+
${stderr}`
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// src/services/telegram.ts
|
|
1057
|
+
var TelegramBotService = class extends EventEmitter {
|
|
1058
|
+
bot = null;
|
|
1059
|
+
isRunning = false;
|
|
1060
|
+
allowedChatIds = /* @__PURE__ */ new Set();
|
|
1061
|
+
constructor() {
|
|
1062
|
+
super();
|
|
1063
|
+
}
|
|
1064
|
+
/**
|
|
1065
|
+
* Start the Telegram bot
|
|
1066
|
+
*/
|
|
1067
|
+
async start() {
|
|
1068
|
+
if (this.isRunning) {
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
const botToken = getApiKey("telegram");
|
|
1072
|
+
if (!botToken) {
|
|
1073
|
+
throw new Error("Telegram bot token not configured. Use: cnapse auth telegram YOUR_BOT_TOKEN");
|
|
1074
|
+
}
|
|
1075
|
+
try {
|
|
1076
|
+
const { Telegraf } = await import("telegraf");
|
|
1077
|
+
this.bot = new Telegraf(botToken);
|
|
1078
|
+
const config = getConfig();
|
|
1079
|
+
if (config.telegram?.chatId) {
|
|
1080
|
+
this.allowedChatIds.add(config.telegram.chatId);
|
|
1081
|
+
}
|
|
1082
|
+
this.setupHandlers();
|
|
1083
|
+
await this.bot.launch();
|
|
1084
|
+
this.isRunning = true;
|
|
1085
|
+
this.emit("started");
|
|
1086
|
+
} catch (error) {
|
|
1087
|
+
throw new Error(`Failed to start Telegram bot: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
/**
|
|
1091
|
+
* Stop the Telegram bot
|
|
1092
|
+
*/
|
|
1093
|
+
async stop() {
|
|
1094
|
+
if (!this.isRunning || !this.bot) {
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
this.bot.stop("SIGTERM");
|
|
1098
|
+
this.isRunning = false;
|
|
1099
|
+
this.bot = null;
|
|
1100
|
+
this.emit("stopped");
|
|
1101
|
+
}
|
|
1102
|
+
/**
|
|
1103
|
+
* Check if bot is running
|
|
1104
|
+
*/
|
|
1105
|
+
get running() {
|
|
1106
|
+
return this.isRunning;
|
|
1107
|
+
}
|
|
1108
|
+
/**
|
|
1109
|
+
* Setup message and command handlers
|
|
1110
|
+
*/
|
|
1111
|
+
setupHandlers() {
|
|
1112
|
+
if (!this.bot) return;
|
|
1113
|
+
this.bot.command("start", async (ctx) => {
|
|
1114
|
+
const chatId = ctx.chat.id;
|
|
1115
|
+
this.allowedChatIds.add(chatId);
|
|
1116
|
+
await ctx.reply(
|
|
1117
|
+
`\u{1F916} C-napse connected!
|
|
1118
|
+
|
|
1119
|
+
Commands:
|
|
1120
|
+
/screen - Take screenshot
|
|
1121
|
+
/describe - Screenshot + AI description
|
|
1122
|
+
/run <cmd> - Execute command
|
|
1123
|
+
/status - System status
|
|
1124
|
+
|
|
1125
|
+
Your chat ID: ${chatId}`
|
|
1126
|
+
);
|
|
1127
|
+
});
|
|
1128
|
+
this.bot.command("screen", async (ctx) => {
|
|
1129
|
+
if (!this.isAllowed(ctx.chat.id)) {
|
|
1130
|
+
await ctx.reply("\u26D4 Not authorized. Send /start first.");
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
await ctx.reply("\u{1F4F8} Taking screenshot...");
|
|
1134
|
+
try {
|
|
1135
|
+
const screenshot = await captureScreenshot();
|
|
1136
|
+
if (!screenshot) {
|
|
1137
|
+
await ctx.reply("\u274C Failed to capture screenshot");
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
const buffer = Buffer.from(screenshot, "base64");
|
|
1141
|
+
await ctx.replyWithPhoto({ source: buffer }, { caption: "\u{1F4F8} Current screen" });
|
|
1142
|
+
} catch (error) {
|
|
1143
|
+
await ctx.reply(`\u274C Error: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1144
|
+
}
|
|
1145
|
+
});
|
|
1146
|
+
this.bot.command("describe", async (ctx) => {
|
|
1147
|
+
if (!this.isAllowed(ctx.chat.id)) {
|
|
1148
|
+
await ctx.reply("\u26D4 Not authorized. Send /start first.");
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
await ctx.reply("\u{1F50D} Analyzing screen...");
|
|
1152
|
+
try {
|
|
1153
|
+
const result = await describeScreen();
|
|
1154
|
+
const buffer = Buffer.from(result.screenshot, "base64");
|
|
1155
|
+
const caption = `\u{1F5A5}\uFE0F Screen Analysis:
|
|
1156
|
+
|
|
1157
|
+
${result.description}`.slice(0, 1024);
|
|
1158
|
+
await ctx.replyWithPhoto({ source: buffer }, { caption });
|
|
1159
|
+
if (result.description.length > 900) {
|
|
1160
|
+
await ctx.reply(result.description);
|
|
1161
|
+
}
|
|
1162
|
+
} catch (error) {
|
|
1163
|
+
await ctx.reply(`\u274C Error: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1164
|
+
}
|
|
1165
|
+
});
|
|
1166
|
+
this.bot.command("run", async (ctx) => {
|
|
1167
|
+
if (!this.isAllowed(ctx.chat.id)) {
|
|
1168
|
+
await ctx.reply("\u26D4 Not authorized. Send /start first.");
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
const cmd = ctx.message.text.replace("/run ", "").trim();
|
|
1172
|
+
if (!cmd) {
|
|
1173
|
+
await ctx.reply("Usage: /run <command>\nExample: /run dir");
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
await ctx.reply(`\u2699\uFE0F Running: ${cmd}`);
|
|
1177
|
+
try {
|
|
1178
|
+
const result = await runCommand(cmd, 3e4);
|
|
1179
|
+
if (result.success) {
|
|
1180
|
+
const output = result.output.slice(0, 4e3) || "(no output)";
|
|
1181
|
+
await ctx.reply(`\u2705 Output:
|
|
1182
|
+
\`\`\`
|
|
1183
|
+
${output}
|
|
1184
|
+
\`\`\``, { parse_mode: "Markdown" });
|
|
1185
|
+
} else {
|
|
1186
|
+
await ctx.reply(`\u274C Error:
|
|
1187
|
+
\`\`\`
|
|
1188
|
+
${result.error}
|
|
1189
|
+
\`\`\``, { parse_mode: "Markdown" });
|
|
1190
|
+
}
|
|
1191
|
+
} catch (error) {
|
|
1192
|
+
await ctx.reply(`\u274C Error: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1193
|
+
}
|
|
1194
|
+
});
|
|
1195
|
+
this.bot.command("status", async (ctx) => {
|
|
1196
|
+
if (!this.isAllowed(ctx.chat.id)) {
|
|
1197
|
+
await ctx.reply("\u26D4 Not authorized. Send /start first.");
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
const config = getConfig();
|
|
1201
|
+
const status = [
|
|
1202
|
+
"\u{1F4CA} C-napse Status",
|
|
1203
|
+
"",
|
|
1204
|
+
`Provider: ${config.provider}`,
|
|
1205
|
+
`Model: ${config.model}`,
|
|
1206
|
+
`Platform: ${process.platform}`,
|
|
1207
|
+
`Node: ${process.version}`
|
|
1208
|
+
].join("\n");
|
|
1209
|
+
await ctx.reply(status);
|
|
1210
|
+
});
|
|
1211
|
+
this.bot.on("text", async (ctx) => {
|
|
1212
|
+
if (!this.isAllowed(ctx.chat.id)) {
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
if (ctx.message.text.startsWith("/")) {
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
const message = {
|
|
1219
|
+
chatId: ctx.chat.id,
|
|
1220
|
+
text: ctx.message.text,
|
|
1221
|
+
from: ctx.from.username || ctx.from.first_name || "User"
|
|
1222
|
+
};
|
|
1223
|
+
this.emit("message", message);
|
|
1224
|
+
this.emit("command", "chat", ctx.message.text, ctx.chat.id);
|
|
1225
|
+
});
|
|
1226
|
+
this.bot.catch((err2) => {
|
|
1227
|
+
this.emit("error", err2);
|
|
1228
|
+
});
|
|
1229
|
+
}
|
|
1230
|
+
/**
|
|
1231
|
+
* Check if chat is authorized
|
|
1232
|
+
*/
|
|
1233
|
+
isAllowed(chatId) {
|
|
1234
|
+
if (this.allowedChatIds.size === 0) {
|
|
1235
|
+
return true;
|
|
1236
|
+
}
|
|
1237
|
+
return this.allowedChatIds.has(chatId);
|
|
1238
|
+
}
|
|
1239
|
+
/**
|
|
1240
|
+
* Send a message to a specific chat
|
|
1241
|
+
*/
|
|
1242
|
+
async sendMessage(chatId, text) {
|
|
1243
|
+
if (!this.bot || !this.isRunning) {
|
|
1244
|
+
throw new Error("Telegram bot is not running");
|
|
1245
|
+
}
|
|
1246
|
+
await this.bot.telegram.sendMessage(chatId, text);
|
|
1247
|
+
}
|
|
1248
|
+
/**
|
|
1249
|
+
* Send a photo to a specific chat
|
|
1250
|
+
*/
|
|
1251
|
+
async sendPhoto(chatId, base64Image, caption) {
|
|
1252
|
+
if (!this.bot || !this.isRunning) {
|
|
1253
|
+
throw new Error("Telegram bot is not running");
|
|
1254
|
+
}
|
|
1255
|
+
const buffer = Buffer.from(base64Image, "base64");
|
|
1256
|
+
await this.bot.telegram.sendPhoto(chatId, { source: buffer }, { caption });
|
|
1257
|
+
}
|
|
1258
|
+
};
|
|
1259
|
+
var instance = null;
|
|
1260
|
+
function getTelegramBot() {
|
|
1261
|
+
if (!instance) {
|
|
1262
|
+
instance = new TelegramBotService();
|
|
1263
|
+
}
|
|
1264
|
+
return instance;
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
// src/hooks/useTelegram.ts
|
|
1268
|
+
function useTelegram(onMessage) {
|
|
1269
|
+
const [isEnabled, setIsEnabled] = useState5(false);
|
|
1270
|
+
const [isStarting, setIsStarting] = useState5(false);
|
|
1271
|
+
const [error, setError] = useState5(null);
|
|
1272
|
+
const [lastMessage, setLastMessage] = useState5(null);
|
|
1273
|
+
const onMessageRef = useRef2(onMessage);
|
|
1274
|
+
useEffect2(() => {
|
|
1275
|
+
onMessageRef.current = onMessage;
|
|
1276
|
+
}, [onMessage]);
|
|
1277
|
+
const start = useCallback3(async () => {
|
|
1278
|
+
if (isEnabled) return;
|
|
1279
|
+
setIsStarting(true);
|
|
1280
|
+
setError(null);
|
|
1281
|
+
try {
|
|
1282
|
+
const bot = getTelegramBot();
|
|
1283
|
+
bot.on("message", (msg) => {
|
|
1284
|
+
setLastMessage(msg);
|
|
1285
|
+
onMessageRef.current?.(msg);
|
|
1286
|
+
});
|
|
1287
|
+
bot.on("error", (err2) => {
|
|
1288
|
+
setError(err2.message);
|
|
1289
|
+
});
|
|
1290
|
+
await bot.start();
|
|
1291
|
+
setIsEnabled(true);
|
|
1292
|
+
} catch (err2) {
|
|
1293
|
+
const errorMsg = err2 instanceof Error ? err2.message : "Failed to start Telegram bot";
|
|
1294
|
+
setError(errorMsg);
|
|
1295
|
+
throw err2;
|
|
1296
|
+
} finally {
|
|
1297
|
+
setIsStarting(false);
|
|
1298
|
+
}
|
|
1299
|
+
}, [isEnabled]);
|
|
1300
|
+
const stop = useCallback3(async () => {
|
|
1301
|
+
if (!isEnabled) return;
|
|
1302
|
+
try {
|
|
1303
|
+
const bot = getTelegramBot();
|
|
1304
|
+
await bot.stop();
|
|
1305
|
+
setIsEnabled(false);
|
|
1306
|
+
} catch (err2) {
|
|
1307
|
+
const errorMsg = err2 instanceof Error ? err2.message : "Failed to stop Telegram bot";
|
|
1308
|
+
setError(errorMsg);
|
|
1309
|
+
throw err2;
|
|
1310
|
+
}
|
|
1311
|
+
}, [isEnabled]);
|
|
1312
|
+
const toggle = useCallback3(async () => {
|
|
1313
|
+
if (isEnabled) {
|
|
1314
|
+
await stop();
|
|
1315
|
+
} else {
|
|
1316
|
+
await start();
|
|
1317
|
+
}
|
|
1318
|
+
}, [isEnabled, start, stop]);
|
|
1319
|
+
return {
|
|
1320
|
+
isEnabled,
|
|
1321
|
+
isStarting,
|
|
1322
|
+
error,
|
|
1323
|
+
lastMessage,
|
|
1324
|
+
toggle,
|
|
1325
|
+
start,
|
|
1326
|
+
stop
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
// src/hooks/useTasks.ts
|
|
1331
|
+
import { useState as useState6, useCallback as useCallback4 } from "react";
|
|
1332
|
+
|
|
1333
|
+
// src/lib/tasks.ts
|
|
1334
|
+
import * as fs from "fs";
|
|
1335
|
+
import * as path from "path";
|
|
1336
|
+
import * as os from "os";
|
|
1337
|
+
var TASK_MEMORY_FILE = path.join(os.homedir(), ".cnapse", "task-memory.json");
|
|
1338
|
+
function loadTaskMemory() {
|
|
1339
|
+
try {
|
|
1340
|
+
if (fs.existsSync(TASK_MEMORY_FILE)) {
|
|
1341
|
+
const data = fs.readFileSync(TASK_MEMORY_FILE, "utf-8");
|
|
1342
|
+
return JSON.parse(data);
|
|
1343
|
+
}
|
|
1344
|
+
} catch {
|
|
1345
|
+
}
|
|
1346
|
+
return { patterns: [], version: 1 };
|
|
1347
|
+
}
|
|
1348
|
+
function saveTaskPattern(input, steps) {
|
|
1349
|
+
try {
|
|
1350
|
+
const memory = loadTaskMemory();
|
|
1351
|
+
const normalized = normalizeInput(input);
|
|
1352
|
+
const existing = memory.patterns.find((p) => p.normalizedInput === normalized);
|
|
1353
|
+
if (existing) {
|
|
1354
|
+
existing.steps = steps;
|
|
1355
|
+
existing.successCount++;
|
|
1356
|
+
existing.lastUsed = (/* @__PURE__ */ new Date()).toISOString();
|
|
1357
|
+
} else {
|
|
1358
|
+
memory.patterns.push({
|
|
1359
|
+
input,
|
|
1360
|
+
normalizedInput: normalized,
|
|
1361
|
+
steps,
|
|
1362
|
+
successCount: 1,
|
|
1363
|
+
lastUsed: (/* @__PURE__ */ new Date()).toISOString()
|
|
1364
|
+
});
|
|
1365
|
+
}
|
|
1366
|
+
memory.patterns = memory.patterns.sort((a, b) => b.successCount - a.successCount).slice(0, 100);
|
|
1367
|
+
const dir = path.dirname(TASK_MEMORY_FILE);
|
|
1368
|
+
if (!fs.existsSync(dir)) {
|
|
1369
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1370
|
+
}
|
|
1371
|
+
fs.writeFileSync(TASK_MEMORY_FILE, JSON.stringify(memory, null, 2));
|
|
1372
|
+
} catch {
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
function normalizeInput(input) {
|
|
1376
|
+
return input.toLowerCase().replace(/[^\w\s]/g, " ").replace(/\s+/g, " ").trim();
|
|
1377
|
+
}
|
|
1378
|
+
function findSimilarPatterns(input) {
|
|
1379
|
+
const memory = loadTaskMemory();
|
|
1380
|
+
const normalized = normalizeInput(input);
|
|
1381
|
+
const words = normalized.split(" ").filter((w) => w.length > 2);
|
|
1382
|
+
return memory.patterns.filter((pattern) => {
|
|
1383
|
+
const patternWords = pattern.normalizedInput.split(" ");
|
|
1384
|
+
const matches = words.filter((w) => patternWords.includes(w));
|
|
1385
|
+
return matches.length >= Math.min(2, words.length * 0.5);
|
|
1386
|
+
}).sort((a, b) => b.successCount - a.successCount).slice(0, 3);
|
|
1387
|
+
}
|
|
1388
|
+
function buildChainOfThoughtPrompt(input) {
|
|
1389
|
+
const similarPatterns = findSimilarPatterns(input);
|
|
1390
|
+
let learnedExamples = "";
|
|
1391
|
+
if (similarPatterns.length > 0) {
|
|
1392
|
+
learnedExamples = `
|
|
1393
|
+
## LEARNED PATTERNS (from successful past tasks)
|
|
1394
|
+
These patterns worked before - use them as reference:
|
|
1395
|
+
|
|
1396
|
+
${similarPatterns.map((p, i) => `
|
|
1397
|
+
Pattern ${i + 1} (used ${p.successCount} times):
|
|
1398
|
+
Input: "${p.input}"
|
|
1399
|
+
Steps: ${JSON.stringify(p.steps, null, 2)}
|
|
1400
|
+
`).join("\n")}
|
|
1401
|
+
`;
|
|
1402
|
+
}
|
|
1403
|
+
return `You are a task parser for Windows PC automation. Your job is to convert natural language into precise, executable steps.
|
|
1404
|
+
|
|
1405
|
+
## THINKING PROCESS
|
|
1406
|
+
Before outputting steps, THINK through these questions:
|
|
1407
|
+
|
|
1408
|
+
1. **WHAT** is the main goal?
|
|
1409
|
+
- What application needs to open?
|
|
1410
|
+
- What action needs to happen inside it?
|
|
1411
|
+
- What is the expected end result?
|
|
1412
|
+
|
|
1413
|
+
2. **HOW** to achieve it on Windows?
|
|
1414
|
+
- Use Win+R (meta+r) to open Run dialog for apps
|
|
1415
|
+
- Wait 1-3 seconds after opening apps for them to load
|
|
1416
|
+
- Use keyboard shortcuts when possible (faster, more reliable)
|
|
1417
|
+
- Common shortcuts: Ctrl+S (save), Ctrl+O (open), Ctrl+N (new), Alt+F4 (close)
|
|
1418
|
+
|
|
1419
|
+
3. **SEQUENCE** - what order makes sense?
|
|
1420
|
+
- Open app FIRST
|
|
1421
|
+
- WAIT for it to load
|
|
1422
|
+
- THEN interact with it
|
|
1423
|
+
- Add waits between actions that need time
|
|
1424
|
+
|
|
1425
|
+
4. **EDGE CASES** - what could go wrong?
|
|
1426
|
+
- App might already be open -> focus_window first
|
|
1427
|
+
- Dialogs might appear -> handle or dismiss them
|
|
1428
|
+
- Typing too fast -> add small waits
|
|
1429
|
+
|
|
1430
|
+
## AVAILABLE ACTIONS
|
|
1431
|
+
- open_app: Open app via Run dialog (e.g., "open_app:notepad", "open_app:code", "open_app:chrome")
|
|
1432
|
+
- type_text: Type text string (e.g., "type_text:Hello World")
|
|
1433
|
+
- press_key: Single key (e.g., "press_key:enter", "press_key:escape", "press_key:tab")
|
|
1434
|
+
- key_combo: Key combination (e.g., "key_combo:control+s", "key_combo:alt+f4", "key_combo:meta+r")
|
|
1435
|
+
- click: Mouse click (e.g., "click:left", "click:right")
|
|
1436
|
+
- wait: Wait N seconds (e.g., "wait:2" - use 1-3s for app loads)
|
|
1437
|
+
- focus_window: Focus by title (e.g., "focus_window:Notepad")
|
|
1438
|
+
- screenshot: Capture and describe screen
|
|
1439
|
+
${learnedExamples}
|
|
1440
|
+
## EXAMPLES WITH REASONING
|
|
1441
|
+
|
|
1442
|
+
### Example 1: "open notepad and type hello"
|
|
1443
|
+
Thinking:
|
|
1444
|
+
- Goal: Open Notepad, then type text into it
|
|
1445
|
+
- How: Win+R -> notepad -> Enter to open, then type
|
|
1446
|
+
- Sequence: Open -> Wait for load -> Type
|
|
1447
|
+
- Edge case: Need wait time for Notepad window to be ready
|
|
1448
|
+
|
|
1449
|
+
Output:
|
|
1450
|
+
[
|
|
1451
|
+
{ "description": "Open Notepad via Run dialog", "action": "open_app:notepad" },
|
|
1452
|
+
{ "description": "Wait for Notepad to fully load", "action": "wait:2" },
|
|
1453
|
+
{ "description": "Type the greeting text", "action": "type_text:hello" }
|
|
1454
|
+
]
|
|
1455
|
+
|
|
1456
|
+
### Example 2: "save the current document"
|
|
1457
|
+
Thinking:
|
|
1458
|
+
- Goal: Save whatever is in the current app
|
|
1459
|
+
- How: Ctrl+S is universal save shortcut
|
|
1460
|
+
- Sequence: Just the key combo, maybe wait for save
|
|
1461
|
+
- Edge case: If file is new, Save As dialog might appear
|
|
1462
|
+
|
|
1463
|
+
Output:
|
|
1464
|
+
[
|
|
1465
|
+
{ "description": "Press Ctrl+S to save", "action": "key_combo:control+s" },
|
|
1466
|
+
{ "description": "Wait for save to complete", "action": "wait:1" }
|
|
1467
|
+
]
|
|
1468
|
+
|
|
1469
|
+
### Example 3: "close this window"
|
|
1470
|
+
Thinking:
|
|
1471
|
+
- Goal: Close the current active window
|
|
1472
|
+
- How: Alt+F4 closes active window on Windows
|
|
1473
|
+
- Sequence: Single action
|
|
1474
|
+
- Edge case: Might prompt to save - user handles that
|
|
1475
|
+
|
|
1476
|
+
Output:
|
|
1477
|
+
[
|
|
1478
|
+
{ "description": "Close active window with Alt+F4", "action": "key_combo:alt+f4" }
|
|
1479
|
+
]
|
|
1480
|
+
|
|
1481
|
+
## YOUR TASK
|
|
1482
|
+
Now parse this request: "${input}"
|
|
1483
|
+
|
|
1484
|
+
First, briefly think through the 4 questions above, then output ONLY a JSON array:
|
|
1485
|
+
[
|
|
1486
|
+
{ "description": "Human readable step", "action": "action_type:params" },
|
|
1487
|
+
...
|
|
1488
|
+
]`;
|
|
1489
|
+
}
|
|
1490
|
+
async function parseTask(input) {
|
|
1491
|
+
const systemPrompt = buildChainOfThoughtPrompt(input);
|
|
1492
|
+
const messages = [
|
|
1493
|
+
{ role: "user", content: input }
|
|
1494
|
+
];
|
|
1495
|
+
try {
|
|
1496
|
+
const response = await chat(messages, systemPrompt);
|
|
1497
|
+
const content = response.content || "[]";
|
|
1498
|
+
const jsonMatch = content.match(/\[[\s\S]*\]/);
|
|
1499
|
+
if (!jsonMatch) {
|
|
1500
|
+
throw new Error("Failed to parse task steps");
|
|
1501
|
+
}
|
|
1502
|
+
const parsedSteps = JSON.parse(jsonMatch[0]);
|
|
1503
|
+
const steps = parsedSteps.map((step, index) => ({
|
|
1504
|
+
id: `step-${index + 1}`,
|
|
1505
|
+
description: step.description,
|
|
1506
|
+
action: step.action,
|
|
1507
|
+
status: "pending"
|
|
1508
|
+
}));
|
|
1509
|
+
return {
|
|
1510
|
+
id: `task-${Date.now()}`,
|
|
1511
|
+
description: input,
|
|
1512
|
+
steps,
|
|
1513
|
+
status: "pending",
|
|
1514
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
1515
|
+
};
|
|
1516
|
+
} catch (error) {
|
|
1517
|
+
return {
|
|
1518
|
+
id: `task-${Date.now()}`,
|
|
1519
|
+
description: input,
|
|
1520
|
+
steps: [{
|
|
1521
|
+
id: "step-1",
|
|
1522
|
+
description: input,
|
|
1523
|
+
action: `chat:${input}`,
|
|
1524
|
+
status: "pending"
|
|
1525
|
+
}],
|
|
1526
|
+
status: "pending",
|
|
1527
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
1528
|
+
};
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
async function executeStep(step) {
|
|
1532
|
+
const [actionType, ...paramParts] = step.action.split(":");
|
|
1533
|
+
const params = paramParts.join(":");
|
|
1534
|
+
switch (actionType) {
|
|
1535
|
+
case "open_app":
|
|
1536
|
+
await keyCombo(["meta", "r"]);
|
|
1537
|
+
await sleep(500);
|
|
1538
|
+
await typeText(params);
|
|
1539
|
+
await sleep(300);
|
|
1540
|
+
await pressKey("Return");
|
|
1541
|
+
step.result = `Opened ${params}`;
|
|
1542
|
+
break;
|
|
1543
|
+
case "type_text":
|
|
1544
|
+
await typeText(params);
|
|
1545
|
+
step.result = `Typed: ${params}`;
|
|
1546
|
+
break;
|
|
1547
|
+
case "press_key":
|
|
1548
|
+
await pressKey(params);
|
|
1549
|
+
step.result = `Pressed ${params}`;
|
|
1550
|
+
break;
|
|
1551
|
+
case "key_combo":
|
|
1552
|
+
const keys = params.split("+").map((k) => k.trim());
|
|
1553
|
+
await keyCombo(keys);
|
|
1554
|
+
step.result = `Pressed ${params}`;
|
|
1555
|
+
break;
|
|
1556
|
+
case "click":
|
|
1557
|
+
const button = params || "left";
|
|
1558
|
+
await clickMouse(button);
|
|
1559
|
+
step.result = `Clicked ${button}`;
|
|
1560
|
+
break;
|
|
1561
|
+
case "wait":
|
|
1562
|
+
const seconds = parseInt(params) || 1;
|
|
1563
|
+
await sleep(seconds * 1e3);
|
|
1564
|
+
step.result = `Waited ${seconds}s`;
|
|
1565
|
+
break;
|
|
1566
|
+
case "focus_window":
|
|
1567
|
+
await focusWindow(params);
|
|
1568
|
+
step.result = `Focused window: ${params}`;
|
|
1569
|
+
break;
|
|
1570
|
+
case "screenshot":
|
|
1571
|
+
const vision = await describeScreen();
|
|
1572
|
+
step.result = vision.description;
|
|
1573
|
+
break;
|
|
1574
|
+
case "chat":
|
|
1575
|
+
step.result = `Task noted: ${params}`;
|
|
1576
|
+
break;
|
|
1577
|
+
default:
|
|
1578
|
+
throw new Error(`Unknown action: ${actionType}`);
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
async function executeTask(task, onProgress) {
|
|
1582
|
+
task.status = "running";
|
|
1583
|
+
for (const step of task.steps) {
|
|
1584
|
+
if (task.status === "failed") {
|
|
1585
|
+
step.status = "skipped";
|
|
1586
|
+
continue;
|
|
1587
|
+
}
|
|
1588
|
+
step.status = "running";
|
|
1589
|
+
onProgress?.(task, step);
|
|
1590
|
+
try {
|
|
1591
|
+
await executeStep(step);
|
|
1592
|
+
step.status = "completed";
|
|
1593
|
+
} catch (error) {
|
|
1594
|
+
step.status = "failed";
|
|
1595
|
+
step.error = error instanceof Error ? error.message : "Unknown error";
|
|
1596
|
+
task.status = "failed";
|
|
1597
|
+
}
|
|
1598
|
+
onProgress?.(task, step);
|
|
1599
|
+
}
|
|
1600
|
+
if (task.status !== "failed") {
|
|
1601
|
+
task.status = "completed";
|
|
1602
|
+
const steps = task.steps.map((s) => ({
|
|
1603
|
+
description: s.description,
|
|
1604
|
+
action: s.action
|
|
1605
|
+
}));
|
|
1606
|
+
saveTaskPattern(task.description, steps);
|
|
1607
|
+
}
|
|
1608
|
+
task.completedAt = /* @__PURE__ */ new Date();
|
|
1609
|
+
return task;
|
|
1610
|
+
}
|
|
1611
|
+
function sleep(ms) {
|
|
1612
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1613
|
+
}
|
|
1614
|
+
function getTaskMemoryStats() {
|
|
1615
|
+
const memory = loadTaskMemory();
|
|
1616
|
+
const totalUses = memory.patterns.reduce((sum, p) => sum + p.successCount, 0);
|
|
1617
|
+
const topPatterns = memory.patterns.sort((a, b) => b.successCount - a.successCount).slice(0, 5).map((p) => `"${p.input}" (${p.successCount}x)`);
|
|
1618
|
+
return {
|
|
1619
|
+
patternCount: memory.patterns.length,
|
|
1620
|
+
totalUses,
|
|
1621
|
+
topPatterns
|
|
1622
|
+
};
|
|
1623
|
+
}
|
|
1624
|
+
function clearTaskMemory() {
|
|
1625
|
+
try {
|
|
1626
|
+
if (fs.existsSync(TASK_MEMORY_FILE)) {
|
|
1627
|
+
fs.unlinkSync(TASK_MEMORY_FILE);
|
|
1628
|
+
}
|
|
1629
|
+
} catch {
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
function formatTask(task) {
|
|
1633
|
+
const statusEmoji = {
|
|
1634
|
+
pending: "\u23F3",
|
|
1635
|
+
running: "\u{1F504}",
|
|
1636
|
+
completed: "\u2705",
|
|
1637
|
+
failed: "\u274C"
|
|
1638
|
+
};
|
|
1639
|
+
const stepStatusEmoji = {
|
|
1640
|
+
pending: "\u25CB",
|
|
1641
|
+
running: "\u25D0",
|
|
1642
|
+
completed: "\u25CF",
|
|
1643
|
+
failed: "\u2717",
|
|
1644
|
+
skipped: "\u25CC"
|
|
338
1645
|
};
|
|
339
|
-
|
|
1646
|
+
let output = `${statusEmoji[task.status]} Task: ${task.description}
|
|
1647
|
+
|
|
1648
|
+
`;
|
|
1649
|
+
for (const step of task.steps) {
|
|
1650
|
+
output += ` ${stepStatusEmoji[step.status]} ${step.description}`;
|
|
1651
|
+
if (step.result) {
|
|
1652
|
+
output += ` \u2192 ${step.result}`;
|
|
1653
|
+
}
|
|
1654
|
+
if (step.error) {
|
|
1655
|
+
output += ` (Error: ${step.error})`;
|
|
1656
|
+
}
|
|
1657
|
+
output += "\n";
|
|
1658
|
+
}
|
|
1659
|
+
return output;
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
// src/hooks/useTasks.ts
|
|
1663
|
+
function useTasks(onProgress) {
|
|
1664
|
+
const [isRunning, setIsRunning] = useState6(false);
|
|
1665
|
+
const [currentTask, setCurrentTask] = useState6(null);
|
|
1666
|
+
const [currentStep, setCurrentStep] = useState6(null);
|
|
1667
|
+
const [error, setError] = useState6(null);
|
|
1668
|
+
const run = useCallback4(async (description) => {
|
|
1669
|
+
setIsRunning(true);
|
|
1670
|
+
setError(null);
|
|
1671
|
+
try {
|
|
1672
|
+
const task = await parseTask(description);
|
|
1673
|
+
setCurrentTask(task);
|
|
1674
|
+
const result = await executeTask(task, (updatedTask, step) => {
|
|
1675
|
+
setCurrentTask({ ...updatedTask });
|
|
1676
|
+
setCurrentStep(step);
|
|
1677
|
+
onProgress?.(updatedTask, step);
|
|
1678
|
+
});
|
|
1679
|
+
setCurrentTask(result);
|
|
1680
|
+
return result;
|
|
1681
|
+
} catch (err2) {
|
|
1682
|
+
const errorMsg = err2 instanceof Error ? err2.message : "Task failed";
|
|
1683
|
+
setError(errorMsg);
|
|
1684
|
+
throw err2;
|
|
1685
|
+
} finally {
|
|
1686
|
+
setIsRunning(false);
|
|
1687
|
+
setCurrentStep(null);
|
|
1688
|
+
}
|
|
1689
|
+
}, [onProgress]);
|
|
1690
|
+
return {
|
|
1691
|
+
isRunning,
|
|
1692
|
+
currentTask,
|
|
1693
|
+
currentStep,
|
|
1694
|
+
error,
|
|
1695
|
+
run,
|
|
1696
|
+
format: formatTask,
|
|
1697
|
+
getMemoryStats: getTaskMemoryStats,
|
|
1698
|
+
clearMemory: clearTaskMemory
|
|
1699
|
+
};
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
// src/components/App.tsx
|
|
1703
|
+
import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1704
|
+
function App() {
|
|
1705
|
+
const { exit } = useApp();
|
|
1706
|
+
const [overlay, setOverlay] = useState7("none");
|
|
1707
|
+
const [screenWatch, setScreenWatch] = useState7(false);
|
|
1708
|
+
const [status, setStatus] = useState7("Ready");
|
|
1709
|
+
const chat2 = useChat(screenWatch);
|
|
1710
|
+
const vision = useVision();
|
|
1711
|
+
const telegram = useTelegram((msg) => {
|
|
1712
|
+
chat2.addSystemMessage(`\u{1F4F1} Telegram [${msg.from}]: ${msg.text}`);
|
|
1713
|
+
});
|
|
1714
|
+
const tasks = useTasks((task, step) => {
|
|
1715
|
+
if (step.status === "running") {
|
|
1716
|
+
setStatus(`Running: ${step.description}`);
|
|
1717
|
+
}
|
|
1718
|
+
});
|
|
1719
|
+
useInput3((inputChar, key) => {
|
|
1720
|
+
if (overlay !== "none") return;
|
|
1721
|
+
if (key.ctrl && inputChar === "c") exit();
|
|
1722
|
+
if (key.ctrl && inputChar === "l") chat2.clearMessages();
|
|
1723
|
+
if (key.ctrl && inputChar === "h") setOverlay("help");
|
|
1724
|
+
if (key.ctrl && inputChar === "p") setOverlay("provider");
|
|
1725
|
+
if (key.ctrl && inputChar === "w") {
|
|
1726
|
+
setScreenWatch((prev) => {
|
|
1727
|
+
const newState = !prev;
|
|
1728
|
+
chat2.addSystemMessage(
|
|
1729
|
+
newState ? "\u{1F5A5}\uFE0F Screen watching enabled." : "\u{1F5A5}\uFE0F Screen watching disabled."
|
|
1730
|
+
);
|
|
1731
|
+
return newState;
|
|
1732
|
+
});
|
|
1733
|
+
}
|
|
1734
|
+
if (key.ctrl && inputChar === "t") {
|
|
1735
|
+
handleTelegramToggle();
|
|
1736
|
+
}
|
|
1737
|
+
});
|
|
1738
|
+
const handleCommand = useCallback5(async (cmd) => {
|
|
340
1739
|
const parts = cmd.slice(1).split(" ");
|
|
341
1740
|
const command = parts[0];
|
|
1741
|
+
const args2 = parts.slice(1).join(" ");
|
|
342
1742
|
switch (command) {
|
|
343
1743
|
case "clear":
|
|
344
|
-
|
|
345
|
-
addSystemMessage("Chat cleared.");
|
|
1744
|
+
chat2.clearMessages();
|
|
1745
|
+
chat2.addSystemMessage("Chat cleared.");
|
|
346
1746
|
break;
|
|
347
1747
|
case "help":
|
|
348
|
-
|
|
349
|
-
|
|
1748
|
+
setOverlay("help");
|
|
1749
|
+
break;
|
|
1750
|
+
case "provider":
|
|
1751
|
+
case "model":
|
|
1752
|
+
setOverlay("provider");
|
|
1753
|
+
break;
|
|
1754
|
+
case "config": {
|
|
1755
|
+
const config = getConfig();
|
|
1756
|
+
chat2.addSystemMessage(
|
|
1757
|
+
`\u2699\uFE0F Configuration:
|
|
1758
|
+
Provider: ${config.provider}
|
|
1759
|
+
Model: ${config.model}
|
|
1760
|
+
Ollama: ${config.ollamaHost}
|
|
1761
|
+
|
|
1762
|
+
Use /provider to change`
|
|
350
1763
|
);
|
|
351
1764
|
break;
|
|
1765
|
+
}
|
|
1766
|
+
case "screen":
|
|
1767
|
+
await handleScreenCommand();
|
|
1768
|
+
break;
|
|
1769
|
+
case "watch":
|
|
1770
|
+
setScreenWatch((prev) => {
|
|
1771
|
+
const newState = !prev;
|
|
1772
|
+
chat2.addSystemMessage(
|
|
1773
|
+
newState ? "\u{1F5A5}\uFE0F Screen watching enabled." : "\u{1F5A5}\uFE0F Screen watching disabled."
|
|
1774
|
+
);
|
|
1775
|
+
return newState;
|
|
1776
|
+
});
|
|
1777
|
+
break;
|
|
1778
|
+
case "telegram":
|
|
1779
|
+
await handleTelegramToggle();
|
|
1780
|
+
break;
|
|
1781
|
+
case "task":
|
|
1782
|
+
if (args2) {
|
|
1783
|
+
await handleTaskCommand(args2);
|
|
1784
|
+
} else {
|
|
1785
|
+
chat2.addSystemMessage("Usage: /task <description>\nExample: /task open notepad and type hello");
|
|
1786
|
+
}
|
|
1787
|
+
break;
|
|
1788
|
+
case "memory":
|
|
1789
|
+
if (args2 === "clear") {
|
|
1790
|
+
tasks.clearMemory();
|
|
1791
|
+
chat2.addSystemMessage("\u{1F9E0} Task memory cleared.");
|
|
1792
|
+
} else {
|
|
1793
|
+
const stats = tasks.getMemoryStats();
|
|
1794
|
+
chat2.addSystemMessage(
|
|
1795
|
+
`\u{1F9E0} Task Memory:
|
|
1796
|
+
|
|
1797
|
+
Learned patterns: ${stats.patternCount}
|
|
1798
|
+
Total successful uses: ${stats.totalUses}
|
|
1799
|
+
|
|
1800
|
+
` + (stats.topPatterns.length > 0 ? ` Top patterns:
|
|
1801
|
+
${stats.topPatterns.map((p) => ` \u2022 ${p}`).join("\n")}
|
|
1802
|
+
|
|
1803
|
+
` : " No patterns learned yet.\n\n") + `The more you use /task, the smarter it gets!
|
|
1804
|
+
Use /memory clear to reset.`
|
|
1805
|
+
);
|
|
1806
|
+
}
|
|
1807
|
+
break;
|
|
1808
|
+
case "quit":
|
|
1809
|
+
case "exit":
|
|
1810
|
+
exit();
|
|
1811
|
+
break;
|
|
352
1812
|
default:
|
|
353
|
-
addSystemMessage(`Unknown command: ${command}
|
|
1813
|
+
chat2.addSystemMessage(`Unknown command: ${command}
|
|
1814
|
+
Type /help for commands`);
|
|
354
1815
|
}
|
|
355
|
-
};
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
1816
|
+
}, [chat2, exit]);
|
|
1817
|
+
const handleScreenCommand = useCallback5(async () => {
|
|
1818
|
+
chat2.addSystemMessage("\u{1F4F8} Analyzing screen...");
|
|
1819
|
+
setStatus("Analyzing...");
|
|
1820
|
+
try {
|
|
1821
|
+
const description = await vision.analyze();
|
|
1822
|
+
chat2.addSystemMessage(`\u{1F5A5}\uFE0F Screen:
|
|
1823
|
+
|
|
1824
|
+
${description}`);
|
|
1825
|
+
} catch (err2) {
|
|
1826
|
+
chat2.addSystemMessage(`\u274C ${vision.error || "Vision failed"}`);
|
|
1827
|
+
} finally {
|
|
1828
|
+
setStatus("Ready");
|
|
1829
|
+
}
|
|
1830
|
+
}, [chat2, vision]);
|
|
1831
|
+
const handleTelegramToggle = useCallback5(async () => {
|
|
1832
|
+
if (telegram.isEnabled) {
|
|
1833
|
+
await telegram.stop();
|
|
1834
|
+
chat2.addSystemMessage("\u{1F4F1} Telegram stopped.");
|
|
1835
|
+
} else {
|
|
1836
|
+
chat2.addSystemMessage("\u{1F4F1} Starting Telegram...");
|
|
1837
|
+
setStatus("Starting Telegram...");
|
|
1838
|
+
try {
|
|
1839
|
+
await telegram.start();
|
|
1840
|
+
chat2.addSystemMessage(
|
|
1841
|
+
"\u{1F4F1} Telegram started!\nSend /start to your bot to connect.\nCommands: /screen, /describe, /run, /status"
|
|
1842
|
+
);
|
|
1843
|
+
} catch {
|
|
1844
|
+
chat2.addSystemMessage(`\u274C ${telegram.error || "Telegram failed"}`);
|
|
1845
|
+
} finally {
|
|
1846
|
+
setStatus("Ready");
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
}, [chat2, telegram]);
|
|
1850
|
+
const handleTaskCommand = useCallback5(async (description) => {
|
|
1851
|
+
chat2.addSystemMessage(`\u{1F4CB} Parsing: ${description}`);
|
|
1852
|
+
setStatus("Parsing task...");
|
|
1853
|
+
try {
|
|
1854
|
+
const task = await tasks.run(description);
|
|
1855
|
+
chat2.addSystemMessage(`
|
|
1856
|
+
${tasks.format(task)}`);
|
|
1857
|
+
chat2.addSystemMessage(
|
|
1858
|
+
task.status === "completed" ? "\u2705 Task completed!" : "\u274C Task failed."
|
|
1859
|
+
);
|
|
1860
|
+
} catch {
|
|
1861
|
+
chat2.addSystemMessage(`\u274C ${tasks.error || "Task failed"}`);
|
|
1862
|
+
} finally {
|
|
1863
|
+
setStatus("Ready");
|
|
1864
|
+
}
|
|
1865
|
+
}, [chat2, tasks]);
|
|
1866
|
+
const handleSubmit = useCallback5(async (value) => {
|
|
1867
|
+
if (!value.trim()) return;
|
|
1868
|
+
if (value.startsWith("/")) {
|
|
1869
|
+
await handleCommand(value);
|
|
1870
|
+
} else {
|
|
1871
|
+
setStatus("Thinking...");
|
|
1872
|
+
await chat2.sendMessage(value);
|
|
1873
|
+
setStatus("Ready");
|
|
1874
|
+
}
|
|
1875
|
+
}, [chat2, handleCommand]);
|
|
1876
|
+
const handleProviderSelect = useCallback5((provider, model) => {
|
|
1877
|
+
chat2.addSystemMessage(`\u2705 Updated: ${provider} / ${model}`);
|
|
1878
|
+
}, [chat2]);
|
|
1879
|
+
if (overlay === "help") {
|
|
1880
|
+
return /* @__PURE__ */ jsx7(Box7, { flexDirection: "column", height: "100%", alignItems: "center", justifyContent: "center", children: /* @__PURE__ */ jsx7(
|
|
1881
|
+
HelpMenu,
|
|
359
1882
|
{
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
1883
|
+
onClose: () => setOverlay("none"),
|
|
1884
|
+
onSelect: (cmd) => {
|
|
1885
|
+
setOverlay("none");
|
|
1886
|
+
handleCommand(cmd);
|
|
1887
|
+
}
|
|
364
1888
|
}
|
|
365
|
-
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
1889
|
+
) });
|
|
1890
|
+
}
|
|
1891
|
+
if (overlay === "provider") {
|
|
1892
|
+
return /* @__PURE__ */ jsx7(Box7, { flexDirection: "column", height: "100%", alignItems: "center", justifyContent: "center", children: /* @__PURE__ */ jsx7(
|
|
1893
|
+
ProviderSelector,
|
|
1894
|
+
{
|
|
1895
|
+
onClose: () => setOverlay("none"),
|
|
1896
|
+
onSelect: handleProviderSelect
|
|
1897
|
+
}
|
|
1898
|
+
) });
|
|
1899
|
+
}
|
|
1900
|
+
const visibleMessages = chat2.messages.slice(-20);
|
|
1901
|
+
const isProcessing = chat2.isProcessing || vision.isAnalyzing || tasks.isRunning || telegram.isStarting;
|
|
1902
|
+
return /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", height: "100%", children: [
|
|
1903
|
+
/* @__PURE__ */ jsx7(Header, { screenWatch, telegramEnabled: telegram.isEnabled }),
|
|
1904
|
+
/* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", flexGrow: 1, borderStyle: "round", borderColor: "gray", padding: 1, children: [
|
|
1905
|
+
/* @__PURE__ */ jsx7(Text7, { bold: true, color: "gray", children: " Chat " }),
|
|
1906
|
+
visibleMessages.map((msg) => /* @__PURE__ */ jsx7(
|
|
373
1907
|
ChatMessage,
|
|
374
1908
|
{
|
|
375
1909
|
role: msg.role,
|
|
@@ -380,25 +1914,26 @@ function App() {
|
|
|
380
1914
|
msg.id
|
|
381
1915
|
))
|
|
382
1916
|
] }),
|
|
383
|
-
error && /* @__PURE__ */
|
|
1917
|
+
chat2.error && /* @__PURE__ */ jsx7(Box7, { marginY: 1, children: /* @__PURE__ */ jsxs6(Text7, { color: "red", children: [
|
|
384
1918
|
"Error: ",
|
|
385
|
-
error
|
|
1919
|
+
chat2.error
|
|
386
1920
|
] }) }),
|
|
387
|
-
/* @__PURE__ */
|
|
1921
|
+
/* @__PURE__ */ jsx7(
|
|
388
1922
|
ChatInput,
|
|
389
1923
|
{
|
|
390
|
-
value:
|
|
391
|
-
onChange:
|
|
1924
|
+
value: "",
|
|
1925
|
+
onChange: () => {
|
|
1926
|
+
},
|
|
392
1927
|
onSubmit: handleSubmit,
|
|
393
1928
|
isProcessing
|
|
394
1929
|
}
|
|
395
1930
|
),
|
|
396
|
-
/* @__PURE__ */
|
|
1931
|
+
/* @__PURE__ */ jsx7(StatusBar, { status })
|
|
397
1932
|
] });
|
|
398
1933
|
}
|
|
399
1934
|
|
|
400
1935
|
// src/index.tsx
|
|
401
|
-
import { jsx as
|
|
1936
|
+
import { jsx as jsx8 } from "react/jsx-runtime";
|
|
402
1937
|
var args = process.argv.slice(2);
|
|
403
1938
|
if (args.length > 0) {
|
|
404
1939
|
const command = args[0];
|
|
@@ -441,15 +1976,15 @@ if (args.length > 0) {
|
|
|
441
1976
|
process.exit(0);
|
|
442
1977
|
}
|
|
443
1978
|
if (subcommand === "show" || !subcommand) {
|
|
444
|
-
const
|
|
1979
|
+
const config = getConfig();
|
|
445
1980
|
console.log("\nC-napse Configuration:");
|
|
446
|
-
console.log(` Provider: ${
|
|
447
|
-
console.log(` Model: ${
|
|
448
|
-
console.log(` Ollama Host: ${
|
|
1981
|
+
console.log(` Provider: ${config.provider}`);
|
|
1982
|
+
console.log(` Model: ${config.model}`);
|
|
1983
|
+
console.log(` Ollama Host: ${config.ollamaHost}`);
|
|
449
1984
|
console.log(` API Keys configured:`);
|
|
450
|
-
console.log(` - OpenRouter: ${
|
|
451
|
-
console.log(` - Anthropic: ${
|
|
452
|
-
console.log(` - OpenAI: ${
|
|
1985
|
+
console.log(` - OpenRouter: ${config.apiKeys.openrouter ? "\u2713" : "\u2717"}`);
|
|
1986
|
+
console.log(` - Anthropic: ${config.apiKeys.anthropic ? "\u2713" : "\u2717"}`);
|
|
1987
|
+
console.log(` - OpenAI: ${config.apiKeys.openai ? "\u2713" : "\u2717"}`);
|
|
453
1988
|
console.log("");
|
|
454
1989
|
process.exit(0);
|
|
455
1990
|
}
|
|
@@ -493,45 +2028,8 @@ Manual Setup:
|
|
|
493
2028
|
process.exit(0);
|
|
494
2029
|
}
|
|
495
2030
|
case "init": {
|
|
496
|
-
const
|
|
497
|
-
|
|
498
|
-
input: process.stdin,
|
|
499
|
-
output: process.stdout
|
|
500
|
-
});
|
|
501
|
-
const question = (q) => new Promise((resolve) => rl.question(q, resolve));
|
|
502
|
-
console.log("\n\u{1F680} C-napse Setup\n");
|
|
503
|
-
console.log("Select a provider:");
|
|
504
|
-
console.log(" 1. ollama - Local AI (free, requires Ollama installed)");
|
|
505
|
-
console.log(" 2. openrouter - OpenRouter API (pay per use, many models)");
|
|
506
|
-
console.log(" 3. anthropic - Anthropic Claude (pay per use)");
|
|
507
|
-
console.log(" 4. openai - OpenAI GPT (pay per use)");
|
|
508
|
-
console.log("");
|
|
509
|
-
const providerChoice = await question("Enter choice (1-4) [1]: ");
|
|
510
|
-
const providers = ["ollama", "openrouter", "anthropic", "openai"];
|
|
511
|
-
const providerIndex = parseInt(providerChoice || "1") - 1;
|
|
512
|
-
const provider = providers[providerIndex] || "ollama";
|
|
513
|
-
setProvider(provider);
|
|
514
|
-
console.log(`\u2713 Provider set to: ${provider}`);
|
|
515
|
-
if (provider !== "ollama") {
|
|
516
|
-
const apiKey = await question(`
|
|
517
|
-
Enter your ${provider} API key: `);
|
|
518
|
-
if (apiKey) {
|
|
519
|
-
setApiKey(provider, apiKey);
|
|
520
|
-
console.log(`\u2713 API key saved`);
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
const defaultModels = {
|
|
524
|
-
ollama: "qwen2.5:0.5b",
|
|
525
|
-
openrouter: "qwen/qwen-2.5-coder-32b-instruct",
|
|
526
|
-
anthropic: "claude-3-5-sonnet-20241022",
|
|
527
|
-
openai: "gpt-4o"
|
|
528
|
-
};
|
|
529
|
-
const model = await question(`
|
|
530
|
-
Model [${defaultModels[provider]}]: `);
|
|
531
|
-
setModel(model || defaultModels[provider]);
|
|
532
|
-
console.log(`\u2713 Model set to: ${model || defaultModels[provider]}`);
|
|
533
|
-
rl.close();
|
|
534
|
-
console.log("\n\u2705 Setup complete! Run `cnapse` to start chatting.\n");
|
|
2031
|
+
const { Setup } = await import("./Setup-Q32JPHGP.js");
|
|
2032
|
+
render(/* @__PURE__ */ jsx8(Setup, {}));
|
|
535
2033
|
process.exit(0);
|
|
536
2034
|
}
|
|
537
2035
|
default: {
|
|
@@ -539,4 +2037,4 @@ Model [${defaultModels[provider]}]: `);
|
|
|
539
2037
|
}
|
|
540
2038
|
}
|
|
541
2039
|
}
|
|
542
|
-
render(/* @__PURE__ */
|
|
2040
|
+
render(/* @__PURE__ */ jsx8(App, {}));
|