@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/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 Box5, Text as Text5, useApp, useInput } from "ink";
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 config2 = getConfig();
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
- config2.provider,
33
+ config.provider,
68
34
  " \u2502 ",
69
- config2.model
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 config2 = getConfig();
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 (config2.provider) {
345
+ switch (config.provider) {
147
346
  case "openrouter":
148
- return chatOpenRouter(allMessages, config2.model);
347
+ return chatOpenRouter(allMessages, config.model);
149
348
  case "ollama":
150
- return chatOllama(allMessages, config2.model);
349
+ return chatOllama(allMessages, config.model);
151
350
  case "anthropic":
152
- return chatAnthropic(allMessages, config2.model);
351
+ return chatAnthropic(allMessages, config.model);
153
352
  case "openai":
154
- return chatOpenAI(allMessages, config2.model);
353
+ return chatOpenAI(allMessages, config.model);
155
354
  default:
156
- throw new Error(`Unknown provider: ${config2.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 config2 = getConfig();
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": config2.openrouter.siteUrl,
171
- "X-Title": config2.openrouter.appName
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 config2 = getConfig();
190
- const response = await fetch(`${config2.ollamaHost}/api/chat`, {
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/components/App.tsx
264
- import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
265
- function App() {
266
- const { exit } = useApp();
267
- const [messages, setMessages] = useState([
268
- {
269
- id: "0",
270
- role: "system",
271
- content: "Welcome to C-napse! Type your message and press Enter.\n\nShortcuts:\n Ctrl+C - Exit\n /clear - Clear chat\n /help - Show help",
272
- timestamp: /* @__PURE__ */ new Date()
273
- }
274
- ]);
275
- const [input, setInput] = useState("");
276
- const [isProcessing, setIsProcessing] = useState(false);
277
- const [status, setStatus] = useState("Ready");
278
- const [error, setError] = useState(null);
279
- useInput((inputChar, key) => {
280
- if (key.ctrl && inputChar === "c") {
281
- exit();
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
- if (key.ctrl && inputChar === "l") {
284
- setMessages([messages[0]]);
285
- setError(null);
286
- }
287
- });
288
- const handleSubmit = async (value) => {
289
- if (!value.trim() || isProcessing) return;
290
- const userInput = value.trim();
291
- setInput("");
292
- setError(null);
293
- if (userInput.startsWith("/")) {
294
- handleCommand(userInput);
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 userMsg = {
298
- id: Date.now().toString(),
299
- role: "user",
300
- content: userInput,
301
- timestamp: /* @__PURE__ */ new Date()
505
+ const checkScreen = async () => {
506
+ const desc = await getScreenDescription();
507
+ if (desc) {
508
+ screenContextRef.current = desc;
509
+ }
302
510
  };
303
- setMessages((prev) => [...prev, userMsg]);
304
- const assistantId = (Date.now() + 1).toString();
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: assistantId,
309
- role: "assistant",
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
- apiMessages.push({ role: "user", content: userInput });
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 (err) {
327
- const errorMsg = err instanceof Error ? err.message : "Unknown error";
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
- const handleCommand = (cmd) => {
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
- setMessages([messages[0]]);
345
- addSystemMessage("Chat cleared.");
1744
+ chat2.clearMessages();
1745
+ chat2.addSystemMessage("Chat cleared.");
346
1746
  break;
347
1747
  case "help":
348
- addSystemMessage(
349
- "Commands:\n /clear - Clear chat history\n /help - Show this help\n\nJust type naturally to chat with the AI!"
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 addSystemMessage = (content) => {
357
- setMessages((prev) => [
358
- ...prev,
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
- id: Date.now().toString(),
361
- role: "system",
362
- content,
363
- timestamp: /* @__PURE__ */ new Date()
1883
+ onClose: () => setOverlay("none"),
1884
+ onSelect: (cmd) => {
1885
+ setOverlay("none");
1886
+ handleCommand(cmd);
1887
+ }
364
1888
  }
365
- ]);
366
- };
367
- const visibleMessages = messages.slice(-20);
368
- return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", height: "100%", children: [
369
- /* @__PURE__ */ jsx5(Header, {}),
370
- /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", flexGrow: 1, borderStyle: "round", borderColor: "gray", padding: 1, children: [
371
- /* @__PURE__ */ jsx5(Text5, { bold: true, color: "gray", children: " Chat " }),
372
- visibleMessages.map((msg) => /* @__PURE__ */ jsx5(
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__ */ jsx5(Box5, { marginY: 1, children: /* @__PURE__ */ jsxs4(Text5, { color: "red", children: [
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__ */ jsx5(
1921
+ /* @__PURE__ */ jsx7(
388
1922
  ChatInput,
389
1923
  {
390
- value: input,
391
- onChange: setInput,
1924
+ value: "",
1925
+ onChange: () => {
1926
+ },
392
1927
  onSubmit: handleSubmit,
393
1928
  isProcessing
394
1929
  }
395
1930
  ),
396
- /* @__PURE__ */ jsx5(StatusBar, { status })
1931
+ /* @__PURE__ */ jsx7(StatusBar, { status })
397
1932
  ] });
398
1933
  }
399
1934
 
400
1935
  // src/index.tsx
401
- import { jsx as jsx6 } from "react/jsx-runtime";
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 config2 = getConfig();
1979
+ const config = getConfig();
445
1980
  console.log("\nC-napse Configuration:");
446
- console.log(` Provider: ${config2.provider}`);
447
- console.log(` Model: ${config2.model}`);
448
- console.log(` Ollama Host: ${config2.ollamaHost}`);
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: ${config2.apiKeys.openrouter ? "\u2713" : "\u2717"}`);
451
- console.log(` - Anthropic: ${config2.apiKeys.anthropic ? "\u2713" : "\u2717"}`);
452
- console.log(` - OpenAI: ${config2.apiKeys.openai ? "\u2713" : "\u2717"}`);
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 readline = await import("readline");
497
- const rl = readline.createInterface({
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__ */ jsx6(App, {}));
2040
+ render(/* @__PURE__ */ jsx8(App, {}));