beast-agent 2.6.6 → 2.7.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "beast-agent",
3
3
  "productName": "Beast Agent",
4
- "version": "2.6.6",
4
+ "version": "2.7.0",
5
5
  "description": "Ultra-fast local agent shell for Windows.",
6
6
  "author": "algokodcom (AlgoKod)",
7
7
  "license": "MIT",
@@ -1,109 +1,597 @@
1
1
  'use strict';
2
2
 
3
- /* Beast Computer Use (#4): Windows GUI otomasyonu — ekran görüntüsü +
4
- fare/klavye kontrolü. Ek paket YOK: Electron nativeImage ekranı verir,
5
- mouse/klavye PowerShell üzerinden user32 API ile sürülür.
3
+ /* Beast Computer Use v2 (cua-driver): Windows/macOS/Linux GUI otomasyonu.
4
+ Hermes SKILL.md modeli: arka planda sür (cursor oynatmaz, focus çalmaz),
5
+ önce capture (SOM → numaralı element index'i), element index'iyle tıkla,
6
+ aksiyon sonrası capture_after ile doğrula; yalnız sürücünün döndüğü
7
+ yapılandırılmış sinyale göre (effect/escalation) merdivenden çık.
6
8
 
7
- Güvenlik: yalnızca engine araç çağrılarıyla çalışır; koordinat sınırlı,
8
- hız limiting'li (aksiyon arası min gecikme) ve yazma metni kırpılır. */
9
+ Sürücü: https://github.com/trycua/cua — `cua-driver mcp` stdio MCP sunucusu;
10
+ el yazımı minimal JSON-RPC 2.0 client'ı (mcp.js ile aynı çerçeve).
11
+ Binary yoksa eski PowerShell backend'ine düşer (yalnız koordinat + foreground).
9
12
 
10
- const { execFile } = require('child_process');
13
+ Kurulum (Windows): irm https://cua.ai/driver/install.ps1 | iex
14
+ Override: BEAST_CUA_DRIVER_CMD ile sürücü yolu/başka binary verilebilir.
11
15
 
