@projectservan8n/cnapse 0.6.0 → 0.6.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@projectservan8n/cnapse",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "Autonomous PC intelligence - AI assistant for desktop automation",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -202,6 +202,21 @@ export function App() {
202
202
  }
203
203
  }, [chat, tasks]);
204
204
 
205
+ // Check if message looks like a computer control request
206
+ const isComputerControlRequest = useCallback((text: string): boolean => {
207
+ const lower = text.toLowerCase();
208
+ const patterns = [
209
+ /^(can you |please |)?(open|close|minimize|maximize|restore|focus|click|type|press|scroll|move|drag)/i,
210
+ /^(can you |please |)?move (the |my |)mouse/i,
211
+ /^(can you |please |)?(start|launch|run) [a-z]/i,
212
+ /(open|close|minimize|maximize) (the |my |)?[a-z]/i,
213
+ /click (on |the |)/i,
214
+ /type ["'].+["']/i,
215
+ /press (enter|escape|tab|ctrl|alt|shift|space|backspace|delete|f\d+)/i,
216
+ ];
217
+ return patterns.some(p => p.test(lower));
218
+ }, []);
219
+
205
220
  // Submit handler
206
221
  const handleSubmit = useCallback(async (value: string) => {
207
222
  if (!value.trim()) return;
@@ -209,12 +224,16 @@ export function App() {
209
224
 
210
225
  if (value.startsWith('/')) {
211
226
  await handleCommand(value);
227
+ } else if (isComputerControlRequest(value)) {
228
+ // Auto-route to task system for computer control
229
+ chat.addSystemMessage(`🤖 Executing: ${value}`);
230
+ await handleTaskCommand(value);
212
231
  } else {
213
232
  setStatus('Thinking...');
214
233
  await chat.sendMessage(value);
215
234
  setStatus('Ready');
216
235
  }
217
- }, [chat, handleCommand]);
236
+ }, [chat, handleCommand, handleTaskCommand, isComputerControlRequest]);
218
237
 
219
238
  // Provider selection callback
