natureco-cli 5.16.0 → 5.17.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/package.json +1 -1
- package/src/tools/computer_use.js +210 -57
- package/src/tools/computer_use_loop.js +224 -0
- package/src/utils/tools.js +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "natureco-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.17.0",
|
|
4
4
|
"description": "OpenClaw'dan daha güvenli, daha hızlı, daha ucuz AI agent CLI. Multi-agent, self-evolving skills, audit log, maliyet optimizasyonu ve NatureCo platform-native.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"natureco": "bin/natureco.js"
|
|
@@ -1,138 +1,291 @@
|
|
|
1
|
-
const {
|
|
1
|
+
const { spawnSync } = require('child_process');
|
|
2
2
|
const os = require('os');
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
|
|
6
6
|
const PLATFORM = os.platform();
|
|
7
7
|
|
|
8
|
+
const KEY_MAP_DARWIN = {
|
|
9
|
+
enter: 'return',
|
|
10
|
+
return: 'return',
|
|
11
|
+
tab: 'tab',
|
|
12
|
+
escape: 'escape',
|
|
13
|
+
esc: 'escape',
|
|
14
|
+
up: 'up',
|
|
15
|
+
down: 'down',
|
|
16
|
+
left: 'left',
|
|
17
|
+
right: 'right',
|
|
18
|
+
backspace: 'delete',
|
|
19
|
+
delete: 'forwarddelete',
|
|
20
|
+
forwarddelete: 'forwarddelete',
|
|
21
|
+
home: 'home',
|
|
22
|
+
end: 'end',
|
|
23
|
+
pageup: 'page up',
|
|
24
|
+
pagedown: 'page down',
|
|
25
|
+
space: 'space',
|
|
26
|
+
' ': 'space',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const MODIFIER_MAP = {
|
|
30
|
+
command: 'command down',
|
|
31
|
+
cmd: 'command down',
|
|
32
|
+
option: 'option down',
|
|
33
|
+
alt: 'option down',
|
|
34
|
+
control: 'control down',
|
|
35
|
+
ctrl: 'control down',
|
|
36
|
+
shift: 'shift down',
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
function escapeText(s) {
|
|
40
|
+
return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function osaScript(script, timeoutMs = 10000) {
|
|
44
|
+
const result = spawnSync('osascript', ['-e', script], {
|
|
45
|
+
timeout: timeoutMs,
|
|
46
|
+
encoding: 'utf8',
|
|
47
|
+
maxBuffer: 1024 * 1024,
|
|
48
|
+
});
|
|
49
|
+
if (result.error) {
|
|
50
|
+
if (result.error.code === 'ETIMEDOUT') {
|
|
51
|
+
return { success: false, error: 'osascript timed out after ' + timeoutMs + 'ms' };
|
|
52
|
+
}
|
|
53
|
+
return { success: false, error: result.error.message };
|
|
54
|
+
}
|
|
55
|
+
if (result.status !== 0) {
|
|
56
|
+
const msg = result.stderr || result.stdout || 'unknown error';
|
|
57
|
+
let friendly = msg;
|
|
58
|
+
if (msg.includes('yardımcı erişime izin verilmiyor') || msg.includes('access for assistive devices')) {
|
|
59
|
+
friendly = 'Accessibility izni gerekli. System Settings > Privacy & Security > Accessibility > Terminal/iTerm2\'ye izin verin.';
|
|
60
|
+
} else if (msg.includes('(-1700') || msg.includes('can\'t convert')) {
|
|
61
|
+
friendly = 'AppleScript hatasi: ' + msg.slice(0, 200);
|
|
62
|
+
}
|
|
63
|
+
return { success: false, error: friendly };
|
|
64
|
+
}
|
|
65
|
+
return { success: true, data: result.stdout };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function checkAccessibility() {
|
|
69
|
+
const r = osaScript('tell application "System Events" to get name of first process whose frontmost is true', 3000);
|
|
70
|
+
return r.success;
|
|
71
|
+
}
|
|
72
|
+
|
|
8
73
|
async function computerUse(params) {
|
|
9
|
-
const { action, x, y, key, text, button, clicks,
|
|
74
|
+
const { action, x, y, key, text, button, clicks, file } = params;
|
|
10
75
|
|
|
11
76
|
if (action === 'screenshot') {
|
|
12
77
|
const outputFile = file || path.join(os.tmpdir(), 'natureco_screen_' + Date.now() + '.png');
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
78
|
+
try {
|
|
79
|
+
if (PLATFORM === 'darwin') {
|
|
80
|
+
spawnSync('screencapture', ['-x', outputFile], { timeout: 5000 });
|
|
81
|
+
} else if (PLATFORM === 'win32') {
|
|
82
|
+
spawnSync('powershell', ['-Command',
|
|
83
|
+
'Add-Type -AssemblyName System.Windows.Forms; ' +
|
|
84
|
+
'$bmp = [System.Drawing.Bitmap]::new([System.Windows.Forms.Screen]::PrimaryScreen.Bounds.Width, [System.Windows.Forms.Screen]::PrimaryScreen.Bounds.Height); ' +
|
|
85
|
+
'$g = [System.Drawing.Graphics]::FromImage($bmp); ' +
|
|
86
|
+
'$g.CopyFromScreen(0, 0, 0, 0, $bmp.Size); ' +
|
|
87
|
+
'$bmp.Save("' + outputFile.replace(/"/g, '') + '", [System.Drawing.Imaging.ImageFormat]::Png)'
|
|
88
|
+
], { timeout: 10000 });
|
|
89
|
+
} else {
|
|
90
|
+
spawnSync('import', ['-window', 'root', outputFile], { timeout: 5000 });
|
|
91
|
+
}
|
|
92
|
+
return { success: true, file: outputFile, platform: PLATFORM, note: 'Ekran goruntusu alindi' };
|
|
93
|
+
} catch (e) {
|
|
94
|
+
return { success: false, error: 'Screenshot hatasi: ' + e.message };
|
|
19
95
|
}
|
|
20
|
-
return { success: true, file: outputFile, platform: PLATFORM, note: 'Ekran goruntusu alindi: ' + outputFile };
|
|
21
96
|
}
|
|
22
97
|
|
|
23
98
|
if (action === 'click') {
|
|
24
99
|
if (typeof x !== 'number' || typeof y !== 'number') return { success: false, error: 'x ve y gerekli' };
|
|
25
100
|
const btn = button || 'left';
|
|
26
101
|
if (PLATFORM === 'darwin') {
|
|
27
|
-
|
|
28
|
-
|
|
102
|
+
const acc = checkAccessibility();
|
|
103
|
+
if (!acc) return { success: false, error: 'Accessibility izni gerekli. System Settings > Privacy & Security > Accessibility > Terminal\'e izin verin.' };
|
|
104
|
+
const r = osaScript('tell application "System Events" to click at {' + x + ', ' + y + '}');
|
|
105
|
+
if (!r.success) return r;
|
|
106
|
+
return { success: true, action: 'click', x, y, button: btn };
|
|
107
|
+
}
|
|
108
|
+
if (PLATFORM === 'win32') {
|
|
29
109
|
const c = clicks || 1;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
110
|
+
spawnSync('powershell', ['-Command',
|
|
111
|
+
'[System.Windows.Forms.Cursor]::Position = New-Object System.Drawing.Point(' + x + ', ' + y + '); ' +
|
|
112
|
+
'[System.Windows.Forms.SendKeys]::SendWait("' + (c > 1 ? '{DOUBLECLICK}' : '{CLICK}') + '")'
|
|
113
|
+
], { timeout: 5000 });
|
|
114
|
+
return { success: true, action: 'click', x, y, button: btn };
|
|
33
115
|
}
|
|
34
|
-
|
|
116
|
+
spawnSync('xdotool', ['mousemove', String(x), String(y), 'click', '1'], { timeout: 5000 });
|
|
117
|
+
return { success: true, action: 'click', x, y, button: btn };
|
|
35
118
|
}
|
|
36
119
|
|
|
37
120
|
if (action === 'type') {
|
|
38
121
|
if (!text) return { success: false, error: 'text gerekli' };
|
|
39
122
|
if (PLATFORM === 'darwin') {
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
123
|
+
const acc = checkAccessibility();
|
|
124
|
+
if (!acc) return { success: false, error: 'Accessibility izni gerekli' };
|
|
125
|
+
const r = osaScript('tell application "System Events" to keystroke "' + escapeText(text) + '"');
|
|
126
|
+
if (!r.success) return r;
|
|
127
|
+
return { success: true, action: 'type', text };
|
|
128
|
+
}
|
|
129
|
+
if (PLATFORM === 'win32') {
|
|
130
|
+
spawnSync('powershell', ['-Command',
|
|
131
|
+
'[System.Windows.Forms.SendKeys]::SendWait("' + text.replace(/[{}()^+% ~]/g, '{$&}') + '")'
|
|
132
|
+
], { timeout: 5000 });
|
|
133
|
+
return { success: true, action: 'type', text };
|
|
46
134
|
}
|
|
135
|
+
spawnSync('xdotool', ['type', text], { timeout: 5000 });
|
|
47
136
|
return { success: true, action: 'type', text };
|
|
48
137
|
}
|
|
49
138
|
|
|
50
139
|
if (action === 'keypress') {
|
|
51
140
|
if (!key) return { success: false, error: 'key gerekli' };
|
|
52
141
|
if (PLATFORM === 'darwin') {
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
142
|
+
const acc = checkAccessibility();
|
|
143
|
+
if (!acc) return { success: false, error: 'Accessibility izni gerekli' };
|
|
144
|
+
|
|
145
|
+
const parts = key.toLowerCase().split('+').map(p => p.trim());
|
|
146
|
+
const mods = [];
|
|
147
|
+
let actualKey = '';
|
|
148
|
+
for (const p of parts) {
|
|
149
|
+
if (MODIFIER_MAP[p]) mods.push(MODIFIER_MAP[p]);
|
|
150
|
+
else actualKey = p;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (KEY_MAP_DARWIN[actualKey]) {
|
|
154
|
+
const keyName = KEY_MAP_DARWIN[actualKey];
|
|
155
|
+
const usingClause = mods.length > 0 ? ' using {' + mods.join(', ') + '}' : '';
|
|
156
|
+
const r = osaScript('tell application "System Events" to keystroke ' + keyName + usingClause);
|
|
157
|
+
if (!r.success) return r;
|
|
158
|
+
} else if (actualKey) {
|
|
159
|
+
const usingClause = mods.length > 0 ? ' using {' + mods.join(', ') + '}' : '';
|
|
160
|
+
const r = osaScript('tell application "System Events" to keystroke "' + escapeText(actualKey) + '"' + usingClause);
|
|
161
|
+
if (!r.success) return r;
|
|
162
|
+
} else {
|
|
163
|
+
if (mods.length > 0) {
|
|
164
|
+
return { success: false, error: 'Modifier-only keypress not supported. Provide a key with modifiers (e.g. cmd+q).' };
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return { success: true, action: 'keypress', key };
|
|
61
168
|
}
|
|
169
|
+
if (PLATFORM === 'win32') {
|
|
170
|
+
const keyMap = {
|
|
171
|
+
enter: '{ENTER}', tab: '{TAB}', escape: '{ESC}', up: '{UP}', down: '{DOWN}',
|
|
172
|
+
left: '{LEFT}', right: '{RIGHT}', backspace: '{BACKSPACE}', delete: '{DELETE}',
|
|
173
|
+
home: '{HOME}', end: '{END}', pageup: '{PGUP}', pagedown: '{PGDN}',
|
|
174
|
+
};
|
|
175
|
+
spawnSync('powershell', ['-Command',
|
|
176
|
+
'[System.Windows.Forms.SendKeys]::SendWait("' + (keyMap[key.toLowerCase()] || key) + '")'
|
|
177
|
+
], { timeout: 5000 });
|
|
178
|
+
return { success: true, action: 'keypress', key };
|
|
179
|
+
}
|
|
180
|
+
spawnSync('xdotool', ['key', key], { timeout: 5000 });
|
|
62
181
|
return { success: true, action: 'keypress', key };
|
|
63
182
|
}
|
|
64
183
|
|
|
65
184
|
if (action === 'mouse_move') {
|
|
66
185
|
if (typeof x !== 'number' || typeof y !== 'number') return { success: false, error: 'x ve y gerekli' };
|
|
67
186
|
if (PLATFORM === 'darwin') {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
187
|
+
const acc = checkAccessibility();
|
|
188
|
+
if (!acc) return { success: false, error: 'Accessibility izni gerekli' };
|
|
189
|
+
const r = osaScript('tell application "System Events" to set position of mouse to {' + x + ', ' + y + '}');
|
|
190
|
+
if (!r.success) return r;
|
|
191
|
+
return { success: true, action: 'mouse_move', x, y };
|
|
192
|
+
}
|
|
193
|
+
if (PLATFORM === 'win32') {
|
|
194
|
+
spawnSync('powershell', ['-Command',
|
|
195
|
+
'[System.Windows.Forms.Cursor]::Position = New-Object System.Drawing.Point(' + x + ', ' + y + ')'
|
|
196
|
+
], { timeout: 5000 });
|
|
197
|
+
return { success: true, action: 'mouse_move', x, y };
|
|
73
198
|
}
|
|
199
|
+
spawnSync('xdotool', ['mousemove', String(x), String(y)], { timeout: 5000 });
|
|
74
200
|
return { success: true, action: 'mouse_move', x, y };
|
|
75
201
|
}
|
|
76
202
|
|
|
77
203
|
if (action === 'mouse_position') {
|
|
78
204
|
if (PLATFORM === 'darwin') {
|
|
79
|
-
const
|
|
80
|
-
|
|
205
|
+
const r = osaScript('tell application "System Events" to return position of mouse');
|
|
206
|
+
if (!r.success) return r;
|
|
207
|
+
const [mx, my] = r.data.trim().split(', ').map(Number);
|
|
81
208
|
return { success: true, x: mx, y: my };
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
const mx = parseInt(out.match(/x:(\d+)/)?.[1] || '0');
|
|
89
|
-
const my = parseInt(out.match(/y:(\d+)/)?.[1] || '0');
|
|
209
|
+
}
|
|
210
|
+
if (PLATFORM === 'win32') {
|
|
211
|
+
const result = spawnSync('powershell', ['-Command',
|
|
212
|
+
'[System.Windows.Forms.Cursor]::Position.X.ToString() + ", " + [System.Windows.Forms.Cursor]::Position.Y.ToString()'
|
|
213
|
+
], { timeout: 5000, encoding: 'utf8' });
|
|
214
|
+
const [mx, my] = result.stdout.trim().split(', ').map(Number);
|
|
90
215
|
return { success: true, x: mx, y: my };
|
|
91
216
|
}
|
|
217
|
+
const result = spawnSync('xdotool', ['getmouselocation'], { timeout: 5000, encoding: 'utf8' });
|
|
218
|
+
const mx = parseInt(result.stdout.match(/x:(\d+)/)?.[1] || '0');
|
|
219
|
+
const my = parseInt(result.stdout.match(/y:(\d+)/)?.[1] || '0');
|
|
220
|
+
return { success: true, x: mx, y: my };
|
|
92
221
|
}
|
|
93
222
|
|
|
94
223
|
if (action === 'scroll') {
|
|
95
224
|
if (typeof y !== 'number') return { success: false, error: 'y (pixels) gerekli' };
|
|
96
225
|
if (PLATFORM === 'darwin') {
|
|
97
|
-
|
|
98
|
-
return { success:
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
226
|
+
const acc = checkAccessibility();
|
|
227
|
+
if (!acc) return { success: false, error: 'Accessibility izni gerekli' };
|
|
228
|
+
const direction = y > 0 ? 'up' : 'down';
|
|
229
|
+
const times = Math.abs(Math.ceil(y / 40));
|
|
230
|
+
const r = osaScript('tell application "System Events" to repeat ' + times + ' times\n key code 125\nend repeat');
|
|
231
|
+
if (!r.success) return r;
|
|
232
|
+
|
|
233
|
+
return { success: true, action: 'scroll', y, note: 'Scrolled ' + direction + ' ' + times + ' steps' };
|
|
234
|
+
}
|
|
235
|
+
spawnSync('xdotool', ['click', y < 0 ? '4' : '5', '--repeat', String(Math.abs(Math.ceil(y / 50)))], { timeout: 5000 });
|
|
236
|
+
return { success: true, action: 'scroll', y };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (action === 'drag') {
|
|
240
|
+
if (typeof x !== 'number' || typeof y !== 'number') return { success: false, error: 'x ve y (baslangic) gerekli' };
|
|
241
|
+
const x2 = params.x2, y2 = params.y2;
|
|
242
|
+
if (typeof x2 !== 'number' || typeof y2 !== 'number') return { success: false, error: 'x2 ve y2 (bitis) gerekli' };
|
|
243
|
+
if (PLATFORM === 'darwin') {
|
|
244
|
+
const acc = checkAccessibility();
|
|
245
|
+
if (!acc) return { success: false, error: 'Accessibility izni gerekli' };
|
|
246
|
+
const r = osaScript('tell application "System Events"\n set mousePos to {' + x + ', ' + y + '}\n set mousePos2 to {' + x2 + ', ' + y2 + '}\n set position of mouse to mousePos\n delay 0.1\n mouse down\n set position of mouse to mousePos2\n delay 0.1\n mouse up\nend tell');
|
|
247
|
+
if (!r.success) return r;
|
|
248
|
+
return { success: true, action: 'drag', from: { x, y }, to: { x: x2, y: y2 } };
|
|
249
|
+
}
|
|
250
|
+
if (PLATFORM === 'linux') {
|
|
251
|
+
spawnSync('xdotool', ['mousemove', String(x), String(y), 'mousedown', '1', 'mousemove', String(x2), String(y2), 'mouseup', '1'], { timeout: 5000 });
|
|
252
|
+
return { success: true, action: 'drag', from: { x, y }, to: { x: x2, y: y2 } };
|
|
102
253
|
}
|
|
254
|
+
return { success: false, error: 'drag only supported on macOS and Linux' };
|
|
103
255
|
}
|
|
104
256
|
|
|
105
257
|
if (action === 'info') {
|
|
106
258
|
const displays = [];
|
|
107
259
|
try {
|
|
108
260
|
if (PLATFORM === 'darwin') {
|
|
109
|
-
const
|
|
110
|
-
for (const line of
|
|
261
|
+
const result = spawnSync('system_profiler', ['SPDisplaysDataType'], { timeout: 5000, encoding: 'utf8' });
|
|
262
|
+
for (const line of result.stdout.split('\n')) {
|
|
111
263
|
const m = line.match(/(\d+) x (\d+)/);
|
|
112
264
|
if (m) displays.push({ width: parseInt(m[1]), height: parseInt(m[2]) });
|
|
113
265
|
}
|
|
114
266
|
}
|
|
115
267
|
} catch {}
|
|
116
|
-
return { success: true, platform: PLATFORM, displays: displays.length > 0 ? displays : undefined
|
|
268
|
+
return { success: true, platform: PLATFORM, displays: displays.length > 0 ? displays : undefined };
|
|
117
269
|
}
|
|
118
270
|
|
|
119
|
-
return { success: false, error: 'Gecersiz action: ' + action + ' (screenshot, click, type, keypress, mouse_move, mouse_position, scroll, info)' };
|
|
271
|
+
return { success: false, error: 'Gecersiz action: ' + action + ' (screenshot, click, type, keypress, mouse_move, mouse_position, scroll, drag, info)' };
|
|
120
272
|
}
|
|
121
273
|
|
|
122
274
|
module.exports = {
|
|
123
275
|
name: 'computer_use',
|
|
124
|
-
description: 'GUI otomasyonu: screenshot, click, type, keypress, mouse_move, mouse_position, scroll, info. macOS/Windows/Linux.',
|
|
276
|
+
description: 'GUI otomasyonu: screenshot, click, type, keypress, mouse_move, mouse_position, scroll, drag, info. macOS/Windows/Linux.',
|
|
125
277
|
inputSchema: {
|
|
126
278
|
type: 'object',
|
|
127
279
|
properties: {
|
|
128
|
-
action: { type: 'string', description: 'screenshot, click, type, keypress, mouse_move, mouse_position, scroll, info', enum: ['screenshot', 'click', 'type', 'keypress', 'mouse_move', 'mouse_position', 'scroll', 'info'] },
|
|
280
|
+
action: { type: 'string', description: 'screenshot, click, type, keypress, mouse_move, mouse_position, scroll, drag, info', enum: ['screenshot', 'click', 'type', 'keypress', 'mouse_move', 'mouse_position', 'scroll', 'drag', 'info'] },
|
|
129
281
|
x: { type: 'number', description: 'X koordinati' },
|
|
130
282
|
y: { type: 'number', description: 'Y koordinati' },
|
|
131
283
|
button: { type: 'string', description: 'Fare tusu: left, right, middle (default: left)' },
|
|
132
284
|
clicks: { type: 'number', description: 'Tiklama sayisi (default: 1)' },
|
|
133
|
-
key: { type: 'string', description: '(keypress) Tus adi: enter, tab, escape, up, down, left, right, backspace, delete' },
|
|
285
|
+
key: { type: 'string', description: '(keypress) Tus adi veya kombinasyon: enter, tab, escape, up, down, left, right, backspace, delete, cmd+q, cmd+shift+z, alt+space' },
|
|
286
|
+
x2: { type: 'number', description: '(drag) Bitis X koordinati' },
|
|
287
|
+
y2: { type: 'number', description: '(drag) Bitis Y koordinati' },
|
|
134
288
|
text: { type: 'string', description: '(type) Yazilacak metin' },
|
|
135
|
-
query: { type: 'string', description: 'Arama sorgusu' },
|
|
136
289
|
file: { type: 'string', description: '(screenshot) Kayit dosyasi' },
|
|
137
290
|
},
|
|
138
291
|
required: ['action'],
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* computer_use_loop — Autonomous GUI interaction with visual feedback loop
|
|
3
|
+
*
|
|
4
|
+
* Takes a goal → loops: screenshot → LLM vision analysis → execute action →
|
|
5
|
+
* screenshot → verify → repeat until goal achieved.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const https = require('https');
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const os = require('os');
|
|
12
|
+
|
|
13
|
+
function loadConfig() {
|
|
14
|
+
try { return JSON.parse(fs.readFileSync(path.join(os.homedir(), '.natureco', 'config.json'), 'utf8')); } catch { return {}; }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isMiniMax(url) { return url && (url.includes('minimax.io') || url.includes('minimaxi.com') || url.includes('minimax.cn')); }
|
|
18
|
+
function isGemini(url) { return url && (url.includes('generativelanguage.googleapis.com') || url.includes('gemini')); }
|
|
19
|
+
|
|
20
|
+
function apiCall(providerUrl, apiKey, body) {
|
|
21
|
+
return new Promise((resolve, reject) => {
|
|
22
|
+
const base = providerUrl.replace(/\/+$/, '');
|
|
23
|
+
const endpoint = isMiniMax(base)
|
|
24
|
+
? base + '/v1/text/chatcompletion_v2'
|
|
25
|
+
: isGemini(base)
|
|
26
|
+
? base + '/openai/chat/completions'
|
|
27
|
+
: base + '/chat/completions';
|
|
28
|
+
const req = https.request(endpoint, {
|
|
29
|
+
method: 'POST',
|
|
30
|
+
headers: { 'Authorization': 'Bearer ' + apiKey, 'Content-Type': 'application/json' },
|
|
31
|
+
timeout: 120000,
|
|
32
|
+
}, (res) => {
|
|
33
|
+
let data = '';
|
|
34
|
+
res.on('data', c => data += c);
|
|
35
|
+
res.on('end', () => {
|
|
36
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
37
|
+
try { resolve(JSON.parse(data)); } catch { reject(new Error('Parse error')); }
|
|
38
|
+
} else reject(new Error('HTTP ' + res.statusCode + ': ' + data.slice(0, 500)));
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
req.on('error', reject);
|
|
42
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('Timeout')); });
|
|
43
|
+
req.write(JSON.stringify(body));
|
|
44
|
+
req.end();
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function screenshotBase64() {
|
|
49
|
+
const file = path.join(os.tmpdir(), 'ncloop_' + Date.now() + '.png');
|
|
50
|
+
require('child_process').execSync('screencapture -x "' + file + '"', { timeout: 5000 });
|
|
51
|
+
const buf = fs.readFileSync(file);
|
|
52
|
+
fs.unlinkSync(file);
|
|
53
|
+
return buf.toString('base64');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function executeAction(action, params) {
|
|
57
|
+
const { spawnSync } = require('child_process');
|
|
58
|
+
const PLATFORM = os.platform();
|
|
59
|
+
|
|
60
|
+
const ESC = (s) => s.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
61
|
+
|
|
62
|
+
function osaScript(script, timeoutMs = 10000) {
|
|
63
|
+
const r = spawnSync('osascript', ['-e', script], { timeout: timeoutMs, encoding: 'utf8', maxBuffer: 1024 * 1024 });
|
|
64
|
+
if (r.error) return { success: false, error: r.error.code === 'ETIMEDOUT' ? 'osascript timed out' : r.error.message };
|
|
65
|
+
if (r.status !== 0) return { success: false, error: r.stderr || r.stdout || 'error' };
|
|
66
|
+
return { success: true };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
switch (action) {
|
|
71
|
+
case 'click':
|
|
72
|
+
return osaScript('tell application "System Events" to click at {' + params.x + ', ' + params.y + '}');
|
|
73
|
+
case 'type':
|
|
74
|
+
return osaScript('tell application "System Events" to keystroke "' + ESC(params.text) + '"');
|
|
75
|
+
case 'keypress': {
|
|
76
|
+
const KEY_MAP = { enter: 'return', tab: 'tab', escape: 'escape', up: 'up', down: 'down', left: 'left', right: 'right', backspace: 'delete', space: 'space' };
|
|
77
|
+
const MOD_MAP = { cmd: 'command down', command: 'command down', option: 'option down', alt: 'option down', ctrl: 'control down', shift: 'shift down' };
|
|
78
|
+
const parts = params.key.toLowerCase().split('+').map(p => p.trim());
|
|
79
|
+
const mods = [];
|
|
80
|
+
let actual = '';
|
|
81
|
+
for (const p of parts) {
|
|
82
|
+
if (MOD_MAP[p]) mods.push(MOD_MAP[p]);
|
|
83
|
+
else actual = p;
|
|
84
|
+
}
|
|
85
|
+
const k = KEY_MAP[actual] || actual;
|
|
86
|
+
const using = mods.length > 0 ? ' using {' + mods.join(', ') + '}' : '';
|
|
87
|
+
return osaScript('tell application "System Events" to keystroke ' + k + using);
|
|
88
|
+
}
|
|
89
|
+
case 'mouse_move':
|
|
90
|
+
return osaScript('tell application "System Events" to set position of mouse to {' + params.x + ', ' + params.y + '}');
|
|
91
|
+
case 'scroll': {
|
|
92
|
+
const times = Math.abs(Math.ceil((params.y || 0) / 40));
|
|
93
|
+
return osaScript('tell application "System Events"\n repeat ' + times + ' times\n key code 125\n end repeat\nend tell');
|
|
94
|
+
}
|
|
95
|
+
case 'wait':
|
|
96
|
+
return { success: true };
|
|
97
|
+
default:
|
|
98
|
+
return { success: false, error: 'Unknown action: ' + action };
|
|
99
|
+
}
|
|
100
|
+
} catch (e) {
|
|
101
|
+
return { success: false, error: e.message };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const SYSTEM_PROMPT = `Sen bir GUI otomasyon asistanisin. Gorev: Ekran goruntusunu analiz et ve siradaki action'i JSON olarak belirle.
|
|
106
|
+
|
|
107
|
+
Actions:
|
|
108
|
+
- click: { "action": "click", "x": sayi, "y": sayi }
|
|
109
|
+
- type: { "action": "type", "text": "yazilacak metin" }
|
|
110
|
+
- keypress: { "action": "keypress", "key": "enter|tab|escape|up|down|left|right|backspace|cmd+q|alt+space" }
|
|
111
|
+
- mouse_move: { "action": "mouse_move", "x": sayi, "y": sayi }
|
|
112
|
+
- scroll: { "action": "scroll", "y": piksel_sayisi (positive=down, negative=up) }
|
|
113
|
+
- wait: { "action": "wait" }
|
|
114
|
+
- done: { "action": "done", "reason": "gorev tamamlandi" }
|
|
115
|
+
|
|
116
|
+
Sadece JSON yanit ver, baska metin ekleme. Her action'dan sonra otomatik screenshot alinir, sen sadece bir sonraki en mantikli adimi soyle.`;
|
|
117
|
+
|
|
118
|
+
async function loop(goal, maxSteps) {
|
|
119
|
+
const cfg = loadConfig();
|
|
120
|
+
const providerUrl = cfg.providerUrl;
|
|
121
|
+
const apiKey = cfg.providerApiKey;
|
|
122
|
+
const model = cfg.providerModel || 'default';
|
|
123
|
+
|
|
124
|
+
if (!providerUrl || !apiKey) {
|
|
125
|
+
return JSON.stringify({ success: false, error: 'Provider not configured' });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const steps = [];
|
|
129
|
+
for (let i = 0; i < maxSteps; i++) {
|
|
130
|
+
try {
|
|
131
|
+
// 1. Screenshot
|
|
132
|
+
const b64 = screenshotBase64();
|
|
133
|
+
steps.push({ step: i + 1, action: 'screenshot' });
|
|
134
|
+
|
|
135
|
+
// 2. LLM vision analysis
|
|
136
|
+
const messages = [
|
|
137
|
+
{ role: 'system', content: SYSTEM_PROMPT },
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
const history = steps.filter(s => s.action && s.action !== 'screenshot' && s.action !== 'done').slice(-5);
|
|
141
|
+
if (history.length > 0) {
|
|
142
|
+
messages.push({ role: 'user', content: 'Onceki adimlar:\n' + history.map(h =>
|
|
143
|
+
'Adim ' + h.step + ': ' + JSON.stringify(h)
|
|
144
|
+
).join('\n') });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
messages.push({
|
|
148
|
+
role: 'user',
|
|
149
|
+
content: [
|
|
150
|
+
{ type: 'text', text: 'Gorev: ' + goal + '\n\nEkran goruntusunu analiz et. Siradaki action ne?' },
|
|
151
|
+
{ type: 'image_url', image_url: { url: 'data:image/png;base64,' + b64 } },
|
|
152
|
+
],
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const body = {
|
|
156
|
+
model,
|
|
157
|
+
messages,
|
|
158
|
+
stream: false,
|
|
159
|
+
temperature: 0.2,
|
|
160
|
+
max_tokens: 500,
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const result = await apiCall(providerUrl, apiKey, body);
|
|
164
|
+
const reply = result.choices?.[0]?.message?.content || '';
|
|
165
|
+
let decision;
|
|
166
|
+
try {
|
|
167
|
+
decision = JSON.parse(reply);
|
|
168
|
+
} catch {
|
|
169
|
+
const m = reply.match(/\{[\s\S]*\}/);
|
|
170
|
+
decision = m ? JSON.parse(m[0]) : { action: 'wait' };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (!decision.action) decision.action = 'wait';
|
|
174
|
+
|
|
175
|
+
// 3. Execute
|
|
176
|
+
if (decision.action === 'done') {
|
|
177
|
+
steps.push({ step: i + 1, action: 'done', reason: decision.reason || 'Goal achieved' });
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const execResult = executeAction(decision.action, decision);
|
|
182
|
+
steps.push({ step: i + 1, action: decision.action, params: decision, result: execResult.success });
|
|
183
|
+
|
|
184
|
+
if (!execResult.success) {
|
|
185
|
+
// Retry once with wait
|
|
186
|
+
require('child_process').execSync('sleep 1');
|
|
187
|
+
const retryResult = executeAction(decision.action, decision);
|
|
188
|
+
if (!retryResult.success) {
|
|
189
|
+
steps.push({ step: i + 1, action: 'error', error: execResult.error });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Small delay between actions
|
|
194
|
+
require('child_process').execSync('sleep 0.5');
|
|
195
|
+
|
|
196
|
+
// Safety: if too many steps, break
|
|
197
|
+
if (i >= maxSteps - 1) {
|
|
198
|
+
steps.push({ step: i + 1, action: 'done', reason: 'Max steps reached' });
|
|
199
|
+
}
|
|
200
|
+
} catch (e) {
|
|
201
|
+
steps.push({ step: i + 1, action: 'error', error: e.message });
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return JSON.stringify({ success: true, goal, totalSteps: steps.length, steps });
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const name = 'computer_use_loop';
|
|
210
|
+
const description = 'Otonom GUI etkilesimi: bir hedef ver, loop halinde screenshot → LLM analizi → action → screenshot... hedef tamamlanana kadar devam eder. Web formlari, login, menus islemleri icin ideal.';
|
|
211
|
+
const parameters = {
|
|
212
|
+
type: 'object',
|
|
213
|
+
properties: {
|
|
214
|
+
goal: { type: 'string', description: 'Yapilacak islem (ornek: "natureco.me/login sayfasinda kullanici adi ve sifre girerek giris yap")' },
|
|
215
|
+
maxSteps: { type: 'number', description: 'Maksimum adim sayisi (default: 30)' },
|
|
216
|
+
},
|
|
217
|
+
required: ['goal'],
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
async function execute(params) {
|
|
221
|
+
return await loop(params.goal, params.maxSteps || 30);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
module.exports = { name, description, parameters, execute };
|
package/src/utils/tools.js
CHANGED
|
@@ -64,7 +64,7 @@ const EMOJI_MAP = {
|
|
|
64
64
|
url_safety: '🛡️', approval: '✅', checkpoint: '💾', file_state: '🔍',
|
|
65
65
|
pii_redact: '🔒', clarify: '❓', session_search: '🔎', x_search: '🐦',
|
|
66
66
|
discord: '💬', send_message: '📨', async_delegation: '⏳', blueprint: '📐',
|
|
67
|
-
spotify: '🎧', homeassistant: '🏠', microsoft_graph: '📊',
|
|
67
|
+
spotify: '🎧', homeassistant: '🏠', microsoft_graph: '📊', computer_use: '🖱️', computer_use_loop: '🔄',
|
|
68
68
|
google_meet: '📹',
|
|
69
69
|
// Orchestrator
|
|
70
70
|
workflow: '⚙️',
|
|
@@ -122,7 +122,7 @@ const TOOLSET_MAP = {
|
|
|
122
122
|
x_search: 'web', discord: 'communication', send_message: 'communication',
|
|
123
123
|
async_delegation: 'agent', blueprint: 'planning', workflow: 'orchestrator',
|
|
124
124
|
spotify: 'media', homeassistant: 'iot', microsoft_graph: 'office',
|
|
125
|
-
computer_use: 'automation', google_meet: 'communication',
|
|
125
|
+
computer_use: 'automation', computer_use_loop: 'automation', google_meet: 'communication',
|
|
126
126
|
social_open: 'communication', youtube_ac: 'media',
|
|
127
127
|
sub_agent: 'agent', memory_provider: 'memory',
|
|
128
128
|
};
|