12
- /* aksiyonlar arası minimum boşluk — wx'i art arda tıklarla donatmamak için */
13
- let lastActionAt = 0;
14
- function pace() {
15
- const wait = 120 - (Date.now() - lastActionAt);
16
- lastActionAt = Date.now();
17
- if (wait > 0) return new Promise((r) => setTimeout(r, wait));
18
- return Promise.resolve();
16
+ Güvenlik: şüpheli shell yazma pattern'leri ve kilit/logout tuş kombinasyonları
17
+ modül seviyesinde blokludur; onay kapısı engine tarafında çalışır. */
18
+
19
+ const { spawn, execFile } = require('child_process');
20
+ const log = require('./logger');
21
+ const { emitInstallProgress } = require('./progressbus');
22
+
23
+ /* ---------- durum ---------- */
24
+ const S = {
25
+ proc: null,
26
+ buf: '',
27
+ nextId: 1,
28
+ pending: new Map(),
29
+ booted: false,
30
+ tools: null,
31
+ downUntil: 0,
32
+ starting: null,
33
+ target: null, /* son hedef: { pid, window_id, app_name, title } */
34
+ snapshot: null, /* { id, pid, window_id, map:Map(element_index→element), at } */
35
+ probe: null, /* { at, res } — 60 sn cache'li sürücü taraması */
36
+ };
37
+ const CALL_TIMEOUT = 120000; /* capture'ın AX yürüyüşü 20 sn'yi bulabilir */
38
+ const RETRY_COOLDOWN = 30000;
39
+
40
+ /* ---------- otomatik kurulum (Hermes installer deseni: best-effort, arka planda) ---------- */
41
+
42
+ const CUA_INSTALL_PS1 = 'https://cua.ai/driver/install.ps1';
43
+ const CUA_INSTALL_SH = 'https://cua.ai/driver/install.sh';
44
+ /* oturum başına tek otomatik kurulum; elle override edilen binary'ye dokunulmaz */
45
+ const INST = { running: null, done: false, failed: false, startedAt: 0, tick: null };
46
+
47
+ function autoInstallAllowed() {
48
+ /* BEAST_CUA_DRIVER_CMD verildiyse kullanıcı sürücüyü kendisi yönetiyor —
49
+ otomatik kurulum asla çalışmaz (tests dahil). */
50
+ return !String(process.env.BEAST_CUA_DRIVER_CMD || '').trim();
19
51
  }
20
52
 
21
- function ps(script) {
22
- return new Promise((resolve, reject) => {
53
+ /* cua-driver --version: kurulu mu? 60 sn cache'li (Kurulum sekmesi 4 sn'de tarar) */
54
+ function probe(force) {
55
+ if (!force && S.probe && Date.now() - S.probe.at < 60000) return Promise.resolve(S.probe.res);
56
+ return new Promise((resolve) => {
57
+ execFile(driverCmd(), ['--version'], { timeout: 10000, windowsHide: true }, (err, stdout) => {
58
+ const res = {
59
+ installed: !err && !!String(stdout || '').trim(),
60
+ version: String(stdout || '').trim().split('\n')[0] || '',
61
+ installing: !!INST.running,
62
+ failed: !!INST.failed,
63
+ };
64
+ S.probe = { at: Date.now(), res };
65
+ resolve(res);
66
+ });
67
+ });
68
+ }
69
+
70
+ function runInstaller(cb) {
71
+ if (process.platform === 'win32') {
23
72
  execFile(
24
73
  'powershell.exe',
25
- ['-NoProfile', '-NonInteractive', '-Command', script],
26
- { timeout: 20000, windowsHide: true },
27
- (err, stdout) => (err ? reject(err) : resolve(String(stdout || '').trim()))
74
+ ['-NoProfile', '-NonInteractive', '-Command', `irm ${CUA_INSTALL_PS1} | iex`],
75
+ { timeout: 15 * 60 * 1000, windowsHide: true },
76
+ cb
28
77
  );
78
+ } else {
79
+ execFile('bash', ['-lc', `curl -fsSL ${CUA_INSTALL_SH} | bash`], { timeout: 15 * 60 * 1000 }, cb);
80
+ }
81
+ }
82
+
83
+ /** cua-driver kurulumunu arka planda başlat (Hermes'in best-effort installer'ı gibi).
84
+ Yüzde üretmeyen installer için Kurulum sekmesinde yavaş ilerleme çubuğu çizilir. */
85
+ function autoInstall() {
86
+ if (!autoInstallAllowed()) {
87
+ return { ok: false, disabled: true, error: 'BEAST_CUA_DRIVER_CMD elle verilmiş — otomatik kurulum atlandı' };
88
+ }
89
+ if (INST.running) return { ok: true, installing: true };
90
+ if (INST.done) return { ok: true, installing: false, done: true };
91
+ if (INST.failed) return { ok: false, error: 'önceki kurulum denemesi başarısız — elle kur: irm ' + CUA_INSTALL_PS1 + ' | iex' };
92
+ INST.startedAt = Date.now();
93
+ log.info('computeruse', 'cua-driver kurulumu arka planda başlıyor (cua.ai installer)');
94
+ INST.tick = setInterval(() => {
95
+ /* yüzdeleri üretmeyen installer: 10 sn'de bir yumuşak ilerleme, tavan %95 */
96
+ const el = (Date.now() - INST.startedAt) / 1000;
97
+ emitInstallProgress('cua-driver', { pct: Math.min(95, 3 + el / 4), loaded: 0, total: 0 });
98
+ }, 10000);
99
+ INST.running = new Promise((resolve) => {
100
+ runInstaller((err, stdout, stderr) => {
101
+ if (INST.tick) { clearInterval(INST.tick); INST.tick = null; }
102
+ INST.running = null;
103
+ S.probe = null;
104
+ if (err) {
105
+ INST.failed = true;
106
+ log.error('computeruse', 'cua-driver kurulumu başarısız: ' + String((stderr || err.message) || '').slice(0, 200));
107
+ resolve({ ok: false, error: 'kurulum başarısız — elle kur: irm ' + CUA_INSTALL_PS1 + ' | iex' });
108
+ } else {
109
+ INST.done = true;
110
+ S.downUntil = 0; /* sonraki computer_use çağrısı taze boot dener */
111
+ log.info('computeruse', 'cua-driver kuruldu — sonraki computer_use çağrısında devreye girer');
112
+ emitInstallProgress('cua-driver', { pct: 100, loaded: 1, total: 1 });
113
+ resolve({ ok: true, done: true });
114
+ }
115
+ });
29
116
  });
117
+ return { ok: true, installing: true };
30
118
  }
31
119
 
32
- /** Sanal masaüstü çözünürlüğü */
33
- async function screenSize() {
34
- const out = await ps(
35
- 'Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.Screen]::PrimaryScreen.Bounds.Width.ToString() + "x" + [System.Windows.Forms.Screen]::PrimaryScreen.Bounds.Height'
36
- );
37
- const m = out.match(/(\d+)x(\d+)/);
38
- if (!m) throw new Error('çözünürlük alınamadı');
39
- return { w: Number(m[1]), h: Number(m[2]) };
120
+ /* sürücü yoksa sessizce tetikle (oturum başına bir kez) */
121
+ function maybeAutoInstall() {
122
+ if (INST.running || INST.done || INST.failed) return;
123
+ if (!autoInstallAllowed()) return;
124
+ probe(true).then((st) => {
125
+ if (!st.installed) autoInstall();
126
+ }).catch(() => {});
40
127
  }
41
128
 
42
- /**
43
- * Aksiyon uygula. op:
44
- * click x y → sol tık
45
- * dblclick x y → çift tık
46
- * rightclick x y → sağ tık
47
- * move x y → imleci taşı
48
- * type text → klavyeyle yaz (Unicode destekli, SendInput WM_CHAR benzeri)
49
- * key combo → "enter", "ctrl+s", "alt+tab", "win" ...
50
- * scroll dx dy → tekerlek
51
- */
52
- async function act(op, args = {}) {
53
- await pace();
54
- const x = Math.max(0, Math.round(Number(args.x) || 0));
55
- const y = Math.max(0, Math.round(Number(args.y) || 0));
129
+ function driverCmd() {
130
+ const v = String(process.env.BEAST_CUA_DRIVER_CMD || '').trim();
131
+ return v || 'cua-driver';
132
+ }
133
+
134
+ /* ---------- MCP stdio istemcisi ---------- */
135
+
136
+ function teardown(reason) {
137
+ if (S.proc) {
138
+ try { S.proc.kill(); } catch {}
139
+ S.proc = null;
140
+ }
141
+ S.booted = false;
142
+ S.tools = null;
143
+ for (const [, p] of S.pending) {
144
+ try { p.reject(new Error(reason || 'cua-driver bağlantısı kapandı')); } catch {}
145
+ }
146
+ S.pending.clear();
147
+ }
148
+
149
+ function boot() {
150
+ return new Promise((resolve) => {
151
+ let done = false;
152
+ const finish = (ok) => {
153
+ if (done) return;
154
+ done = true;
155
+ if (!ok) {
156
+ S.downUntil = Date.now() + RETRY_COOLDOWN;
157
+ maybeAutoInstall(); /* sürücü yoksa arka planda kurulum başlat */
158
+ }
159
+ resolve(ok);
160
+ };
161
+ let proc;
162
+ try {
163
+ proc = spawn(driverCmd(), ['mcp'], {
164
+ windowsHide: true,
165
+ env: { ...process.env, CUA_DRIVER_RS_TELEMETRY_ENABLED: '0' },
166
+ });
167
+ } catch {
168
+ return finish(false);
169
+ }
170
+ S.proc = proc;
171
+ proc.on('error', () => { teardown(); finish(false); });
172
+ proc.on('exit', () => { teardown(); finish(false); });
173
+ proc.stdout.setEncoding('utf8');
174
+ proc.stdout.on('data', (d) => {
175
+ S.buf += d;
176
+ let i;
177
+ while ((i = S.buf.indexOf('\n')) >= 0) {
178
+ const line = S.buf.slice(0, i).trim();
179
+ S.buf = S.buf.slice(i + 1);
180
+ if (!line) continue;
181
+ let m;
182
+ try { m = JSON.parse(line); } catch { continue; }
183
+ if (m.id != null && S.pending.has(m.id)) {
184
+ const p = S.pending.get(m.id);
185
+ S.pending.delete(m.id);
186
+ if (m.error) p.reject(new Error(String((m.error && m.error.message) || 'MCP hatası')));
187
+ else p.resolve(m.result);
188
+ }
189
+ /* bildirimler (agent cursor vb.) yok sayılır */
190
+ }
191
+ });
192
+ /* initialize → initialized → tools/list */
193
+ (async () => {
194
+ try {
195
+ await rpc('initialize', {
196
+ protocolVersion: '2024-11-05',
197
+ capabilities: {},
198
+ clientInfo: { name: 'beast-agent', version: '1.0.0' },
199
+ }, 15000);
200
+ writeLine({ jsonrpc: '2.0', method: 'notifications/initialized' });
201
+ const list = await rpc('tools/list', {}, 15000);
202
+ S.tools = new Set(((list && list.tools) || []).map((t) => t.name));
203
+ S.booted = true;
204
+ finish(true);
205
+ } catch {
206
+ teardown();
207
+ finish(false);
208
+ }
209
+ })();
210
+ });
211
+ }
212
+
213
+ let starting = null;
214
+ async function ensure() {
215
+ if (S.proc && S.booted) return true;
216
+ if (Date.now() < S.downUntil) {
217
+ maybeAutoInstall(); /* cooldown'da da kurulum eksikse başlatılsın */
218
+ return false;
219
+ }
220
+ if (!starting) {
221
+ starting = boot().finally(() => { starting = null; });
222
+ }
223
+ return starting;
224
+ }
225
+
226
+ function writeLine(obj) {
227
+ if (!S.proc || !S.proc.stdin.writable) return;
228
+ try { S.proc.stdin.write(JSON.stringify(obj) + '\n'); } catch {}
229
+ }
230
+
231
+ function rpc(method, params, timeoutMs) {
232
+ return new Promise((resolve, reject) => {
233
+ const id = S.nextId++;
234
+ const timer = setTimeout(() => {
235
+ S.pending.delete(id);
236
+ reject(new Error('cua-driver zaman aşımı: ' + method));
237
+ }, timeoutMs || 30000);
238
+ S.pending.set(id, {
239
+ resolve: (v) => { clearTimeout(timer); resolve(v); },
240
+ reject: (e) => { clearTimeout(timer); reject(e); },
241
+ });
242
+ writeLine({ jsonrpc: '2.0', id, method, params: params || {} });
243
+ });
244
+ }
245
+
246
+ /* MCP CallTool zarfını { isError, text, structured, image } biçimine indirger */
247
+ function readResult(res) {
248
+ let text = '';
249
+ let image = null;
250
+ for (const c of Array.isArray(res && res.content) ? res.content : []) {
251
+ if (c && c.type === 'text') text += (text ? '\n' : '') + String(c.text || '');
252
+ else if (c && c.type === 'image') image = 'data:' + (c.mimeType || 'image/png') + ';base64,' + c.data;
253
+ }
254
+ text = text.replace(/^\u2705+\s*/, '').trim();
255
+ return { isError: !!res.isError, text, structured: (res && res.structuredContent) || null, image };
256
+ }
257
+
258
+ function hasTool(name) {
259
+ return !S.tools || S.tools.has(name); /* tools/list boş döndüyse geç */
260
+ }
261
+
262
+ async function call(tool, args, timeoutMs) {
263
+ if (!(await ensure())) throw new Error('cua-driver yok');
264
+ if (!hasTool(tool)) throw new Error('cua-driver bu aksiyonu desteklemiyor: ' + tool);
265
+ const res = await rpc('tools/call', { name: tool, arguments: args || {} }, timeoutMs || CALL_TIMEOUT);
266
+ return readResult(res);
267
+ }
268
+
269
+ /* structuredContent + metin fallback'ından satır dizisi çıkarır */
270
+ function rowsOf(r) {
271
+ const s = r && r.structured;
272
+ if (Array.isArray(s)) return s;
273
+ if (s && Array.isArray(s.windows)) return s.windows;
274
+ if (s && Array.isArray(s.elements)) return s.elements;
275
+ if (s && Array.isArray(s.apps)) return s.apps;
276
+ if (r && r.text) {
277
+ try {
278
+ const m = r.text.match(/\[[\s\S]*\]/);
279
+ if (m) return JSON.parse(m[0]);
280
+ } catch {}
281
+ }
282
+ return [];
283
+ }
284
+
285
+ /* ---------- hedef seçimi ---------- */
286
+
287
+ async function findTarget(app) {
288
+ const r = await call('list_windows', { on_screen_only: true });
289
+ let cands = rowsOf(r).filter((w) => w && w.pid != null && w.window_id != null);
290
+ if (app) {
291
+ const q = String(app).toLowerCase();
292
+ cands = cands.filter((w) =>
293
+ String(w.app_name || '').toLowerCase().includes(q) ||
294
+ String(w.title || '').toLowerCase().includes(q));
295
+ }
296
+ if (!cands.length) return null;
297
+ cands.sort((a, b) => (Number(b.z_index) || 0) - (Number(a.z_index) || 0));
298
+ return {
299
+ pid: Number(cands[0].pid),
300
+ window_id: Number(cands[0].window_id),
301
+ app_name: String(cands[0].app_name || ''),
302
+ title: String(cands[0].title || ''),
303
+ };
304
+ }
305
+
306
+ /* capture → snapshot cache + Hermes tarzı numaralı index (+ opsiyonel görsel) */
307
+ async function doCapture(args = {}) {
308
+ const t = args.app || !S.target ? await findTarget(args.app) : S.target;
309
+ if (!t) {
310
+ return { ok: false, error: 'hedef pencere bulunamadı' + (args.app ? ': ' + args.app : '') };
311
+ }
312
+ S.target = t;
313
+ const mode = args.mode === 'vision' || args.mode === 'ax' ? args.mode : 'som';
314
+ const wargs = { pid: t.pid, window_id: t.window_id };
315
+ if (mode === 'vision') wargs.include_accessibility_tree = false;
316
+ if (mode === 'ax') wargs.include_screenshot = false;
317
+ const r = await call('get_window_state', wargs);
318
+ if (r.isError) return { ok: false, error: (r.text || 'capture başarısız').slice(0, 300) };
319
+ const s = r.structured || {};
320
+ const elements = Array.isArray(s.elements) ? s.elements : [];
321
+ S.snapshot = {
322
+ id: String(s.snapshot_id || s.snapshotId || ''),
323
+ pid: t.pid,
324
+ window_id: t.window_id,
325
+ map: new Map(),
326
+ at: Date.now(),
327
+ };
328
+ for (const el of elements) {
329
+ if (el && el.element_index != null) S.snapshot.map.set(Number(el.element_index), el);
330
+ }
331
+ const out = {
332
+ ok: true,
333
+ op: 'capture',
334
+ app: t.app_name,
335
+ window: { pid: t.pid, window_id: t.window_id, title: t.title },
336
+ elements: elements.length,
337
+ index: elements.slice(0, 200).map((el) => {
338
+ const f = el.frame || {};
339
+ const val = el.value != null && String(el.value).length ? " = '" + String(el.value).slice(0, 40) + "'" : '';
340
+ return `#${el.element_index} ${el.role || 'Element'} '${String(el.label || '').slice(0, 60)}'${val} @ (${f.x ?? 0}, ${f.y ?? 0}, ${f.w ?? 0}, ${f.h ?? 0})`;
341
+ }).join('\n'),
342
+ note: 'element index ile hedefle: {op:"click", element:N}; element bayatlarsa yeniden capture',
343
+ };
344
+ if (mode !== 'ax' && r.image) out.__injectImage = r.image;
345
+ return out;
346
+ }
347
+
348
+ /* snapshot element çözümü — bayat/bilinmeyen index açık hata verir */
349
+ function resolveEl(args) {
350
+ const idx = Number(args && args.element);
351
+ if (!idx) return {};
352
+ const snap = S.snapshot;
353
+ if (!snap || !snap.map.has(idx)) {
354
+ return { error: `element #${idx} bulunamadı/bayatladı — önce capture`, stale: true };
355
+ }
356
+ return { el: snap.map.get(idx) };
357
+ }
358
+
359
+ /* aksiyon argümanlarını cua-driver hedef biçimine çevirir */
360
+ function address(args, t) {
361
+ const r = resolveEl(args);
362
+ if (r.error) return r;
363
+ const out = {};
364
+ if (r.el) {
365
+ out.pid = t.pid;
366
+ const tk = r.el.element_token || r.el.token;
367
+ if (tk) out.element_token = tk;
368
+ else {
369
+ out.element_index = Number(r.el.element_index);
370
+ out.snapshot_id = S.snapshot.id;
371
+ out.window_id = S.snapshot.window_id;
372
+ }
373
+ return { out };
374
+ }
375
+ if (args.x != null || args.y != null) {
376
+ out.pid = t.pid;
377
+ out.x = Number(args.x) || 0;
378
+ out.y = Number(args.y) || 0;
379
+ return { out };
380
+ }
381
+ /* hedef yok: yalnız uygulama hedefli (focused element) */
382
+ if (args.app) out.pid = t.pid;
383
+ return { out };
384
+ }
385
+
386
+ function verdict(r) {
387
+ if (r.isError) return { ok: false, error: (r.text || 'cua-driver hatası').slice(0, 300) };
388
+ const s = r.structured || {};
389
+ const out = { ok: true, effect: s.effect, verified: s.verified, path: s.path, code: s.code };
390
+ if (s.escalation) out.escalation = s.escalation;
391
+ if (r.text) out.note = r.text.slice(0, 300);
392
+ return out;
393
+ }
394
+
395
+ /* ---------- güvenlik (Hermes SKILL.md sert kuralları) ---------- */
396
+
397
+ const BLOCKED_TYPE_PATTERNS = [
398
+ /\b(?:curl|wget|irm|iwr|invoke-webrequest)\b[^|\n]*\|\s*(?:&\s*)?(?:bash|sh|zsh|powershell|pwsh|iex|invoke-expression)\b/i,
399
+ /\bsudo\s+rm\s+-rf\b/i,
400
+ /\brm\s+-rf\s+(?:\/|~)/,
401
+ /:\(\)\s*\{[^}]*\}\s*;\s*:/,
402
+ ];
403
+ const BLOCKED_KEYS = [/^win\+l$/, /^ctrl\+alt\+del(?:ete)?$/, /^alt\+ctrl\+del(?:ete)?$/];
404
+
405
+ const KEY_ALIASES = {
406
+ enter: 'return', esc: 'escape', del: 'delete', pgup: 'pageup', pgdn: 'pagedown',
407
+ win: 'cmd', option: 'alt', control: 'ctrl',
408
+ };
409
+ const KEY_MODS = new Set(['ctrl', 'alt', 'shift', 'cmd', 'fn']);
410
+
411
+ function guard(op, args) {
412
+ if (op === 'type') {
413
+ const text = String(args.text ?? '');
414
+ if (!text.trim()) return { ok: false, error: 'boş metin' };
415
+ for (const re of BLOCKED_TYPE_PATTERNS) {
416
+ if (re.test(text)) return { ok: false, error: 'şüpheli shell pattern bloklu', blocked: true };
417
+ }
418
+ }
419
+ if (op === 'key') {
420
+ const combo = String(args.keys || args.combo || '').trim().toLowerCase();
421
+ if (!combo) return { ok: false, error: 'tuş gerekli' };
422
+ const norm = combo.replace(/\s+/g, '');
423
+ for (const re of BLOCKED_KEYS) {
424
+ if (re.test(norm)) return { ok: false, error: 'kilit/logout kombinasyonu bloklu: ' + combo, blocked: true };
425
+ }
426
+ }
427
+ return null;
428
+ }
429
+
430
+ /* ---------- cua-driver aksiyonları ---------- */
431
+
432
+ async function targetFor(args) {
433
+ if (!args || (!args.app && S.target)) return S.target || (await findTarget(null));
434
+ return findTarget(args && args.app);
435
+ }
436
+
437
+ function keyParts(combo) {
438
+ const parts = String(combo).toLowerCase().split('+').map((p) => p.trim()).filter(Boolean);
439
+ const mods = [];
440
+ let key = '';
441
+ for (const p of parts) {
442
+ const k = KEY_ALIASES[p] || p;
443
+ if (KEY_MODS.has(k)) mods.push(k);
444
+ else key = k;
445
+ }
446
+ return { mods, key };
447
+ }
448
+
449
+ async function driverAct(op, args) {
450
+ const t = await targetFor(args);
451
+ if (!t && op !== 'list_apps') {
452
+ return { ok: false, error: 'hedef pencere bulunamadı' + (args.app ? ': ' + args.app : '') };
453
+ }
56
454
  switch (op) {
57
- case 'move':
58
- await mouseMove(x, y);
59
- return { ok: true };
60
455
  case 'click':
61
- await mouseMove(x, y);
62
- await clickSimple('left');
63
- return { ok: true, at: { x, y } };
64
456
  case 'dblclick':
65
- await mouseMove(x, y);
66
- await clickSimple('left');
67
- await sleep(70);
68
- await clickSimple('left');
69
- return { ok: true, at: { x, y } };
70
- case 'rightclick':
71
- await mouseMove(x, y);
72
- await clickSimple('right');
73
- return { ok: true, at: { x, y } };
457
+ case 'rightclick': {
458
+ const a = address(args, t);
459
+ if (a.error) return a;
460
+ if (!a.out.element_token && a.out.element_index == null && a.out.x == null) {
461
+ return { ok: false, error: 'element veya x/y gerekli' };
462
+ }
463
+ const p = a.out;
464
+ if (op === 'click') {
465
+ p.button = ['left', 'right', 'middle'].includes(args.button) ? args.button : 'left';
466
+ return verdict(await call('click', p));
467
+ }
468
+ if (op === 'dblclick') return verdict(await call('double_click', p));
469
+ return verdict(await call('right_click', p));
470
+ }
74
471
  case 'type': {
75
472
  const text = String(args.text ?? '').slice(0, 2000);
76
- if (!text) return { ok: false, error: 'boş metin' };
77
- /* Add-Type SendInput tabanlı Unicode typer */
78
- await ps(buildTyperScript(text));
79
- return { ok: true, chars: text.length };
473
+ const a = address(args, t);
474
+ if (a.error) return a;
475
+ return verdict(await call('type_text', { text, ...a.out }));
80
476
  }
81
477
  case 'key': {
82
- const combo = String(args.combo || args.key || '').trim().slice(0, 40);
83
- if (!combo) return { ok: false, error: 'tuş gerekli' };
84
- await ps(buildKeyScript(combo));
85
- return { ok: true, key: combo };
478
+ const combo = String(args.keys || args.combo || '').trim().toLowerCase();
479
+ const { mods, key } = keyParts(combo);
480
+ if (!key && !mods.length) return { ok: false, error: 'tuş gerekli' };
481
+ const a = address(args, t);
482
+ if (a.error) return a;
483
+ if (mods.length) {
484
+ return verdict(await call('hotkey', { keys: [...mods, key || ''], ...a.out }));
485
+ }
486
+ return verdict(await call('press_key', { key: key || 'return', ...a.out }));
86
487
  }
87
488
  case 'scroll': {
88
- const dy = Math.max(-10, Math.min(10, Math.round(Number(args.dy) || 0)));
89
- if (!dy) return { ok: false, error: 'dy gerekli (-10..10)' };
90
- await mouseMove(x || 640, y || 360);
91
- const clicks = Math.abs(dy) * 3;
92
- for (let i = 0; i < clicks; i++) {
93
- await wheel(dy > 0 ? 1 : -1); // pozitif dy = aşağı
489
+ const direction = ['up', 'down', 'left', 'right'].includes(args.direction) ? args.direction : null;
490
+ if (!direction) return { ok: false, error: 'direction gerekli (up/down/left/right)' };
491
+ const amount = Math.max(1, Math.min(50, Math.round(Number(args.amount) || 3)));
492
+ const a = address(args, t);
493
+ if (a.error) return a;
494
+ return verdict(await call('scroll', { direction, amount, ...a.out }));
495
+ }
496
+ case 'drag': {
497
+ const pt = async (elIdx, ax, ay) => {
498
+ const r = elIdx ? resolveEl({ element: elIdx }) : {};
499
+ if (r.error) return r;
500
+ if (r.el && r.el.frame) {
501
+ return { x: r.el.frame.x + r.el.frame.w / 2, y: r.el.frame.y + r.el.frame.h / 2 };
502
+ }
503
+ if (ax != null && ay != null) return { x: Number(ax), y: Number(ay) };
504
+ return { error: 'drag: from/to (element veya x/y) gerekli' };
505
+ };
506
+ const from = await pt(args.from_element, args.from_x, args.from_y);
507
+ if (from.error) return from;
508
+ const to = await pt(args.to_element, args.to_x, args.to_y);
509
+ if (to.error) return to;
510
+ const p = { from_x: from.x, from_y: from.y, to_x: to.x, to_y: to.y };
511
+ if (args.duration_ms) p.duration_ms = Math.max(0, Math.min(10000, Math.round(Number(args.duration_ms))));
512
+ if (args.steps) p.steps = Math.max(1, Math.min(200, Math.round(Number(args.steps))));
513
+ return verdict(await call('drag', p));
514
+ }
515
+ case 'list_apps': {
516
+ const r = await call('list_apps');
517
+ if (r.isError) return { ok: false, error: (r.text || 'list_apps başarısız').slice(0, 300) };
518
+ const rows = rowsOf(r).slice(0, 40).map((a) => ({
519
+ name: a.name || a.app_name || a.bundle_id || String(a).slice(0, 40),
520
+ pid: a.pid || null,
521
+ running: a.running !== false,
522
+ }));
523
+ return { ok: true, op: 'list_apps', apps: rows, note: 'hedeflemek için op argümanlarında app=<ad> kullan' };
524
+ }
525
+ case 'focus_app': {
526
+ S.target = t;
527
+ if (args.raise_window) {
528
+ const r = await call('bring_to_front', { pid: t.pid });
529
+ if (r.isError) return { ok: false, error: (r.text || 'focus başarısız').slice(0, 300) };
94
530
  }
95
- return { ok: true, dy };
531
+ return {
532
+ ok: true,
533
+ op: 'focus_app',
534
+ app: t.app_name,
535
+ window: { pid: t.pid, window_id: t.window_id, title: t.title },
536
+ note: 'hedef seçildi — girdiler artık bu uygulamaya yönelir (raise_window olmadan odak çalınmaz)',
537
+ };
96
538
  }
97
539
  default:
98
- return { ok: false, error: `bilinmeyen op: ${op}` };
540
+ return null; /* move vb. → legacy */
541
+ }
542
+ }
543
+
544
+ /* capture_after: aksiyon sonrası aynı hedefte doğrulama görüntüsü */
545
+ async function attachCapture(out) {
546
+ try {
547
+ const c = await doCapture({});
548
+ if (c && c.__injectImage) {
549
+ out.__injectImage = c.__injectImage;
550
+ delete c.__injectImage;
551
+ }
552
+ out.capture = { ok: !!c.ok, elements: c.elements, index: c.index || undefined, error: c.error };
553
+ } catch (e) {
554
+ out.capture = { ok: false, error: String((e && e.message) || e) };
99
555
  }
100
556
  }
101
557
 
558
+ /* ---------- legacy PowerShell backend (fallback) ---------- */
559
+
560
+ let lastActionAt = 0;
561
+ function pace() {
562
+ const wait = 120 - (Date.now() - lastActionAt);
563
+ lastActionAt = Date.now();
564
+ if (wait > 0) return new Promise((r) => setTimeout(r, wait));
565
+ return Promise.resolve();
566
+ }
567
+
102
568
  function sleep(ms) {
103
569
  return new Promise((r) => setTimeout(r, ms));
104
570
  }
105
571
 
106
- /* ---------- PowerShell snippet üreticileri ---------- */
572
+ function ps(script) {
573
+ return new Promise((resolve, reject) => {
574
+ execFile(
575
+ 'powershell.exe',
576
+ ['-NoProfile', '-NonInteractive', '-Command', script],
577
+ { timeout: 20000, windowsHide: true },
578
+ (err, stdout) => (err ? reject(err) : resolve(String(stdout || '').trim()))
579
+ );
580
+ });
581
+ }
582
+
583
+ function clamp(n, lo, hi) {
584
+ return Math.max(lo, Math.min(hi, n));
585
+ }
586
+
587
+ async function legacyScreenSize() {
588
+ const out = await ps(
589
+ 'Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.Screen]::PrimaryScreen.Bounds.Width.ToString() + "x" + [System.Windows.Forms.Screen]::PrimaryScreen.Bounds.Height'
590
+ );
591
+ const m = out.match(/(\d+)x(\d+)/);
592
+ if (!m) throw new Error('çözünürlük alınamadı');
593
+ return { w: Number(m[1]), h: Number(m[2]) };
594
+ }
107
595
 
108
596
  const MOUSE_MOVE_TPL = `
109
597
  Add-Type @"
@@ -127,8 +615,7 @@ public class BW {
127
615
  [BW]::mouse_event(0x0800, 0, 0, %%DELTA%%, [UIntPtr]::Zero)
128
616
  `;
129
617
 
130
- /* Unicode güvenli yazım: SendInput yerine pano + Ctrl+V fallback'u en sağlamı.
131
- Panoyu geri yüklemek yerine tek seferlik kullanıp bırakıyoruz (hız öncelik). */
618
+ /* Unicode güvenli yazım: pano + Ctrl+V en sağlamı (tek seferlik, panoyu geri yüklemez) */
132
619
  function buildTyperScript(text) {
133
620
  const b64 = Buffer.from(text, 'utf16le').toString('base64');
134
621
  return `
@@ -156,10 +643,10 @@ function buildKeyScript(combo) {
156
643
  if (p === 'ctrl') mods += '^';
157
644
  else if (p === 'alt') mods += '%';
158
645
  else if (p === 'shift') mods += '+';
159
- else if (p === 'win') out += ''; // win ayrı ele
646
+ else if (p === 'win') out += '';
160
647
  else if (/^f\d{1,2}$/.test(p)) out += '{' + p.toUpperCase() + '}';
161
648
  else if (map[p]) out += map[p];
162
- else out += p.slice(0, 1).toUpperCase(); // tek harf/tuş
649
+ else out += p.slice(0, 1).toUpperCase();
163
650
  }
164
651
  const seq = mods && out ? mods + '(' + out + ')' : mods + out;
165
652
  const b64 = Buffer.from(seq, 'utf16le').toString('base64');
@@ -174,7 +661,6 @@ async function mouseMove(x, y) {
174
661
  await ps(MOUSE_MOVE_TPL.replace(/%%X%%/g, String(x)).replace(/%%Y%%/g, String(y)));
175
662
  }
176
663
 
177
- /* down+up tek script'te — karışık replace zinciri yerine bu kullanılır */
178
664
  async function clickSimple(button) {
179
665
  const down = button === 'right' ? 0x0008 : 0x0002;
180
666
  const up = button === 'right' ? 0x0010 : 0x0004;
@@ -193,8 +679,159 @@ Start-Sleep -Milliseconds 45
193
679
  }
194
680
 
195
681
  async function wheel(dir) {
196
- const delta = dir < 0 ? -240 : 240; // WHEEL_DELTA=120 katları; negatif=yukarı görüntüde aşağı kayar bazı app'lerde
682
+ const delta = dir < 0 ? -240 : 240;
197
683
  await ps(WHEEL_TPL.replace(/%%DELTA%%/g, String(delta)));
198
684
  }
199
685
 
200
- module.exports = { act, screenSize, clickSimple, mouseMove };
686
+ /* legacy op yüzeyi: koordinat tabanlı eski aksiyonlar (cua-driver'sız mod) */
687
+ async function legacyAct(op, args) {
688
+ op = String(op || '').trim().toLowerCase();
689
+ const noDriver = (what) => ({
690
+ ok: false,
691
+ error: `${what} için cua-driver gerekli — kurulum: irm ${CUA_INSTALL_PS1} | iex` +
692
+ (INST.running ? ' (kurulum arka planda sürüyor — birkaç dakika sonra tekrar dene)' : '') +
693
+ (INST.failed ? ' (önceki otomatik kurulum başarısız oldu)' : ''),
694
+ });
695
+ switch (op) {
696
+ case 'capture':
697
+ case 'list_apps':
698
+ case 'focus_app':
699
+ return noDriver('capture/list_apps/focus_app');
700
+ case 'drag':
701
+ return noDriver('drag');
702
+ case 'click':
703
+ case 'dblclick':
704
+ case 'rightclick':
705
+ case 'type': {
706
+ if (args.element != null) return noDriver('element tıklama');
707
+ break;
708
+ }
709
+ default:
710
+ break;
711
+ }
712
+ if (op === 'screen_size') {
713
+ const s = await legacyScreenSize();
714
+ return { ok: true, ...s };
715
+ }
716
+ await pace();
717
+ const x = Math.max(0, Math.round(Number(args.x) || 0));
718
+ const y = Math.max(0, Math.round(Number(args.y) || 0));
719
+ switch (op) {
720
+ case 'move':
721
+ await mouseMove(x, y);
722
+ return { ok: true };
723
+ case 'click':
724
+ if (args.x == null && args.y == null) return { ok: false, error: 'x/y gerekli' };
725
+ await mouseMove(x, y);
726
+ await clickSimple('left');
727
+ return { ok: true, at: { x, y } };
728
+ case 'dblclick':
729
+ if (args.x == null && args.y == null) return { ok: false, error: 'x/y gerekli' };
730
+ await mouseMove(x, y);
731
+ await clickSimple('left');
732
+ await sleep(70);
733
+ await clickSimple('left');
734
+ return { ok: true, at: { x, y } };
735
+ case 'rightclick':
736
+ if (args.x == null && args.y == null) return { ok: false, error: 'x/y gerekli' };
737
+ await mouseMove(x, y);
738
+ await clickSimple('right');
739
+ return { ok: true, at: { x, y } };
740
+ case 'type': {
741
+ const text = String(args.text ?? '').slice(0, 2000);
742
+ if (!text) return { ok: false, error: 'boş metin' };
743
+ await ps(buildTyperScript(text));
744
+ return { ok: true, chars: text.length };
745
+ }
746
+ case 'key': {
747
+ const combo = String(args.keys || args.combo || '').trim().slice(0, 40);
748
+ if (!combo) return { ok: false, error: 'tuş gerekli' };
749
+ await ps(buildKeyScript(combo));
750
+ return { ok: true, key: combo };
751
+ }
752
+ case 'scroll': {
753
+ if (args.direction) {
754
+ const dy = args.direction === 'down' ? Number(args.amount) || 3 : args.direction === 'up' ? -(Number(args.amount) || 3) : 0;
755
+ if (!dy) return { ok: false, error: 'dikey scroll için up/down gerekli' };
756
+ await mouseMove(x || 640, y || 360);
757
+ const clicks = Math.abs(dy) * 3;
758
+ for (let i = 0; i < clicks; i++) await wheel(dy > 0 ? 1 : -1);
759
+ return { ok: true, direction: args.direction, amount: Math.abs(dy) };
760
+ }
761
+ const dy = clamp(Math.round(Number(args.dy) || 0), -10, 10);
762
+ if (!dy) return { ok: false, error: 'direction veya dy gerekli (-10..10)' };
763
+ await mouseMove(x || 640, y || 360);
764
+ const clicks = Math.abs(dy) * 3;
765
+ for (let i = 0; i < clicks; i++) {
766
+ await wheel(dy > 0 ? 1 : -1);
767
+ }
768
+ return { ok: true, dy };
769
+ }
770
+ default:
771
+ return { ok: false, error: `bilinmeyen op: ${op}` };
772
+ }
773
+ }
774
+
775
+ /* ---------- genel giriş ---------- */
776
+
777
+ /**
778
+ * Hermes-SKILL.md aksiyon kümesi:
779
+ * capture mode=som|vision|ax app=… → numaralı element index'i (+görsel)
780
+ * click element=N | x,y button=left|right|middle
781
+ * dblclick / rightclick element=N | x,y
782
+ * type text="…" element=N | x,y
783
+ * key keys="ctrl+s" | "enter"
784
+ * scroll direction=up|down|left|right amount=1..50 element=N | x,y
785
+ * drag from_element/to_element | from_x/y → to_x/y
786
+ * wait seconds=0.5
787
+ * list_apps (argüman yok)
788
+ * focus_app app=… [raise_window=true]
789
+ * move x,y (yalnız legacy — gerçek cursor'u oynatır)
790
+ * State-changing aksiyonlarda capture_after=true → aynı hedefte doğrulama
791
+ * capture'ı tek çağrıda döndürür.
792
+ */
793
+ async function act(op, args = {}) {
794
+ op = String(op || '').trim().toLowerCase();
795
+ args = args || {};
796
+ if (op === 'wait') {
797
+ const ms = clamp(
798
+ args.seconds != null ? Number(args.seconds) * 1000 : args.ms != null ? Number(args.ms) : 500,
799
+ 0, 30000
800
+ );
801
+ await sleep(ms);
802
+ return { ok: true, op: 'wait', waited: Math.round(ms) };
803
+ }
804
+ const g = guard(op, args);
805
+ if (g) return g;
806
+ const stateChanging = ['click', 'dblclick', 'rightclick', 'type', 'key', 'scroll', 'drag'];
807
+ try {
808
+ if (await ensure()) {
809
+ const r = await driverAct(op, args);
810
+ if (r !== null) {
811
+ if (stateChanging.includes(op) && args.capture_after) await attachCapture(r);
812
+ return r;
813
+ }
814
+ }
815
+ } catch (e) {
816
+ teardown(String((e && e.message) || e));
817
+ S.downUntil = Date.now() + RETRY_COOLDOWN;
818
+ /* sürücü koptu → legacy devam */
819
+ }
820
+ return legacyAct(op, args);
821
+ }
822
+
823
+ /* Çözünürlük: sürücü varsa driver (logical points), yoksa PowerShell (piksel) */
824
+ async function screenSize() {
825
+ try {
826
+ if (await ensure()) {
827
+ const r = await call('get_screen_size', {}, 10000);
828
+ const s = r.structured || {};
829
+ const w = Number(s.width || s.w || 0);
830
+ const h = Number(s.height || s.h || 0);
831
+ if (w && h) return { w, h };
832
+ }
833
+ } catch {}
834
+ return legacyScreenSize();
835
+ }
836
+
837
+ module.exports = { act, screenSize, clickSimple, mouseMove, probe, autoInstall };
@@ -4941,19 +4941,36 @@ const TOOLS = [
4941
4941
  function: {
4942
4942
  name: 'computer_act',
4943
4943
  description:
4944
- 'Control mouse/keyboard on the user\u2019s Windows desktop (GUI automation). Ops: click{x,y}, dblclick{x,y}, rightclick{x,y}, move{x,y}, type{text}, key{combo e.g. "ctrl+s","enter","alt+tab"}, scroll{x,y,dy}. Look first with computer_look, act step by step, look again after acting.',
4944
+ 'Drive the user\u2019s Windows desktop in the BACKGROUND via cua-driver: your clicks/typing do NOT move the user\u2019s cursor or steal focus. Canonical flow: 1) capture (mode=som \u2192 screenshot + numbered element index), 2) act by element index (much more reliable than pixels), 3) verify with capture_after=true. Pixel x/y are window-local pixels read from the LATEST capture of that window \u2014 re-capture before clicking. Escalate only on returned signals: effect:"suspected_noop" \u2192 retry pixel, then delivery_mode="foreground". Never type passwords/secrets; never click permission dialogs, 2FA or payment UI \u2014 ask the user instead.',
4945
4945
  parameters: {
4946
4946
  type: 'object',
4947
4947
  properties: {
4948
4948
  op: {
4949
4949
  type: 'string',
4950
- enum: ['click', 'dblclick', 'rightclick', 'move', 'type', 'key', 'scroll'],
4950
+ enum: ['capture', 'click', 'dblclick', 'rightclick', 'type', 'key', 'scroll', 'drag', 'wait', 'list_apps', 'focus_app', 'move'],
4951
+ description: 'capture first (som/vision/ax); click/type/etc act by element or window-local pixel; wait pauses; list_apps enumerates windows; focus_app selects input target (raise_window=true steals focus \u2014 only on explicit request); move = legacy real-cursor move',
4951
4952
  },
4952
- x: { type: 'number' },
4953
- y: { type: 'number' },
4954
- text: { type: 'string', description: 'for op=type (max 2000 chars)' },
4955
- combo: { type: 'string', description: 'for op=key: "enter", "ctrl+s", "alt+tab", "win"' },
4956
- dy: { type: 'number', description: 'for op=scroll: positive = down (-10..10)' },
4953
+ app: { type: 'string', description: 'target app by name/title substring (scopes capture and input)' },
4954
+ element: { type: 'number', description: 'element index from the latest capture \u2014 preferred over x/y' },
4955
+ x: { type: 'number', description: 'window-local pixel (from the latest capture of that window)' },
4956
+ y: { type: 'number', description: 'window-local pixel (from the latest capture of that window)' },
4957
+ button: { type: 'string', enum: ['left', 'right', 'middle'] },
4958
+ text: { type: 'string', description: 'for op=type (max 2000 chars); NEVER type passwords or secrets' },
4959
+ keys: { type: 'string', description: 'for op=key: "enter", "ctrl+s", "alt+tab"' },
4960
+ direction: { type: 'string', enum: ['up', 'down', 'left', 'right'], description: 'for op=scroll' },
4961
+ amount: { type: 'number', description: 'for op=scroll: wheel notches 1..50 (default 3)' },
4962
+ from_element: { type: 'number', description: 'for op=drag' },
4963
+ to_element: { type: 'number', description: 'for op=drag' },
4964
+ from_x: { type: 'number', description: 'for op=drag (window-local pixels)' },
4965
+ from_y: { type: 'number', description: 'for op=drag (window-local pixels)' },
4966
+ to_x: { type: 'number', description: 'for op=drag (window-local pixels)' },
4967
+ to_y: { type: 'number', description: 'for op=drag (window-local pixels)' },
4968
+ seconds: { type: 'number', description: 'for op=wait (max 30)' },
4969
+ mode: { type: 'string', enum: ['som', 'vision', 'ax'], description: 'for op=capture: som = screenshot + numbered elements (default), vision = screenshot only, ax = element tree only' },
4970
+ capture_after: { type: 'boolean', description: 're-capture the target right after this action (verify in the same call)' },
4971
+ delivery_mode: { type: 'string', enum: ['background', 'foreground'], description: 'background (default) never raises the window; foreground is the last-resort rung \u2014 use ONLY after effect:"suspected_noop" or background_unavailable' },
4972
+ dy: { type: 'number', description: 'legacy scroll fallback: positive = down (-10..10)' },
4973
+ combo: { type: 'string', description: 'legacy alias for keys' },
4957
4974
  },
4958
4975
  required: ['op'],
4959
4976
  },
@@ -69,7 +69,7 @@ const TOOL_LABELS = {
69
69
  kb_search: (a) => `Bilgi ara: ${String(a.query || '').slice(0, 60)}`,
70
70
  memory_hygiene: () => 'Hafıza temizliği',
71
71
  computer_look: () => 'Ekranı görüntülüyor',
72
- computer_act: (a) => `Bilgisayar kullanıyor: ${a.op || ''}${a.combo ? ' ' + a.combo : ''}`,
72
+ computer_act: (a) => `Bilgisayar kullanıyor: ${a.op || ''}${a.app ? ' → ' + a.app : ''}${a.element != null ? ' #' + a.element : ''}${(a.keys || a.combo) ? ' [' + (a.keys || a.combo) + ']' : ''}${a.direction ? ' ' + a.direction : ''}`,
73
73
  };
74
74
 
75
75
  function waToolLine(name, args) {
package/src/main.js CHANGED
@@ -6068,12 +6068,33 @@ ipcMain.handle('install:status', async () => {
6068
6068
  rows.push({ id: 'python', name: 'Python — betikler / web arama', state: pyVer ? 'ok' : 'optional', detail: pyVer ? 'v' + pyVer : 'sistemde bulunamadı — opsiyonel' });
6069
6069
  }
6070
6070
 
6071
- /* 7) Edge TTS (bulut) */
6071
+ /* 7) cua-driver — Computer Use (arka plan GUI otomasyonu) */
6072
+ {
6073
+ const st = await require('./agent/computeruse').probe().catch(() => ({ installed: false }));
6074
+ rows.push({
6075
+ id: 'cua-driver',
6076
+ name: 'cua-driver — Computer Use (masaüstü otomasyonu)',
6077
+ state: st.installed ? 'ok' : (st.installing ? 'loading' : (st.failed ? 'failed' : 'missing')),
6078
+ detail: st.installed
6079
+ ? (st.version ? 'kurulu — ' + st.version : 'kurulu')
6080
+ : st.installing
6081
+ ? 'cua.ai sürücüsü arka planda kuruluyor'
6082
+ : (st.failed
6083
+ ? 'kurulum başarısız — elle: irm https://cua.ai/driver/install.ps1 | iex'
6084
+ : 'ilk Computer Use kullanımında arka planda kurulur'),
6085
+ ...(st.installing ? pctFields('cua-driver') : {}),
6086
+ });
6087
+ }
6088
+
6089
+ /* 8) Edge TTS (bulut) */
6072
6090
  rows.push({ id: 'edge', name: 'Edge TTS — seslendirme', state: 'cloud', detail: 'bulut — kurulum gerekmez' });
6073
6091
 
6074
6092
  return rows;
6075
6093
  });
6076
6094
 
6095
+ /* cua-driver kurulumunu şimdi başlat (Kurulum sekmesi otomatiği + elle tetikleme) */
6096
+ ipcMain.handle('cua:install', () => require('./agent/computeruse').autoInstall());
6097
+
6077
6098
  /* embedding modelini şimdi indir (mem0 arama yolu ısıtılır) */
6078
6099
  ipcMain.handle('embed:prefetch', () => {
6079
6100
  try {
package/src/preload.js CHANGED
@@ -56,6 +56,7 @@ contextBridge.exposeInMainWorld('beast', {
56
56
  sttStatus: () => ipcRenderer.invoke('stt:status'),
57
57
  sttPrefetchNow: () => ipcRenderer.invoke('stt:prefetch'),
58
58
  installStatus: () => ipcRenderer.invoke('install:status'),
59
+ cuaInstall: () => ipcRenderer.invoke('cua:install'),
59
60
  embedPrefetch: () => ipcRenderer.invoke('embed:prefetch'),
60
61
  sttLangSet: (lang) => ipcRenderer.invoke('stt:lang:set', lang),
61
62
  updateStatus: () => ipcRenderer.invoke('update:status'),
@@ -23,6 +23,8 @@
23
23
  tip_net_offline: 'İnternet: yok — bağlantı dönünce işler otomatik sürer',
24
24
  sess_drag: 'Sürükleyerek sırayı değiştir',
25
25
  sess_del_confirm: 'Bu sohbet oturumunu silmek istediğine emin misin?',
26
+ cf_ok: 'Evet, sil',
27
+ cf_cancel: 'Vazgeç',
26
28
  tipWatch: 'İzleyiciler (Watchers)',
27
29
  tipCron: 'Cron Görevler',
28
30
  tip_light: 'Açık tema',
@@ -653,6 +655,8 @@
653
655
  tip_net_online: 'Internet: connected',
654
656
  tip_net_offline: 'Internet: offline — work resumes automatically once the connection returns',
655
657
  sess_drag: 'Drag to reorder',
658
+ cf_ok: 'Yes, delete',
659
+ cf_cancel: 'Cancel',
656
660
  tipWatch: 'Watchers',
657
661
  tipCron: 'Cron Jobs',
658
662
  tip_light: 'Light theme',
@@ -452,6 +452,18 @@
452
452
 
453
453
  <div id="fileCtxMenu" hidden></div>
454
454
 
455
+ <!-- in-app onay penceresi: window.confirm Electron/Windows'ta input focus'unu
456
+ bozuyor (electron#31917) — native dialog YOK, hepsi bu modaldan döner -->
457
+ <div id="confirmOverlay" class="mini-overlay" hidden>
458
+ <div class="confirm-dialog">
459
+ <div id="confirmText" class="confirm-text"></div>
460
+ <div class="confirm-btns">
461
+ <button id="confirmCancel" class="btn ghost"></button>
462
+ <button id="confirmOk" class="btn danger"></button>
463
+ </div>
464
+ </div>
465
+ </div>
466
+
455
467
  <input type="file" id="fileInput" multiple hidden
456
468
  accept="image/*,.txt,.md,.json,.csv,.log,.js,.ts,.py,.ps1,.bat,.cmd,.html,.css,.yaml,.yml,.xml,.ini" />
457
469
 
@@ -362,6 +362,33 @@ function toast(msg) {
362
362
  }, 2200);
363
363
  }
364
364
 
365
+ /* IN-APP CONFIRM: window.confirm Electron/Windows'ta kapanınca input focus'unu
366
+ bozuyor (electron#31917 — yazma kabul etmez, klavye ölür). Native dialog
367
+ HİÇ açılmaz; bu modal söz verir (promise) ve kapanınca odağı geri verir. */
368
+ let confirmResolve = null;
369
+ function uiConfirm(msg, okLabel, cancelLabel) {
370
+ return new Promise((resolve) => {
371
+ const ov = $('#confirmOverlay');
372
+ if (!ov) { resolve(window.confirm(msg)); return; }
373
+ $('#confirmText').textContent = String(msg || '');
374
+ $('#confirmOk').textContent = okLabel || _t('cf_ok');
375
+ $('#confirmCancel').textContent = cancelLabel || _t('cf_cancel');
376
+ ov.hidden = false;
377
+ confirmResolve = resolve;
378
+ $('#confirmOk').focus();
379
+ });
380
+ }
381
+ function closeConfirm(result) {
382
+ const ov = $('#confirmOverlay');
383
+ if (ov) ov.hidden = true;
384
+ if (confirmResolve) {
385
+ const r = confirmResolve;
386
+ confirmResolve = null;
387
+ r(result);
388
+ els.input.focus(); /* odayı input'a geri ver — yazma asla kilitli kalmasın */
389
+ }
390
+ }
391
+
365
392
  /* ---------------- messages ---------------- */
366
393
 
367
394
  function addUserBubble(content, atts) {
@@ -829,12 +856,23 @@ async function renderSessions(list) {
829
856
  row.addEventListener('click', () => openSession(s.id));
830
857
  row.querySelector('.sess-del').addEventListener('click', async (e) => {
831
858
  e.stopPropagation();
832
- if (!window.confirm((_t('sess_del_confirm') || 'Bu sohbet oturumunu silmek istediğine emin misin?') + '\n\n' + (s.title || 'Yeni Sohbet'))) return;
859
+ const ok = await uiConfirm(
860
+ (_t('sess_del_confirm') || 'Bu sohbet oturumunu silmek istediğine emin misin?') + '\n\n' + (s.title || 'Yeni Sohbet')
861
+ );
862
+ if (!ok) return;
833
863
  await beast.deleteSession(s.id);
834
864
  if (s.id === activeId) {
865
+ /* aktif sohbet silindi: ÖLÜ UÇ bırakma — taze oturum aç, input yazılabilir
866
+ kalsın (activeId null kalırsa sendCurrent sessizce return eder) */
835
867
  activeId = null;
868
+ streamEl = null;
836
869
  els.msgs.innerHTML = '';
837
870
  showEmpty(true);
871
+ try {
872
+ const created = await beast.createSession();
873
+ await openSession(created.id);
874
+ } catch {}
875
+ els.input.focus();
838
876
  }
839
877
  sessionOrder = sessionOrder.filter((x) => x !== s.id);
840
878
  refreshSessions();
@@ -2154,6 +2192,7 @@ async function renderInstallPane() {
2154
2192
  loading: ['İNİYOR…', '#d9a441'],
2155
2193
  partial: ['İNİYOR… (devam)', '#d9a441'],
2156
2194
  missing: ['İNDİRİLİYOR…', '#d9a441'],
2195
+ failed: ['HATA', '#c0554d'],
2157
2196
  cloud: ['BULUT', 'var(--muted)'],
2158
2197
  optional: ['OPSİYONEL', 'var(--muted)'],
2159
2198
  };
@@ -2173,6 +2212,10 @@ async function renderInstallPane() {
2173
2212
  autoStarted.add('emb');
2174
2213
  beast.embedPrefetch().catch(() => {});
2175
2214
  }
2215
+ if (r.id === 'cua-driver' && r.state === 'missing') {
2216
+ autoStarted.add('cua-driver');
2217
+ beast.cuaInstall().catch(() => {});
2218
+ }
2176
2219
  }
2177
2220
  };
2178
2221
 
@@ -4400,8 +4443,8 @@ function renderBotSettings(pane, b) {
4400
4443
  else toast(r.error || _t('bot_save_fail'));
4401
4444
  });
4402
4445
  const delBtn = $('#bDel');
4403
- if (delBtn) delBtn.addEventListener('click', async () => {
4404
- if (!confirm(_ti('bot_confirm_del', b.name))) return;
4446
+ if (delBtn) delBtn.addEventListener('click', async () => {
4447
+ if (!(await uiConfirm(_ti('bot_confirm_del', b.name)))) return;
4405
4448
  const r = await beast.botsRemove(b.id);
4406
4449
  if (r.ok) {
4407
4450
  /* bot silme sonrası main otomatik restart atar — yeniden çizmeye kalkışma */
@@ -4943,7 +4986,13 @@ function onEvent(ev) {
4943
4986
  ingestAgentActivity(ev);
4944
4987
  return;
4945
4988
  }
4946
- if (ev.type === 'done' || ev.type === 'error') refreshSessions();
4989
+ if (ev.type === 'done' || ev.type === 'error') {
4990
+ /* başka oturumun işi bitti (silinen oturum dahil): busy'i de bırak —
4991
+ yoksa stopBtn görünür kalır, sonraki mesajlar 'kuyruğa eklendi'
4992
+ deyip sonsuza dek bekler (sohbet silince kilitlenme bu yüzden) */
4993
+ if (busy) setBusy(false);
4994
+ refreshSessions();
4995
+ }
4947
4996
  return;
4948
4997
  }
4949
4998
 
@@ -5791,11 +5840,30 @@ async function init() {
5791
5840
  }
5792
5841
 
5793
5842
  els.newChat.addEventListener('click', async () => {
5794
- const created = await beast.createSession();
5795
- await openSession(created.id);
5843
+ try {
5844
+ const created = await beast.createSession();
5845
+ await openSession(created.id);
5846
+ } catch (e) {
5847
+ toast('Yeni sohbet açılamadı: ' + ((e && e.message) || e));
5848
+ }
5796
5849
  els.input.focus();
5797
5850
  });
5798
5851
 
5852
+ /* in-app confirm modal düğmeleri + Enter/Esc kısayolları */
5853
+ {
5854
+ const ok = $('#confirmOk');
5855
+ const cancel = $('#confirmCancel');
5856
+ const ov = $('#confirmOverlay');
5857
+ if (ok && cancel && ov) {
5858
+ ok.addEventListener('click', () => closeConfirm(true));
5859
+ cancel.addEventListener('click', () => closeConfirm(false));
5860
+ ov.addEventListener('keydown', (e) => {
5861
+ if (e.key === 'Escape') { e.stopPropagation(); closeConfirm(false); }
5862
+ else if (e.key === 'Enter') { e.stopPropagation(); closeConfirm(true); }
5863
+ });
5864
+ }
5865
+ }
5866
+
5799
5867
  /* custom pencere butonları: küçült / büyüt / kapat — main'e window:ctrl ile gider.
5800
5868
  Büyüt ikonu maximize/unmaximize olayıyla senkron (□ / ❐). */
5801
5869
  const winMin = $('#winMin');
@@ -6052,7 +6120,7 @@ async function init() {
6052
6120
  });
6053
6121
  if (els.railClear) {
6054
6122
  els.railClear.addEventListener('click', async () => {
6055
- if (!confirm('TÜM paralel ajan geçmişi silinsin mi?\n(Çalışan ajanlar da iptal edilir — geri alınamaz)')) return;
6123
+ if (!(await uiConfirm('TÜM paralel ajan geçmişi silinsin mi?\n(Çalışan ajanlar da iptal edilir — geri alınamaz)'))) return;
6056
6124
  try {
6057
6125
  const r = await beast.agentsClearAll();
6058
6126
  if (r && r.ok) toast('Paralel ajan geçmişi silindi' + (r.removed ? ' — ' + r.removed + ' iş' : ''));
@@ -7616,15 +7684,16 @@ async function codeOpen(rel) {
7616
7684
  els.codeTa.focus();
7617
7685
  }
7618
7686
 
7619
- function codeClose(i) {
7687
+ async function codeClose(i) {
7620
7688
  if (i < 0 || i >= codeTabs.length) return;
7621
7689
  /* KAYIT DİSİPLİNİ: düzenlemeler yalnız Kaydet/Ctrl+S ile diske yazılır.
7622
7690
  Kaydedilmemiş sekme kapatılırken kullanıcıya sorulur — sessiz kayıp yok. */
7623
7691
  if (codeTabs[i].dirty) {
7624
- const ok = confirm(
7692
+ const ok = await uiConfirm(
7625
7693
  '"' + codeTabs[i].rel + '" kaydedilmedi.\n\n' +
7626
7694
  'Tamam = kaydetmeden kapat\n' +
7627
- 'İptal = editörde kal (kaydetmek için Ctrl+S)'
7695
+ 'İptal = editörde kal (kaydetmek için Ctrl+S)',
7696
+ 'Kaydetmeden kapat'
7628
7697
  );
7629
7698
  if (!ok) return;
7630
7699
  }
@@ -8217,7 +8286,7 @@ function bcResetPanel() {
8217
8286
  /* TEK oturum silme — onay sorulur */
8218
8287
  async function bcDeleteHistory(id, title) {
8219
8288
  if (!id) return;
8220
- if (!confirm('"' + (title || 'Beast Code oturumu') + '" silinsin mi?\n\nBu işlem geri alınamaz.')) return;
8289
+ if (!(await uiConfirm('"' + (title || 'Beast Code oturumu') + '" silinsin mi?\n\nBu işlem geri alınamaz.'))) return;
8221
8290
  const r = await beast.bcDelete(id).catch(() => null);
8222
8291
  if (!r || !r.ok) { toast((r && r.error) || 'silinemedi'); return; }
8223
8292
  if (bcSessionId === id) {
@@ -8234,7 +8303,7 @@ if (els.bcHistClear) els.bcHistClear.addEventListener('click', async () => {
8234
8303
  const r0 = await beast.bcHistory().catch(() => null);
8235
8304
  const n = r0 && r0.ok ? (r0.items || []).length : 0;
8236
8305
  if (!n) { toast('Silinecek oturum yok'); return; }
8237
- if (!confirm(n + ' Beast Code oturumunun TÜMÜ silinsin mi?\n\nBu işlem geri alınamaz.')) return;
8306
+ if (!(await uiConfirm(n + ' Beast Code oturumunun TÜMÜ silinsin mi?\n\nBu işlem geri alınamaz.'))) return;
8238
8307
  const r = await beast.bcDeleteAll().catch(() => null);
8239
8308
  if (!r || !r.ok) { toast((r && r.error) || 'silinemedi'); return; }
8240
8309
  /* aktif oturum da silindiyse panel tazelensin */
@@ -1575,6 +1575,23 @@ body.browser-open #settingsDialog { width: min(70vw, calc(100vw - var(--bw, 480p
1575
1575
  }
1576
1576
  .btn:hover { filter: brightness(1.08); }
1577
1577
  .btn.ghost { background: var(--panel2); color: var(--text); border: 1px solid var(--border); }
1578
+ .btn.danger { background: var(--err); color: var(--bg); }
1579
+
1580
+ /* in-app confirm (window.confirm Electron'ta input focus'unu bozuyor) */
1581
+ .confirm-dialog {
1582
+ width: min(420px, 88vw);
1583
+ background: var(--bg);
1584
+ border: 1px solid var(--border);
1585
+ border-radius: 14px;
1586
+ padding: 18px 20px 16px;
1587
+ box-shadow: 0 24px 64px rgba(0, 0, 0, 0.25);
1588
+ display: flex;
1589
+ flex-direction: column;
1590
+ gap: 14px;
1591
+ }
1592
+ .confirm-text { font-size: 13.5px; line-height: 1.5; white-space: pre-wrap; word-break: break-word; }
1593
+ .confirm-btns { display: flex; justify-content: flex-end; gap: 8px; }
1594
+ .confirm-btns .btn { margin-top: 0; }
1578
1595
 
1579
1596
  .inp {
1580
1597
  width: 100%;