@projectservan8n/cnapse 0.2.0 → 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
@@ -81,18 +81,22 @@ C-napse - Autonomous PC Intelligence
81
81
 
82
82
  Usage:
83
83
  cnapse Start interactive chat
84
+ cnapse init Interactive setup wizard
84
85
  cnapse auth <provider> <key> Set API key
85
86
  cnapse config Show configuration
86
87
  cnapse config set <k> <v> Set config value
87
88
  cnapse help Show this help
88
89
 
89
90
  Providers:
90
- ollama - Local AI (default)
91
- openrouter - OpenRouter API
91
+ ollama - Local AI (default, free)
92
+ openrouter - OpenRouter API (many models)
92
93
  anthropic - Anthropic Claude
93
94
  openai - OpenAI GPT
94
95
 
95
- Examples:
96
+ Quick Start:
97
+ cnapse init # Interactive setup
98
+
99
+ Manual Setup:
96
100
  cnapse auth openrouter sk-or-xxxxx
97
101
  cnapse config set provider openrouter
98
102
  cnapse config set model qwen/qwen-2.5-coder-32b-instruct
@@ -107,6 +111,13 @@ Examples:
107
111
  process.exit(0);
108
112
  }
109
113
 
114
+ case 'init': {
115
+ // Interactive setup with Ink UI
116
+ const { Setup } = await import('./components/Setup.js');
117
+ render(<Setup />);
118
+ process.exit(0);
119
+ }
120
+
110
121
  default: {
111
122
  // Treat as a direct question
112
123
  // For now, just start the app
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
+ }