@projectservan8n/cnapse 0.2.1 → 0.4.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.
@@ -0,0 +1,68 @@
1
+ import React from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import type { Task, TaskStep } from '../lib/tasks.js';
4
+
5
+ interface TaskProgressProps {
6
+ task: Task;
7
+ }
8
+
9
+ const statusEmoji: Record<Task['status'], string> = {
10
+ pending: 'ā³',
11
+ running: 'šŸ”„',
12
+ completed: 'āœ…',
13
+ failed: 'āŒ',
14
+ };
15
+
16
+ const stepStatusEmoji: Record<TaskStep['status'], string> = {
17
+ pending: 'ā—‹',
18
+ running: '◐',
19
+ completed: 'ā—',
20
+ failed: 'āœ—',
21
+ skipped: 'ā—Œ',
22
+ };
23
+
24
+ const stepStatusColor: Record<TaskStep['status'], string> = {
25
+ pending: 'gray',
26
+ running: 'yellow',
27
+ completed: 'green',
28
+ failed: 'red',
29
+ skipped: 'gray',
30
+ };
31
+
32
+ export function TaskProgress({ task }: TaskProgressProps) {
33
+ return (
34
+ <Box flexDirection="column" borderStyle="round" borderColor="cyan" padding={1} marginY={1}>
35
+ <Box marginBottom={1}>
36
+ <Text bold>
37
+ {statusEmoji[task.status]} Task: {task.description}
38
+ </Text>
39
+ </Box>
40
+
41
+ {task.steps.map((step, index) => (
42
+ <Box key={step.id} marginLeft={2}>
43
+ <Text color={stepStatusColor[step.status]}>
44
+ {stepStatusEmoji[step.status]} {step.description}
45
+ </Text>
46
+ {step.result && (
47
+ <Text color="gray" dimColor> → {step.result}</Text>
48
+ )}
49
+ {step.error && (
50
+ <Text color="red"> (Error: {step.error})</Text>
51
+ )}
52
+ </Box>
53
+ ))}
54
+
55
+ {task.status === 'completed' && (
56
+ <Box marginTop={1}>
57
+ <Text color="green">āœ“ Task completed</Text>
58
+ </Box>
59
+ )}
60
+
61
+ {task.status === 'failed' && (
62
+ <Box marginTop={1}>
63
+ <Text color="red">āœ— Task failed</Text>
64
+ </Box>
65
+ )}
66
+ </Box>
67
+ );
68
+ }
package/src/index.tsx CHANGED
@@ -112,56 +112,9 @@ Manual Setup:
112
112
  }
113
113
 
114
114
  case 'init': {
115
- // Interactive setup
116
- const readline = await import('readline');
117
- const rl = readline.createInterface({
118
- input: process.stdin,
119
- output: process.stdout,
120
- });
121
-
122
- const question = (q: string): Promise<string> =>
123
- new Promise((resolve) => rl.question(q, resolve));
124
-
125
- console.log('\nšŸš€ C-napse Setup\n');
126
-
127
- console.log('Select a provider:');
128
- console.log(' 1. ollama - Local AI (free, requires Ollama installed)');
129
- console.log(' 2. openrouter - OpenRouter API (pay per use, many models)');
130
- console.log(' 3. anthropic - Anthropic Claude (pay per use)');
131
- console.log(' 4. openai - OpenAI GPT (pay per use)');
132
- console.log('');
133
-
134
- const providerChoice = await question('Enter choice (1-4) [1]: ');
135
- const providers = ['ollama', 'openrouter', 'anthropic', 'openai'] as const;
136
- const providerIndex = parseInt(providerChoice || '1') - 1;
137
- const provider = providers[providerIndex] || 'ollama';
138
-
139
- setProvider(provider);
140
- console.log(`āœ“ Provider set to: ${provider}`);
141
-
142
- if (provider !== 'ollama') {
143
- const apiKey = await question(`\nEnter your ${provider} API key: `);
144
- if (apiKey) {
145
- setApiKey(provider as any, apiKey);
146
- console.log(`āœ“ API key saved`);
147
- }
148
- }
149
-
150
- // Set default model based on provider
151
- const defaultModels: Record<string, string> = {
152
- ollama: 'qwen2.5:0.5b',
153
- openrouter: 'qwen/qwen-2.5-coder-32b-instruct',
154
- anthropic: 'claude-3-5-sonnet-20241022',
155
- openai: 'gpt-4o',
156
- };
157
-
158
- const model = await question(`\nModel [${defaultModels[provider]}]: `);
159
- setModel(model || defaultModels[provider]!);
160
- console.log(`āœ“ Model set to: ${model || defaultModels[provider]}`);
161
-
162
- rl.close();
163
-
164
- console.log('\nāœ… Setup complete! Run `cnapse` to start chatting.\n');
115
+ // Interactive setup with Ink UI
116
+ const { Setup } = await import('./components/Setup.js');
117
+ render(<Setup />);
165
118
  process.exit(0);
166
119
  }