220
239
  const handleProviderSelect = useCallback((provider: string, model: string) => {
package/src/lib/tasks.ts CHANGED
@@ -7,6 +7,8 @@
7
7
  import { chat, Message } from './api.js';
8
8
  import * as computer from '../tools/computer.js';
9
9
  import { describeScreen } from './vision.js';
10
+ import * as filesystem from '../tools/filesystem.js';
11
+ import { runCommand } from '../tools/shell.js';
10
12
  import * as fs from 'fs';
11
13
  import * as path from 'path';
12
14
  import * as os from 'os';
@@ -185,14 +187,36 @@ Before outputting steps, THINK through these questions:
185
187
  - Typing too fast -> add small waits
186
188
 
187
189
  ## AVAILABLE ACTIONS
190
+
191
+ ### App Control
188
192
  - open_app: Open app via Run dialog (e.g., "open_app:notepad", "open_app:code", "open_app:chrome")
193
+ - open_folder: Open VS Code with folder (e.g., "open_folder:E:/MyProject")
194
+ - focus_window: Focus by title (e.g., "focus_window:Notepad")
195
+
196
+ ### Input
189
197
  - type_text: Type text string (e.g., "type_text:Hello World")
190
198
  - press_key: Single key (e.g., "press_key:enter", "press_key:escape", "press_key:tab")
191
199
  - key_combo: Key combination (e.g., "key_combo:control+s", "key_combo:alt+f4", "key_combo:meta+r")
192
200
  - click: Mouse click (e.g., "click:left", "click:right")
201
+
202
+ ### File Operations
203
+ - read_file: Read file contents (e.g., "read_file:E:/test/index.html")
204
+ - write_file: Write content to file (e.g., "write_file:E:/test/output.txt|Hello World")
205
+ - list_files: List files in directory (e.g., "list_files:E:/test")
206
+
207
+ ### AI Coding
208
+ - generate_code: AI generates code based on description (e.g., "generate_code:E:/test/index.html|create an HTML page with input on left, output on right")
209
+ - edit_code: AI modifies existing code (e.g., "edit_code:E:/test/app.js|add error handling to the fetch calls")
210
+
211
+ ### Web Browsing
212
+ - open_url: Open URL in default browser (e.g., "open_url:https://perplexity.ai")
213
+ - browse_and_ask: Open AI website, type question, wait for response (e.g., "browse_and_ask:perplexity|What is the capital of France?")
214
+ - browse_and_ask: Supports: perplexity, chatgpt, claude, google
215
+
216
+ ### Utility
193
217
  - wait: Wait N seconds (e.g., "wait:2" - use 1-3s for app loads)
194
- - focus_window: Focus by title (e.g., "focus_window:Notepad")
195
218
  - screenshot: Capture and describe screen
219
+ - shell: Run shell command (e.g., "shell:npm install")
196
220
  ${learnedExamples}
197
221
  ## EXAMPLES WITH REASONING
198
222
 
@@ -235,6 +259,59 @@ Output:
235
259
  { "description": "Close active window with Alt+F4", "action": "key_combo:alt+f4" }
236
260
  ]
237
261
 
262
+ ### Example 4: "open folder E:/Test in vscode and create an HTML editor"
263
+ Thinking:
264
+ - Goal: Open VS Code with folder, then create/edit HTML file to be an editor
265
+ - How: Use open_folder to launch VS Code with the folder, then use AI to generate code
266
+ - Sequence: Open folder -> List files to see what exists -> Generate/edit the HTML
267
+ - Edge case: File might not exist yet
268
+
269
+ Output:
270
+ [
271
+ { "description": "Open VS Code with the Test folder", "action": "open_folder:E:/Test" },
272
+ { "description": "Wait for VS Code to load", "action": "wait:3" },
273
+ { "description": "List files in the folder", "action": "list_files:E:/Test" },
274
+ { "description": "Generate HTML editor code", "action": "generate_code:E:/Test/editor.html|Create an HTML page with a code editor layout: textarea input on the left side, live preview output on the right side. Include basic CSS for split layout and JavaScript to update preview on input." }
275
+ ]
276
+
277
+ ### Example 5: "read the config.json and add a new setting"
278
+ Thinking:
279
+ - Goal: Read existing file, understand it, modify it
280
+ - How: read_file to get contents, then edit_code to modify
281
+ - Sequence: Read first, then edit
282
+
283
+ Output:
284
+ [
285
+ { "description": "Read the config file", "action": "read_file:config.json" },
286
+ { "description": "Add new setting to config", "action": "edit_code:config.json|add a new setting called 'darkMode' with value true" }
287
+ ]
288
+
289
+ ### Example 6: "ask perplexity what is the best programming language"
290
+ Thinking:
291
+ - Goal: Open Perplexity AI in browser and ask a question
292
+ - How: Use browse_and_ask with perplexity target
293
+ - Sequence: Open site -> type question -> wait for response -> screenshot result
294
+
295
+ Output:
296
+ [
297
+ { "description": "Ask Perplexity the question", "action": "browse_and_ask:perplexity|what is the best programming language" },
298
+ { "description": "Wait for response to generate", "action": "wait:5" },
299
+ { "description": "Capture the response", "action": "screenshot" }
300
+ ]
301
+
302
+ ### Example 7: "search google for weather today"
303
+ Thinking:
304
+ - Goal: Open Google and search for something
305
+ - How: Use browse_and_ask with google target
306
+ - Sequence: Open Google, search, capture results
307
+
308
+ Output:
309
+ [
310
+ { "description": "Search Google", "action": "browse_and_ask:google|weather today" },
311
+ { "description": "Wait for results", "action": "wait:2" },
312
+ { "description": "Capture search results", "action": "screenshot" }
313
+ ]
314
+
238
315
  ## YOUR TASK
239
316
  Now parse this request: "${input}"
240
317
 
@@ -349,6 +426,171 @@ async function executeStep(step: TaskStep): Promise<void> {
349
426
  step.result = `Focused window: ${params}`;
350
427
  break;
351
428
 
429
+ case 'open_folder':
430
+ // Open VS Code with a specific folder
431
+ await runCommand(`code "${params}"`, 10000);
432
+ step.result = `Opened VS Code with folder: ${params}`;
433
+ break;
434
+
435
+ case 'read_file': {
436
+ const readResult = await filesystem.readFile(params);
437
+ if (readResult.success) {
438
+ step.result = readResult.output;
439
+ } else {
440
+ throw new Error(readResult.error || 'Failed to read file');
441
+ }
442
+ break;
443
+ }
444
+
445
+ case 'write_file': {
446
+ // Format: write_file:path|content
447
+ const [filePath, ...contentParts] = params.split('|');
448
+ const content = contentParts.join('|');
449
+ const writeResult = await filesystem.writeFile(filePath, content);
450
+ if (writeResult.success) {
451
+ step.result = `Written to ${filePath}`;
452
+ } else {
453
+ throw new Error(writeResult.error || 'Failed to write file');
454
+ }
455
+ break;
456
+ }
457
+
458
+ case 'list_files': {
459
+ const listResult = await filesystem.listDir(params, false);
460
+ if (listResult.success) {
461
+ step.result = listResult.output;
462
+ } else {
463
+ throw new Error(listResult.error || 'Failed to list files');
464
+ }
465
+ break;
466
+ }
467
+
468
+ case 'generate_code': {
469
+ // Format: generate_code:path|description
470
+ const [codePath, ...descParts] = params.split('|');
471
+ const codeDescription = descParts.join('|');
472
+
473
+ // Ask AI to generate the code
474
+ const codePrompt = `Generate complete, working code for this request. Output ONLY the code, no explanations or markdown:
475
+
476
+ Request: ${codeDescription}
477
+
478
+ File: ${codePath}`;
479
+
480
+ const codeResponse = await chat([{ role: 'user', content: codePrompt }]);
481
+ let generatedCode = codeResponse.content;
482
+
483
+ // Strip markdown code blocks if present
484
+ generatedCode = generatedCode.replace(/^```[\w]*\n?/gm, '').replace(/\n?```$/gm, '').trim();
485
+
486
+ // Write the generated code to file
487
+ const genResult = await filesystem.writeFile(codePath, generatedCode);
488
+ if (genResult.success) {
489
+ step.result = `Generated and saved code to ${codePath}`;
490
+ } else {
491
+ throw new Error(genResult.error || 'Failed to write generated code');
492
+ }
493
+ break;
494
+ }
495
+
496
+ case 'edit_code': {
497
+ // Format: edit_code:path|instructions
498
+ const [editPath, ...instrParts] = params.split('|');
499
+ const instructions = instrParts.join('|');
500
+
501
+ // Read existing file
502
+ const existingResult = await filesystem.readFile(editPath);
503
+ if (!existingResult.success) {
504
+ throw new Error(`Cannot read file: ${existingResult.error}`);
505
+ }
506
+
507
+ // Ask AI to edit the code
508
+ const editPrompt = `Edit this code according to the instructions. Output ONLY the complete modified code, no explanations or markdown:
509
+
510
+ Instructions: ${instructions}
511
+
512
+ Current code:
513
+ ${existingResult.output}`;
514
+
515
+ const editResponse = await chat([{ role: 'user', content: editPrompt }]);
516
+ let editedCode = editResponse.content;
517
+
518
+ // Strip markdown code blocks if present
519
+ editedCode = editedCode.replace(/^```[\w]*\n?/gm, '').replace(/\n?```$/gm, '').trim();
520
+
521
+ // Write the edited code back
522
+ const editWriteResult = await filesystem.writeFile(editPath, editedCode);
523
+ if (editWriteResult.success) {
524
+ step.result = `Edited and saved ${editPath}`;
525
+ } else {
526
+ throw new Error(editWriteResult.error || 'Failed to write edited code');
527
+ }
528
+ break;
529
+ }
530
+
531
+ case 'shell': {
532
+ const shellResult = await runCommand(params, 30000);
533
+ if (shellResult.success) {
534
+ step.result = shellResult.output || 'Command completed';
535
+ } else {
536
+ throw new Error(shellResult.error || 'Command failed');
537
+ }
538
+ break;
539
+ }
540
+
541
+ case 'open_url': {
542
+ // Open URL in default browser
543
+ const url = params.startsWith('http') ? params : `https://${params}`;
544
+ if (process.platform === 'win32') {
545
+ await runCommand(`start "" "${url}"`, 5000);
546
+ } else if (process.platform === 'darwin') {
547
+ await runCommand(`open "${url}"`, 5000);
548
+ } else {
549
+ await runCommand(`xdg-open "${url}"`, 5000);
550
+ }
551
+ step.result = `Opened ${url} in browser`;
552
+ break;
553
+ }
554
+
555
+ case 'browse_and_ask': {
556
+ // Format: browse_and_ask:site|question
557
+ const [site, ...questionParts] = params.split('|');
558
+ const question = questionParts.join('|');
559
+
560
+ // Site-specific URLs and input selectors
561
+ const sites: Record<string, { url: string; waitTime: number; searchSelector?: string }> = {
562
+ perplexity: { url: 'https://www.perplexity.ai', waitTime: 3 },
563
+ chatgpt: { url: 'https://chat.openai.com', waitTime: 4 },
564
+ claude: { url: 'https://claude.ai', waitTime: 4 },
565
+ google: { url: 'https://www.google.com', waitTime: 2 },
566
+ bing: { url: 'https://www.bing.com', waitTime: 2 },
567
+ };
568
+
569
+ const siteConfig = sites[site.toLowerCase()] || { url: `https://${site}`, waitTime: 3 };
570
+
571
+ // Open the site
572
+ if (process.platform === 'win32') {
573
+ await runCommand(`start "" "${siteConfig.url}"`, 5000);
574
+ } else if (process.platform === 'darwin') {
575
+ await runCommand(`open "${siteConfig.url}"`, 5000);
576
+ } else {
577
+ await runCommand(`xdg-open "${siteConfig.url}"`, 5000);
578
+ }
579
+
580
+ // Wait for page to load
581
+ await sleep(siteConfig.waitTime * 1000);
582
+
583
+ // Type the question (most sites have autofocus on search/input)
584
+ await computer.typeText(question);
585
+ await sleep(300);
586
+
587
+ // Press Enter to submit
588
+ await computer.pressKey('Return');
589
+
590
+ step.result = `Asked ${site}: "${question}"`;
591
+ break;
592
+ }
593
+
352
594
  case 'screenshot':
353
595
  const vision = await describeScreen();
354
596
  step.result = vision.description;
@@ -7,6 +7,7 @@ import { getConfig, getApiKey } from '../lib/config.js';
7
7
  import { describeScreen, captureScreenshot } from '../lib/vision.js';
8
8
  import { runCommand } from '../tools/shell.js';
9
9
  import { chat as chatWithAI, chatWithVision, Message } from '../lib/api.js';
10
+ import * as computer from '../tools/computer.js';
10
11
 
11
12
  export interface TelegramMessage {
12
13
  chatId: number;
@@ -22,6 +23,101 @@ export interface TelegramBotEvents {
22
23
  stopped: () => void;
23
24
  }
24
25
 
26
+ /**
27
+ * Convert markdown to Telegram-safe format (MarkdownV2)
28
+ * Escapes special characters and converts some markdown syntax
29
+ */
30
+ function formatForTelegram(text: string): { text: string; parseMode: 'MarkdownV2' | undefined } {
31
+ // Check if text has markdown that could be rendered
32
+ const hasMarkdown = /[*_`\[\]()]/.test(text);
33
+
34
+ if (!hasMarkdown) {
35
+ return { text, parseMode: undefined };
36
+ }
37
+
38
+ try {
39
+ // Convert to Telegram MarkdownV2 format
40
+ let formatted = text;
41
+
42
+ // First, escape special characters that aren't part of markdown
43
+ // MarkdownV2 requires escaping: _ * [ ] ( ) ~ ` > # + - = | { } . !
44
+ const escapeChars = ['\\', '_', '*', '[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!'];
45
+
46
+ // Temporarily replace valid markdown with placeholders
47
+ const placeholders: { placeholder: string; original: string }[] = [];
48
+ let placeholderIndex = 0;
49
+
50
+ // Protect code blocks (```code```)
51
+ formatted = formatted.replace(/```([\s\S]*?)```/g, (match, code) => {
52
+ const placeholder = `__CODEBLOCK_${placeholderIndex++}__`;
53
+ placeholders.push({ placeholder, original: '```' + code.replace(/\\/g, '\\\\') + '```' });
54
+ return placeholder;
55
+ });
56
+
57
+ // Protect inline code (`code`)
58
+ formatted = formatted.replace(/`([^`]+)`/g, (match, code) => {
59
+ const placeholder = `__INLINECODE_${placeholderIndex++}__`;
60
+ placeholders.push({ placeholder, original: '`' + code.replace(/\\/g, '\\\\') + '`' });
61
+ return placeholder;
62
+ });
63
+
64
+ // Protect bold (**text** or __text__)
65
+ formatted = formatted.replace(/\*\*(.+?)\*\*/g, (match, text) => {
66
+ const placeholder = `__BOLD_${placeholderIndex++}__`;
67
+ placeholders.push({ placeholder, original: '*' + text + '*' });
68
+ return placeholder;
69
+ });
70
+
71
+ // Protect italic (*text* or _text_) - but only single asterisks
72
+ formatted = formatted.replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, (match, text) => {
73
+ const placeholder = `__ITALIC_${placeholderIndex++}__`;
74
+ placeholders.push({ placeholder, original: '_' + text + '_' });
75
+ return placeholder;
76
+ });
77
+
78
+ // Protect links [text](url)
79
+ formatted = formatted.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, text, url) => {
80
+ const placeholder = `__LINK_${placeholderIndex++}__`;
81
+ placeholders.push({ placeholder, original: '[' + text + '](' + url + ')' });
82
+ return placeholder;
83
+ });
84
+
85
+ // Now escape remaining special characters
86
+ for (const char of escapeChars) {
87
+ if (char === '\\') continue; // Skip backslash for now
88
+ formatted = formatted.split(char).join('\\' + char);
89
+ }
90
+
91
+ // Restore placeholders
92
+ for (const { placeholder, original } of placeholders) {
93
+ formatted = formatted.replace(placeholder, original);
94
+ }
95
+
96
+ return { text: formatted, parseMode: 'MarkdownV2' };
97
+ } catch {
98
+ // If formatting fails, return plain text
99
+ return { text, parseMode: undefined };
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Send a message with proper formatting, falling back to plain text if markdown fails
105
+ */
106
+ async function sendFormattedMessage(ctx: any, text: string): Promise<void> {
107
+ const { text: formatted, parseMode } = formatForTelegram(text);
108
+
109
+ try {
110
+ if (parseMode) {
111
+ await ctx.reply(formatted, { parse_mode: parseMode });
112
+ } else {
113
+ await ctx.reply(text);
114
+ }
115
+ } catch {
116
+ // If markdown parsing fails, send as plain text
117
+ await ctx.reply(text);
118
+ }
119
+ }
120
+
25
121
  export class TelegramBotService extends EventEmitter {
26
122
  private bot: any = null;
27
123
  private isRunning = false;
@@ -248,6 +344,14 @@ export class TelegramBotService extends EventEmitter {
248
344
  // Send typing indicator
249
345
  await ctx.sendChatAction('typing');
250
346
 
347
+ // Check if this looks like a computer control request
348
+ const computerControlResult = await this.tryComputerControl(userText);
349
+ if (computerControlResult) {
350
+ await sendFormattedMessage(ctx, computerControlResult);
351
+ history.push({ role: 'assistant', content: computerControlResult });
352
+ return;
353
+ }
354
+
251
355
  // Check if this looks like a screen/vision request
252
356
  const isVisionRequest = /screen|see|look|what('?s| is) (on|visible)|show me|screenshot/i.test(userText);
253
357
 
@@ -267,16 +371,16 @@ export class TelegramBotService extends EventEmitter {
267
371
  // Add assistant response to history
268
372
  history.push({ role: 'assistant', content: response.content });
269
373
 
270
- // Send response (split if too long for Telegram)
374
+ // Send response with proper formatting (split if too long for Telegram)
271
375
  const responseText = response.content || '(no response)';
272
376
  if (responseText.length > 4000) {
273
377
  // Split into chunks
274
378
  const chunks = responseText.match(/.{1,4000}/gs) || [responseText];
275
379
  for (const chunk of chunks) {
276
- await ctx.reply(chunk);
380
+ await sendFormattedMessage(ctx, chunk);
277
381
  }
278
382
  } else {
279
- await ctx.reply(responseText);
383
+ await sendFormattedMessage(ctx, responseText);
280
384
  }
281
385
  } catch (error) {
282
386
  const errorMsg = error instanceof Error ? error.message : 'Unknown error';
@@ -302,6 +406,117 @@ export class TelegramBotService extends EventEmitter {
302
406
  return this.allowedChatIds.has(chatId);
303
407
  }
304
408
 
409
+ /**
410
+ * Try to execute computer control commands directly
411
+ * Returns response string if handled, null if not a computer command
412
+ */
413
+ private async tryComputerControl(text: string): Promise<string | null> {
414
+ const lower = text.toLowerCase();
415
+
416
+ // Minimize window
417
+ let match = lower.match(/minimize\s+(?:the\s+)?(.+)/i);
418
+ if (match) {
419
+ const result = await computer.minimizeWindow(match[1].trim());
420
+ return result.success ? `✅ ${result.output}` : `❌ ${result.error}`;
421
+ }
422
+
423
+ // Maximize window
424
+ match = lower.match(/maximize\s+(?:the\s+)?(.+)/i);
425
+ if (match) {
426
+ const result = await computer.maximizeWindow(match[1].trim());
427
+ return result.success ? `✅ ${result.output}` : `❌ ${result.error}`;
428
+ }
429
+
430
+ // Close window
431
+ match = lower.match(/close\s+(?:the\s+)?(.+)/i);
432
+ if (match) {
433
+ const result = await computer.closeWindow(match[1].trim());
434
+ return result.success ? `✅ ${result.output}` : `❌ ${result.error}`;
435
+ }
436
+
437
+ // Restore window
438
+ match = lower.match(/restore\s+(?:the\s+)?(.+)/i);
439
+ if (match) {
440
+ const result = await computer.restoreWindow(match[1].trim());
441
+ return result.success ? `✅ ${result.output}` : `❌ ${result.error}`;
442
+ }
443
+
444
+ // Focus/open window
445
+ match = lower.match(/(?:focus|open|switch to)\s+(?:the\s+)?(.+)/i);
446
+ if (match) {
447
+ const result = await computer.focusWindow(match[1].trim());
448
+ return result.success ? `✅ ${result.output}` : `❌ ${result.error}`;
449
+ }
450
+
451
+ // Type text
452
+ match = text.match(/type\s+["'](.+)["']/i);
453
+ if (match) {
454
+ const result = await computer.typeText(match[1]);
455
+ return result.success ? `✅ ${result.output}` : `❌ ${result.error}`;
456
+ }
457
+
458
+ // Press key
459
+ match = lower.match(/press\s+(?:the\s+)?(\w+)/i);
460
+ if (match) {
461
+ const result = await computer.pressKey(match[1]);
462
+ return result.success ? `✅ ${result.output}` : `❌ ${result.error}`;
463
+ }
464
+
465
+ // Click
466
+ if (/^click$/i.test(lower) || /click\s+(?:the\s+)?mouse/i.test(lower)) {
467
+ const result = await computer.clickMouse('left');
468
+ return result.success ? `✅ ${result.output}` : `❌ ${result.error}`;
469
+ }
470
+
471
+ // Right click
472
+ if (/right\s*click/i.test(lower)) {
473
+ const result = await computer.clickMouse('right');
474
+ return result.success ? `✅ ${result.output}` : `❌ ${result.error}`;
475
+ }
476
+
477
+ // Double click
478
+ if (/double\s*click/i.test(lower)) {
479
+ const result = await computer.doubleClick();
480
+ return result.success ? `✅ ${result.output}` : `❌ ${result.error}`;
481
+ }
482
+
483
+ // Move mouse to coordinates
484
+ match = lower.match(/move\s+(?:the\s+)?mouse\s+(?:to\s+)?(\d+)[,\s]+(\d+)/i);
485
+ if (match) {
486
+ const result = await computer.moveMouse(parseInt(match[1]), parseInt(match[2]));
487
+ return result.success ? `✅ ${result.output}` : `❌ ${result.error}`;
488
+ }
489
+
490
+ // Scroll
491
+ match = lower.match(/scroll\s+(up|down)(?:\s+(\d+))?/i);
492
+ if (match) {
493
+ const amount = match[1] === 'up' ? (parseInt(match[2]) || 3) : -(parseInt(match[2]) || 3);
494
+ const result = await computer.scrollMouse(amount);
495
+ return result.success ? `✅ ${result.output}` : `❌ ${result.error}`;
496
+ }
497
+
498
+ // List windows
499
+ if (/list\s+(?:all\s+)?windows/i.test(lower) || /what\s+windows/i.test(lower)) {
500
+ const result = await computer.listWindows();
501
+ return result.success ? `📋 Open Windows:\n${result.output}` : `❌ ${result.error}`;
502
+ }
503
+
504
+ // Get active window
505
+ if (/(?:active|current|focused)\s+window/i.test(lower) || /what\s+(?:window|app)/i.test(lower)) {
506
+ const result = await computer.getActiveWindow();
507
+ return result.success ? `🪟 Active: ${result.output}` : `❌ ${result.error}`;
508
+ }
509
+
510
+ // Mouse position
511
+ if (/mouse\s+position/i.test(lower) || /where.*mouse/i.test(lower)) {
512
+ const result = await computer.getMousePosition();
513
+ return result.success ? `🖱️ ${result.output}` : `❌ ${result.error}`;
514
+ }
515
+
516
+ // Not a computer control command
517
+ return null;
518
+ }
519
+
305
520
  /**
306
521
  * Send a message to a specific chat
307
522
  */