@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.
- package/dist/Setup-Q32JPHGP.js +174 -0
- package/dist/chunk-COKO6V5J.js +50 -0
- package/dist/index.js +1193 -133
- package/package.json +4 -2
- package/src/agents/coder.ts +62 -0
- package/src/agents/computer.ts +61 -0
- package/src/agents/executor.ts +179 -0
- package/src/agents/filer.ts +56 -0
- package/src/agents/index.ts +12 -0
- package/src/agents/router.ts +160 -0
- package/src/agents/shell.ts +67 -0
- package/src/agents/types.ts +80 -0
- package/src/components/App.tsx +225 -8
- package/src/components/Header.tsx +11 -1
- package/src/components/HelpMenu.tsx +143 -0
- package/src/components/Setup.tsx +203 -0
- package/src/components/TaskProgress.tsx +68 -0
- package/src/index.tsx +3 -50
- package/src/lib/api.ts +2 -2
- package/src/lib/config.ts +21 -0
- package/src/lib/screen.ts +118 -0
- package/src/lib/tasks.ts +268 -0
- package/src/lib/vision.ts +254 -0
- package/src/services/telegram.ts +278 -0
- package/src/tools/clipboard.ts +55 -0
- package/src/tools/computer.ts +454 -0
- package/src/tools/filesystem.ts +272 -0
- package/src/tools/index.ts +35 -0
- package/src/tools/network.ts +204 -0
- package/src/tools/process.ts +194 -0
- package/src/tools/shell.ts +140 -0
- package/src/tools/vision.ts +65 -0
- package/src/types/screenshot-desktop.d.ts +10 -0
|
@@ -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
|
|
117
|
-
|
|
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
|
+
}
|
package/src/lib/tasks.ts
ADDED
|
@@ -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
|
+
}
|