167
120
 
package/src/lib/api.ts CHANGED
@@ -18,11 +18,11 @@ When responding:
18
18
  - Use markdown formatting for code blocks
19
19
  - If asked to do something, explain what you'll do first`;
20
20
 
21
- export async function chat(messages: Message[]): Promise<ChatResponse> {
21
+ export async function chat(messages: Message[], systemPrompt?: string): Promise<ChatResponse> {
22
22
  const config = getConfig();
23
23
 
24
24
  const allMessages: Message[] = [
25
- { role: 'system', content: SYSTEM_PROMPT },
25
+ { role: 'system', content: systemPrompt || SYSTEM_PROMPT },
26
26
  ...messages,
27
27
  ];
28
28
 
package/src/lib/config.ts CHANGED
@@ -7,12 +7,17 @@ interface ConfigSchema {
7
7
  openrouter?: string;
8
8
  anthropic?: string;
9
9
  openai?: string;
10
+ telegram?: string;
10
11
  };
11
12
  ollamaHost: string;
12
13
  openrouter: {
13
14
  siteUrl: string;
14
15
  appName: string;
15
16
  };
17
+ telegram: {
18
+ chatId?: number;
19
+ enabled: boolean;
20
+ };
16
21
  }
17
22
 
18
23
  const config = new Conf<ConfigSchema>({
@@ -26,6 +31,9 @@ const config = new Conf<ConfigSchema>({
26
31
  siteUrl: 'https://github.com/projectservan8n/C-napse',
27
32
  appName: 'C-napse',
28
33
  },
34
+ telegram: {
35
+ enabled: false,
36
+ },
29
37
  },
30
38
  });
31
39
 
@@ -36,6 +44,7 @@ export function getConfig() {
36
44
  apiKeys: config.get('apiKeys'),
37
45
  ollamaHost: config.get('ollamaHost'),
38
46
  openrouter: config.get('openrouter'),
47
+ telegram: config.get('telegram'),
39
48
  };
40
49
  }
41
50
 
@@ -57,4 +66,16 @@ export function getApiKey(provider: keyof ConfigSchema['apiKeys']): string | und
57
66
  return config.get('apiKeys')[provider];
58
67
  }
59
68
 
69
+ export function setTelegramChatId(chatId: number) {
70
+ const telegram = config.get('telegram');
71
+ telegram.chatId = chatId;
72
+ config.set('telegram', telegram);
73
+ }
74
+
75
+ export function setTelegramEnabled(enabled: boolean) {
76
+ const telegram = config.get('telegram');
77
+ telegram.enabled = enabled;
78
+ config.set('telegram', telegram);
79
+ }
80
+
60
81
  export { config };
@@ -0,0 +1,118 @@
1
+ import { exec } from 'child_process';
2
+ import { promisify } from 'util';
3
+ import { tmpdir } from 'os';
4
+ import { join } from 'path';
5
+ import { readFile, unlink } from 'fs/promises';
6
+
7
+ const execAsync = promisify(exec);
8
+
9
+ let lastScreenHash: string | null = null;
10
+ let isCapturing = false;
11
+
12
+ /**
13
+ * Capture screenshot and return base64 encoded image
14
+ * Uses platform-specific tools
15
+ */
16
+ export async function captureScreen(): Promise<string | null> {
17
+ if (isCapturing) return null;
18
+ isCapturing = true;
19
+
20
+ const tempFile = join(tmpdir(), `cnapse-screen-${Date.now()}.png`);
21
+
22
+ try {
23
+ const platform = process.platform;
24
+
25
+ if (platform === 'win32') {
26
+ // Windows: Use PowerShell to capture screen
27
+ await execAsync(`
28
+ Add-Type -AssemblyName System.Windows.Forms
29
+ $screen = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds
30
+ $bitmap = New-Object System.Drawing.Bitmap($screen.Width, $screen.Height)
31
+ $graphics = [System.Drawing.Graphics]::FromImage($bitmap)
32
+ $graphics.CopyFromScreen($screen.Location, [System.Drawing.Point]::Empty, $screen.Size)
33
+ $bitmap.Save("${tempFile.replace(/\\/g, '\\\\')}")
34
+ $graphics.Dispose()
35
+ $bitmap.Dispose()
36
+ `, { shell: 'powershell.exe' });
37
+ } else if (platform === 'darwin') {
38
+ // macOS: Use screencapture
39
+ await execAsync(`screencapture -x "${tempFile}"`);
40
+ } else {
41
+ // Linux: Try various tools
42
+ try {
43
+ await execAsync(`gnome-screenshot -f "${tempFile}" 2>/dev/null || scrot "${tempFile}" 2>/dev/null || import -window root "${tempFile}"`);
44
+ } catch {
45
+ return null;
46
+ }
47
+ }
48
+
49
+ // Read the file and convert to base64
50
+ const imageBuffer = await readFile(tempFile);
51
+ const base64 = imageBuffer.toString('base64');
52
+
53
+ // Clean up
54
+ await unlink(tempFile).catch(() => {});
55
+
56
+ return base64;
57
+ } catch (error) {
58
+ return null;
59
+ } finally {
60
+ isCapturing = false;
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Simple hash function for change detection
66
+ */
67
+ function simpleHash(str: string): string {
68
+ let hash = 0;
69
+ for (let i = 0; i < str.length; i += 100) {
70
+ const char = str.charCodeAt(i);
71
+ hash = ((hash << 5) - hash) + char;
72
+ hash = hash & hash;
73
+ }
74
+ return hash.toString(16);
75
+ }
76
+
77
+ /**
78
+ * Check if screen has changed since last capture
79
+ */
80
+ export async function checkScreenChange(): Promise<{ changed: boolean; image: string | null }> {
81
+ const image = await captureScreen();
82
+
83
+ if (!image) {
84
+ return { changed: false, image: null };
85
+ }
86
+
87
+ const currentHash = simpleHash(image);
88
+ const changed = lastScreenHash !== null && lastScreenHash !== currentHash;
89
+ lastScreenHash = currentHash;
90
+
91
+ return { changed, image };
92
+ }
93
+
94
+ /**
95
+ * Get screen description for context (simplified - just dimensions)
96
+ */
97
+ export async function getScreenDescription(): Promise<string | null> {
98
+ try {
99
+ const platform = process.platform;
100
+
101
+ if (platform === 'win32') {
102
+ const { stdout } = await execAsync(`
103
+ Add-Type -AssemblyName System.Windows.Forms
104
+ $screen = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds
105
+ Write-Output "$($screen.Width)x$($screen.Height)"
106
+ `, { shell: 'powershell.exe' });
107
+ return `Screen ${stdout.trim()} captured`;
108
+ } else if (platform === 'darwin') {
109
+ const { stdout } = await execAsync(`system_profiler SPDisplaysDataType | grep Resolution | head -1`);
110
+ return `Screen ${stdout.trim()}`;
111
+ } else {
112
+ const { stdout } = await execAsync(`xdpyinfo | grep dimensions | awk '{print $2}'`);
113
+ return `Screen ${stdout.trim()} captured`;
114
+ }
115
+ } catch {
116
+ return null;
117
+ }
118
+ }
@@ -0,0 +1,268 @@
1
+ /**
2
+ * Task Automation - Multi-step task sequencing
3
+ * Parses natural language into actionable steps and executes them
4
+ */
5
+
6
+ import { chat, Message } from './api.js';
7
+ import * as computer from '../tools/computer.js';
8
+ import { describeScreen } from './vision.js';
9
+
10
+ export type TaskStepStatus = 'pending' | 'running' | 'completed' | 'failed' | 'skipped';
11
+
12
+ export interface TaskStep {
13
+ id: string;
14
+ description: string;
15
+ action: string; // The actual action to perform
16
+ status: TaskStepStatus;
17
+ result?: string;
18
+ error?: string;
19
+ }
20
+
21
+ export interface Task {
22
+ id: string;
23
+ description: string;
24
+ steps: TaskStep[];
25
+ status: 'pending' | 'running' | 'completed' | 'failed';
26
+ createdAt: Date;
27
+ completedAt?: Date;
28
+ }
29
+
30
+ export type TaskProgressCallback = (task: Task, step: TaskStep) => void;
31
+
32
+ /**
33
+ * Parse natural language task into executable steps
34
+ */
35
+ export async function parseTask(input: string): Promise<Task> {
36
+ const systemPrompt = `You are a task parser for PC automation. Convert user requests into specific, executable steps.
37
+
38
+ Available actions:
39
+ - open_app: Open an application (e.g., "open_app:notepad", "open_app:vscode")
40
+ - type_text: Type text (e.g., "type_text:Hello World")
41
+ - press_key: Press a key (e.g., "press_key:enter", "press_key:escape")
42
+ - key_combo: Key combination (e.g., "key_combo:control+s", "key_combo:alt+f4")
43
+ - click: Click mouse (e.g., "click:left", "click:right")
44
+ - wait: Wait seconds (e.g., "wait:2")
45
+ - focus_window: Focus window by title (e.g., "focus_window:Notepad")
46
+ - screenshot: Take screenshot and describe
47
+
48
+ Respond ONLY with a JSON array of steps, no other text:
49
+ [
50
+ { "description": "Human readable step", "action": "action_type:params" },
51
+ ...
52
+ ]
53
+
54
+ Example input: "open notepad and type hello world"
55
+ Example output:
56
+ [
57
+ { "description": "Open Notepad", "action": "open_app:notepad" },
58
+ { "description": "Wait for Notepad to open", "action": "wait:2" },
59
+ { "description": "Type hello world", "action": "type_text:Hello World" }
60
+ ]
61
+
62
+ Example input: "open vscode, go to folder E:\\Projects, then open terminal"
63
+ Example output:
64
+ [
65
+ { "description": "Open VS Code", "action": "open_app:code" },
66
+ { "description": "Wait for VS Code to load", "action": "wait:3" },
67
+ { "description": "Open folder with Ctrl+K Ctrl+O", "action": "key_combo:control+k" },
68
+ { "description": "Wait for dialog", "action": "wait:1" },
69
+ { "description": "Continue folder open", "action": "key_combo:control+o" },
70
+ { "description": "Wait for folder dialog", "action": "wait:1" },
71
+ { "description": "Type folder path", "action": "type_text:E:\\\\Projects" },
72
+ { "description": "Press Enter to open folder", "action": "press_key:enter" },
73
+ { "description": "Wait for folder to load", "action": "wait:2" },
74
+ { "description": "Open terminal with Ctrl+\`", "action": "key_combo:control+\`" }
75
+ ]`;
76
+
77
+ const messages: Message[] = [
78
+ { role: 'user', content: input }
79
+ ];
80
+
81
+ try {
82
+ const response = await chat(messages, systemPrompt);
83
+ const content = response.content || '[]';
84
+
85
+ // Extract JSON from response
86
+ const jsonMatch = content.match(/\[[\s\S]*\]/);
87
+ if (!jsonMatch) {
88
+ throw new Error('Failed to parse task steps');
89
+ }
90
+
91
+ const parsedSteps = JSON.parse(jsonMatch[0]) as Array<{ description: string; action: string }>;
92
+
93
+ const steps: TaskStep[] = parsedSteps.map((step, index) => ({
94
+ id: `step-${index + 1}`,
95
+ description: step.description,
96
+ action: step.action,
97
+ status: 'pending' as TaskStepStatus,
98
+ }));
99
+
100
+ return {
101
+ id: `task-${Date.now()}`,
102
+ description: input,
103
+ steps,
104
+ status: 'pending',
105
+ createdAt: new Date(),
106
+ };
107
+ } catch (error) {
108
+ // If AI parsing fails, try to create a simple task
109
+ return {
110
+ id: `task-${Date.now()}`,
111
+ description: input,
112
+ steps: [{
113
+ id: 'step-1',
114
+ description: input,
115
+ action: `chat:${input}`,
116
+ status: 'pending',
117
+ }],
118
+ status: 'pending',
119
+ createdAt: new Date(),
120
+ };
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Execute a single task step
126
+ */
127
+ async function executeStep(step: TaskStep): Promise<void> {
128
+ const [actionType, ...paramParts] = step.action.split(':');
129
+ const params = paramParts.join(':'); // Rejoin in case params contain ':'
130
+
131
+ switch (actionType) {
132
+ case 'open_app':
133
+ // Use Windows Run dialog to open apps
134
+ await computer.keyCombo(['meta', 'r']);
135
+ await sleep(500);
136
+ await computer.typeText(params);
137
+ await sleep(300);
138
+ await computer.pressKey('Return');
139
+ step.result = `Opened ${params}`;
140
+ break;
141
+
142
+ case 'type_text':
143
+ await computer.typeText(params);
144
+ step.result = `Typed: ${params}`;
145
+ break;
146
+
147
+ case 'press_key':
148
+ await computer.pressKey(params);
149
+ step.result = `Pressed ${params}`;
150
+ break;
151
+
152
+ case 'key_combo':
153
+ const keys = params.split('+').map(k => k.trim());
154
+ await computer.keyCombo(keys);
155
+ step.result = `Pressed ${params}`;
156
+ break;
157
+
158
+ case 'click':
159
+ const button = (params || 'left') as 'left' | 'right' | 'middle';
160
+ await computer.clickMouse(button);
161
+ step.result = `Clicked ${button}`;
162
+ break;
163
+
164
+ case 'wait':
165
+ const seconds = parseInt(params) || 1;
166
+ await sleep(seconds * 1000);
167
+ step.result = `Waited ${seconds}s`;
168
+ break;
169
+
170
+ case 'focus_window':
171
+ await computer.focusWindow(params);
172
+ step.result = `Focused window: ${params}`;
173
+ break;
174
+
175
+ case 'screenshot':
176
+ const vision = await describeScreen();
177
+ step.result = vision.description;
178
+ break;
179
+
180
+ case 'chat':
181
+ // This is a fallback - just describe what user wants
182
+ step.result = `Task noted: ${params}`;
183
+ break;
184
+
185
+ default:
186
+ throw new Error(`Unknown action: ${actionType}`);
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Execute a complete task with progress callbacks
192
+ */
193
+ export async function executeTask(
194
+ task: Task,
195
+ onProgress?: TaskProgressCallback
196
+ ): Promise<Task> {
197
+ task.status = 'running';
198
+
199
+ for (const step of task.steps) {
200
+ if (task.status === 'failed') {
201
+ step.status = 'skipped';
202
+ continue;
203
+ }
204
+
205
+ step.status = 'running';
206
+ onProgress?.(task, step);
207
+
208
+ try {
209
+ await executeStep(step);
210
+ step.status = 'completed';
211
+ } catch (error) {
212
+ step.status = 'failed';
213
+ step.error = error instanceof Error ? error.message : 'Unknown error';
214
+ task.status = 'failed';
215
+ }
216
+
217
+ onProgress?.(task, step);
218
+ }
219
+
220
+ if (task.status !== 'failed') {
221
+ task.status = 'completed';
222
+ }
223
+ task.completedAt = new Date();
224
+
225
+ return task;
226
+ }
227
+
228
+ /**
229
+ * Helper sleep function
230
+ */
231
+ function sleep(ms: number): Promise<void> {
232
+ return new Promise(resolve => setTimeout(resolve, ms));
233
+ }
234
+
235
+ /**
236
+ * Format task for display
237
+ */
238
+ export function formatTask(task: Task): string {
239
+ const statusEmoji = {
240
+ pending: 'ā³',
241
+ running: 'šŸ”„',
242
+ completed: 'āœ…',
243
+ failed: 'āŒ',
244
+ };
245
+
246
+ const stepStatusEmoji = {
247
+ pending: 'ā—‹',
248
+ running: '◐',
249
+ completed: 'ā—',
250
+ failed: 'āœ—',
251
+ skipped: 'ā—Œ',
252
+ };
253
+
254
+ let output = `${statusEmoji[task.status]} Task: ${task.description}\n\n`;
255
+
256
+ for (const step of task.steps) {
257
+ output += ` ${stepStatusEmoji[step.status]} ${step.description}`;
258
+ if (step.result) {
259
+ output += ` → ${step.result}`;
260
+ }
261
+ if (step.error) {
262
+ output += ` (Error: ${step.error})`;
263
+ }
264
+ output += '\n';
265
+ }
266
+
267
+ return output;
268
+ }