beast-agent 2.11.0 → 2.13.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/src/main.js CHANGED
@@ -27,6 +27,8 @@ const usageMod = require('./agent/usage');
27
27
  const bus = require('./agent/bus');
28
28
  const computeruse = require('./agent/computeruse');
29
29
  const log = require('./agent/logger');
30
+ const headroom = require('./agent/headroom');
31
+ const QRCode = require('qrcode'); /* Expo Go QR (bc-expurl) — whatsapp ile aynı paket */
30
32
 
31
33
  /* #3 otomatik updater: sessiz — indirir, kapanışta kurar, kullanıcıya soru sormaz.
32
34
  Paketlenmemiş (npm start) modda devre dışı; Update sekmesi ve /update komutu kontrol eder. */
@@ -3103,11 +3105,16 @@ function reloadBackend() {
3103
3105
  flushDesktopOnDone(ev); /* biriken desktop mesajlarını sıraya bas */
3104
3106
  bcFlushOnDone(ev); /* Beast Code kuyruğunu iş bitiminde boşalt */
3105
3107
  stFlushOnDone(ev); /* Beast Studio kuyruğunu iş bitiminde boşalt */
3108
+ sbFlushOnDone(ev); /* Sandbox kuyruğunu iş bitiminde boşalt */
3106
3109
  /* BC canlı önizleme: ajan bir dev server başlattıysa adresi yakala —
3107
3110
  preview butonu ve otomatik açılış DAİMA bu sunucuyu öncelikli kullanır */
3108
3111
  if (ev.type === 'bc-preview' && /^https?:\/\/(?:localhost|127\.0\.0\.1)(?::\d{2,5})?/i.test(String(ev.url || ''))) {
3109
3112
  bcLastServerUrl = String(ev.url);
3110
3113
  }
3114
+ /* Expo Go QR: ajanın dev sunucusu exp:// adresi yazdıysa QR üretip panele bas */
3115
+ if (ev.type === 'bc-expurl') {
3116
+ bcExpQrMake(String(ev.url || '')).catch(() => {});
3117
+ }
3111
3118
  /* WA canlı iş takibi: oturum bir WhatsApp sohbetine bağlıysa araç
3112
3119
  hareketlerini (terminal, web, dosya…) kısa satırla bildir.
3113
3120
  Spam olmasın: sohbet başına en az 7 sn'de bir tek satır. */
@@ -3284,6 +3291,33 @@ function reloadBackend() {
3284
3291
  }
3285
3292
  },
3286
3293
  });
3294
+ /* PANEL RUN köprüsü: ajanın panel_run aracı → ÇALIŞTIR panelindeki yönetilen
3295
+ süreç koşucusu (sandbox:run IPC ile aynı makine). */
3296
+ engine.sbRunHook = sbRunStartManaged;
3297
+
3298
+ /* HEADROOM (opsiyonel token sıkıştırma proxy'si): engine zincirinden provider
3299
+ listesi alınır; durum değişince renderer'a 'headroom-status' düşer */
3300
+ headroom.setProviderGetter(() => {
3301
+ const seen = new Map();
3302
+ for (const c of (engine && engine.cfg && engine.cfg.chain) || []) {
3303
+ if (!c || !c.providerId || !c.url || seen.has(c.providerId)) continue;
3304
+ seen.set(c.providerId, {
3305
+ id: String(c.providerId),
3306
+ name: String(c.providerName || c.providerId),
3307
+ baseUrl: String(c.url).replace(/\/chat\/completions$/, ''),
3308
+ });
3309
+ }
3310
+ return [...seen.values()];
3311
+ });
3312
+ headroom.setStatusSink((st) => {
3313
+ try {
3314
+ if (win && !win.isDestroyed()) win.webContents.send('agent:event', { type: 'headroom-status', status: st });
3315
+ } catch {}
3316
+ });
3317
+ /* ayarlardan açık kaldıysa: uygulama açılışında arka planda başlat */
3318
+ if (settings.headroom && settings.headroom.enabled) {
3319
+ setTimeout(() => { headroom.setEnabled(true).catch(() => {}); }, 4000);
3320
+ }
3287
3321
  return engine.publicState();
3288
3322
  }
3289
3323
 
@@ -3512,6 +3546,7 @@ app.whenReady().then(() => {
3512
3546
  flushBrowserStorage(); // x.com/google oturumları (cookies) diske yazılsın
3513
3547
  try { toolsMod.disposeShellSessions(); } catch {} // kalıcı shell oturumlarını kapat
3514
3548
  try { require('./agent/mcp').stopAll(); } catch {} // MCP server süreçlerini kapat
3549
+ try { headroom.stopAll(); } catch {} // headroom proxy süreçlerini kapat
3515
3550
  });
3516
3551
 
3517
3552
  if (process.argv.includes('--smoke')) {
@@ -6244,12 +6279,45 @@ ipcMain.handle('install:status', async () => {
6244
6279
  /* 8) Edge TTS (bulut) */
6245
6280
  rows.push({ id: 'edge', name: 'Edge TTS — seslendirme', state: 'cloud', detail: 'bulut — kurulum gerekmez' });
6246
6281
 
6282
+ /* 9) Headroom CLI — arka plan paket kurulumu burada görünür;
6283
+ aç/kapat Token Sıkıştırma sekmesinde */
6284
+ {
6285
+ const st = headroom.probe();
6286
+ let state = 'optional';
6287
+ if (st.installed) state = 'ok';
6288
+ else if (st.installing) state = 'loading';
6289
+ if (st.failed) state = 'failed';
6290
+ rows.push({
6291
+ id: 'headroom',
6292
+ name: 'Headroom CLI — token sıkıştırma motoru',
6293
+ state,
6294
+ detail: st.installing
6295
+ ? 'arka planda kuruluyor — uv tool install headroom-ai[proxy]'
6296
+ : st.failed
6297
+ ? (st.error || 'kurulum başarısız')
6298
+ : st.installed
6299
+ ? 'kurulu — aç/kapat: Token Sıkıştırma sekmesi'
6300
+ : 'kurulu değil — Token Sıkıştırma sekmesinden açınca otomatik kurulur',
6301
+ ...(st.installing ? pctFields('headroom') : {}),
6302
+ });
6303
+ }
6304
+
6247
6305
  return rows;
6248
6306
  });
6249
6307
 
6250
6308
  /* cua-driver kurulumunu şimdi başlat (Kurulum sekmesi otomatiği + elle tetikleme) */
6251
6309
  ipcMain.handle('cua:install', () => require('./agent/computeruse').autoInstall());
6252
6310
 
6311
+ /* HEADROOM (opsiyonel token sıkıştırma): aç/kapa + durum */
6312
+ ipcMain.handle('headroom:toggle', async (_e, on) => {
6313
+ settings.headroom = { ...(settings.headroom || {}), enabled: !!on };
6314
+ saveSettings();
6315
+ const r = await headroom.setEnabled(!!on).catch((e) => ({ ok: false, error: String((e && e.message) || e) }));
6316
+ return r;
6317
+ });
6318
+ ipcMain.handle('headroom:status', () => headroom.probe());
6319
+ ipcMain.handle('headroom:stats', () => headroom.stats());
6320
+
6253
6321
  /* embedding modelini şimdi indir (mem0 arama yolu ısıtılır) */
6254
6322
  ipcMain.handle('embed:prefetch', () => {
6255
6323
  try {
@@ -7459,9 +7527,10 @@ ipcMain.handle('beastcode:send', async (_e, payload) => {
7459
7527
  return { ok: true, sessionId: s.id, mode: s.bcMode };
7460
7528
  }
7461
7529
  if (busy) {
7462
- /* ajan çalışıyor → kuyruğa al; iş bitince (done/error) toplu gider */
7463
- const q = bcQueuePush(ws, text, attachments);
7464
- return { ok: true, queued: true, count: q.msgs.length, sessionId: s.id };
7530
+ /* OPENCODE STEER: koşan tur varken mesaj kuyrukta BEKLEMEZ — engine.send
7531
+ konuşmaya ekler, koşan tur sonraki istekte görür (ana chat ile aynı) */
7532
+ engine.send(s.id, attachments.length ? { text, attachments } : text, { userAction: true });
7533
+ return { ok: true, sessionId: s.id, steered: true };
7465
7534
  }
7466
7535
  /* boşta: kısa pencere — hızlı ard arda mesajlar tek işte birleşir */
7467
7536
  const q = bcQueuePush(ws, text, attachments);
@@ -7745,8 +7814,9 @@ ipcMain.handle('studio:send', async (_e, payload) => {
7745
7814
  return { ok: true, sessionId: s.id, mode: s.bcMode };
7746
7815
  }
7747
7816
  if (busy) {
7748
- const q = stQueuePush(ws, text, attachments);
7749
- return { ok: true, queued: true, count: q.msgs.length, sessionId: s.id };
7817
+ /* OPENCODE STEER: koşan tur varken mesaj kuyrukta beklemez — konuşmaya eklenir */
7818
+ engine.send(s.id, attachments.length ? { text, attachments } : text, { userAction: true });
7819
+ return { ok: true, sessionId: s.id, steered: true };
7750
7820
  }
7751
7821
  const q = stQueuePush(ws, text, attachments);
7752
7822
  clearTimeout(q.timer);
@@ -8226,6 +8296,454 @@ ipcMain.handle('studio:delete', async (_e, rel) => {
8226
8296
  }
8227
8297
  });
8228
8298
 
8299
+ /* Sağ tık menüsü: HTML dosyasını dahili tarayıcıda GÖRÜNÜR aç */
8300
+ /* ---------- BEAST SANDBOX: GitHub repolarını indir & içinde çalış ----------
8301
+ Her repo KENDİ klasöründe (~/Beast-Sandbox/<ad>) ve KENDİ gizli oturumunda
8302
+ koşar (klasör bazlı, bcCode disiplinli — Beast Code motorunun aynısı; ajanın
8303
+ run_command/read/write araçları repo klasöründe çalışır). Ana sohbet VE
8304
+ Code/Studio panellerinden tamamen ayrıdır; ana chat solda yerinde kalır. */
8305
+ const sbSessions = new Map(); /* klasör → sessionId */
8306
+ const SB_DEBOUNCE_MS = 900;
8307
+ const sbQueue = new Map(); /* klasör → { timer, msgs[] } */
8308
+
8309
+ function sandboxRoot() {
8310
+ const dir = path.join(app.getPath('home'), 'Beast-Sandbox');
8311
+ try { fs.mkdirSync(dir, { recursive: true }); } catch {}
8312
+ return dir;
8313
+ }
8314
+
8315
+ function sbSessionFile() {
8316
+ return path.join(beastDir(), 'sandbox-sessions.json');
8317
+ }
8318
+
8319
+ let sbMapLoaded = false;
8320
+ function sbLoadMap() {
8321
+ if (sbMapLoaded) return;
8322
+ sbMapLoaded = true;
8323
+ try {
8324
+ const raw = JSON.parse(fs.readFileSync(sbSessionFile(), 'utf8'));
8325
+ for (const [k, v] of Object.entries(raw || {})) {
8326
+ if (k && typeof v === 'string') sbSessions.set(k, v);
8327
+ }
8328
+ } catch {}
8329
+ }
8330
+
8331
+ function sbSaveMap() {
8332
+ try {
8333
+ fs.writeFileSync(sbSessionFile(), JSON.stringify(Object.fromEntries(sbSessions), null, 2));
8334
+ } catch {}
8335
+ }
8336
+
8337
+ function sandboxGetSession(folder) {
8338
+ sbLoadMap();
8339
+ let sid = sbSessions.get(folder);
8340
+ if (sid) {
8341
+ try {
8342
+ const s = engine.cache.get(sid) || engine._load(sid);
8343
+ if (s) return s;
8344
+ } catch {}
8345
+ sbSessions.delete(folder);
8346
+ }
8347
+ const s = engine._load(engine.createSession().id);
8348
+ s.messages = s.messages || [];
8349
+ s.bgTitle = 'Beast Sandbox'; /* _view.isBg → sohbet geçmişinde gizli */
8350
+ s.bcCode = true; /* todo disiplini + hızlı iş kapanışı (engine) */
8351
+ s.sbSandbox = true; /* engine: SANDBOX sistem bloğu — ajan açık kaynak repoda olduğunu bilir */
8352
+ try {
8353
+ fs.appendFileSync(
8354
+ engine._file(s.id),
8355
+ JSON.stringify({ t: 'meta2', bgOf: '', title: 'Beast Sandbox', at: new Date().toISOString() }) + '\n'
8356
+ );
8357
+ } catch {}
8358
+ bcMarkWs(s, folder); /* klasör bağı oturum dosyasına yazılır */
8359
+ engine.cache.set(s.id, s);
8360
+ sbSessions.set(folder, s.id);
8361
+ sbSaveMap();
8362
+ return s;
8363
+ }
8364
+
8365
+ function sbQueuePush(ws, text, attachments) {
8366
+ let q = sbQueue.get(ws);
8367
+ if (!q) {
8368
+ q = { timer: null, msgs: [] };
8369
+ sbQueue.set(ws, q);
8370
+ }
8371
+ q.msgs.push({
8372
+ text,
8373
+ attachments: Array.isArray(attachments) && attachments.length ? attachments : undefined,
8374
+ });
8375
+ return q;
8376
+ }
8377
+
8378
+ function sbFlush(folder) {
8379
+ const q = sbQueue.get(folder);
8380
+ if (!q || !q.msgs.length) return;
8381
+ const s = sandboxGetSession(folder);
8382
+ if (engine.isBusy(s.id)) return; /* steer sistemine rağmen kuyruğu koru */
8383
+ let merged = '';
8384
+ let mergedAtts = null;
8385
+ for (const m of q.msgs) {
8386
+ if (m.text) merged += (merged ? '\n' : '') + m.text;
8387
+ if (!mergedAtts && Array.isArray(m.attachments) && m.attachments.length) mergedAtts = m.attachments;
8388
+ }
8389
+ if (!merged.trim() && !mergedAtts) {
8390
+ sbQueue.delete(folder);
8391
+ clearTimeout(q.timer);
8392
+ return;
8393
+ }
8394
+ s.workspace = folder;
8395
+ s.bcCode = true;
8396
+ s.sbSandbox = true;
8397
+ engine.cache.set(s.id, s);
8398
+ const payload = mergedAtts ? { text: merged, attachments: mergedAtts } : merged;
8399
+ engine.send(s.id, payload, { userAction: true });
8400
+ sbQueue.delete(folder);
8401
+ clearTimeout(q.timer);
8402
+ }
8403
+
8404
+ function sbFlushOnDone(ev) {
8405
+ if (!ev || (ev.type !== 'done' && ev.type !== 'error') || !ev.sessionId) return;
8406
+ for (const [folder, sid] of sbSessions) {
8407
+ if (String(sid) === String(ev.sessionId) && sbQueue.has(folder)) {
8408
+ setTimeout(() => { try { sbFlush(folder); } catch {} }, 150);
8409
+ }
8410
+ }
8411
+ }
8412
+
8413
+ /* repo adresten (url | owner/repo) normalize et */
8414
+ function sbRepoFromInput(input) {
8415
+ let s = String(input || '').trim().replace(/\.git$/, '');
8416
+ if (!s) return null;
8417
+ const m = /^(?:https?:\/\/)?(?:www\.)?github\.com\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)/i.exec(s);
8418
+ if (m) return { url: 'https://github.com/' + m[1] + '/' + m[2] + '.git', name: m[2] };
8419
+ if (/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(s)) {
8420
+ const parts = s.split('/');
8421
+ return { url: 'https://github.com/' + parts[0] + '/' + parts[1] + '.git', name: parts[1] };
8422
+ }
8423
+ return null;
8424
+ }
8425
+
8426
+ ipcMain.handle('sandbox:list', async () => {
8427
+ const root = sandboxRoot();
8428
+ const items = [];
8429
+ try {
8430
+ sbLoadMap();
8431
+ for (const e of fs.readdirSync(root, { withFileTypes: true })) {
8432
+ if (!e.isDirectory() || e.name.startsWith('.')) continue;
8433
+ const folder = path.join(root, e.name);
8434
+ let sid = '';
8435
+ let busy = false;
8436
+ const known = sbSessions.get(folder);
8437
+ if (known) {
8438
+ sid = String(known);
8439
+ try { busy = engine.isBusy(known); } catch {}
8440
+ }
8441
+ items.push({ name: e.name, folder, sid, busy });
8442
+ }
8443
+ } catch {}
8444
+ return { ok: true, root, items };
8445
+ });
8446
+
8447
+ ipcMain.handle('sandbox:clone', async (_e, input) => {
8448
+ const repo = sbRepoFromInput(input);
8449
+ if (!repo) return { ok: false, error: 'github.com/owner/repo, owner/repo ya da tam .git adresi gir' };
8450
+ const root = sandboxRoot();
8451
+ const base = String(repo.name || 'repo').replace(/[\\/:*?"<>|]/g, '_').slice(0, 60) || 'repo';
8452
+ let dest = path.join(root, base);
8453
+ let n = 2;
8454
+ while (fs.existsSync(dest)) dest = path.join(root, base + '-' + n++);
8455
+ const finalName = path.basename(dest);
8456
+ return new Promise((resolve) => {
8457
+ const { spawn } = require('child_process');
8458
+ let settled = false;
8459
+ let lastPct = -1;
8460
+ let lastEmit = 0;
8461
+ let errText = '';
8462
+ /* git ilerlemeyi stderr'a \r ile AYNI satıra yazar (Receiving objects: %34 …)
8463
+ — her karede yüzde yakalanır, panele canlı akar */
8464
+ const onChunk = (buf) => {
8465
+ const txt = String(buf);
8466
+ errText += txt;
8467
+ const frames = txt.split(/\r/);
8468
+ const cur = (frames[frames.length - 1] || '').trim();
8469
+ const m = /(\d{1,3})%/.exec(cur);
8470
+ const pct = m ? Math.min(99, Number(m[1])) : null;
8471
+ const now = Date.now();
8472
+ if (cur && (pct !== null ? pct !== lastPct : now - lastEmit > 400)) {
8473
+ lastPct = pct != null ? pct : lastPct;
8474
+ lastEmit = now;
8475
+ try {
8476
+ if (win && !win.isDestroyed()) {
8477
+ win.webContents.send('agent:event', { type: 'sb-clone', pct, text: cur.slice(0, 90) });
8478
+ }
8479
+ } catch {}
8480
+ }
8481
+ };
8482
+ let proc;
8483
+ try {
8484
+ proc = spawn('git', ['clone', '--progress', repo.url, dest], { windowsHide: true });
8485
+ } catch (e) {
8486
+ return resolve({ ok: false, error: 'git başlatılamadı: ' + String((e && e.message) || e) });
8487
+ }
8488
+ proc.stdout.on('data', onChunk);
8489
+ proc.stderr.on('data', onChunk);
8490
+ const done = (err) => {
8491
+ if (settled) return;
8492
+ settled = true;
8493
+ try {
8494
+ if (win && !win.isDestroyed()) win.webContents.send('agent:event', { type: 'sb-clone', pct: null, done: !err });
8495
+ } catch {}
8496
+ if (err) {
8497
+ try { fs.rmSync(dest, { recursive: true, force: true }); } catch {}
8498
+ const estr = errText || String((err && err.message) || err);
8499
+ const msg = err && err.code === 'ENOENT'
8500
+ ? 'git kurulu değil — https://git-scm.com adresinden kur'
8501
+ : /not found|does not exist|Could not/i.test(estr)
8502
+ ? 'repo bulunamadı ya da erişilemedi (gizli repo mu?)'
8503
+ : /Authentication|403|Permission/i.test(estr)
8504
+ ? 'erişim reddedildi (gizli repo ya da ağ sorunu)'
8505
+ : (estr.split('\n').filter((l) => l.trim()).pop() || 'klonlama başarısız').slice(0, 200);
8506
+ resolve({ ok: false, error: msg });
8507
+ return;
8508
+ }
8509
+ log.info('main', 'sandbox: klonlandı ' + repo.url + ' → ' + finalName);
8510
+ resolve({ ok: true, name: finalName, folder: dest });
8511
+ };
8512
+ proc.on('error', done);
8513
+ proc.on('close', (code) => done(code === 0 ? null : { code }));
8514
+ /* güvenlik tavanı: 10 dk */
8515
+ setTimeout(() => {
8516
+ if (!settled) {
8517
+ try { proc.kill(); } catch {}
8518
+ done(new Error('zaman aşımı'));
8519
+ }
8520
+ }, 600000);
8521
+ });
8522
+ });
8523
+
8524
+ ipcMain.handle('sandbox:send', async (_e, payload) => {
8525
+ const text = String((payload && payload.msg) || '').trim();
8526
+ const attachments = Array.isArray(payload && payload.attachments) ? payload.attachments : [];
8527
+ const folder = String((payload && payload.folder) || '');
8528
+ if (!folder) return { ok: false, error: 'repo seçilmedi' };
8529
+ if (!folder.startsWith(sandboxRoot() + path.sep)) return { ok: false, error: 'geçersiz klasör' };
8530
+ if (!text && !attachments.length) return { ok: false, error: 'boş mesaj' };
8531
+ if (!engine) return { ok: false, error: 'ajan hazır değil' };
8532
+ if (!engine.publicState().hasModel) return { ok: false, error: 'model yok — Ayarlar → Provider sekmesinden ekle' };
8533
+ const s = sandboxGetSession(folder);
8534
+ s.workspace = folder;
8535
+ s.bcCode = true;
8536
+ s.sbSandbox = true;
8537
+ engine.cache.set(s.id, s);
8538
+ const busy = engine.isBusy(s.id);
8539
+ const q = sbQueuePush(folder, text, attachments);
8540
+ if (busy) return { ok: true, queued: true, count: q.msgs.length, sessionId: s.id };
8541
+ clearTimeout(q.timer);
8542
+ q.timer = setTimeout(() => { try { sbFlush(folder); } catch {} }, SB_DEBOUNCE_MS);
8543
+ return { ok: true, sessionId: s.id, pending: true };
8544
+ });
8545
+
8546
+ ipcMain.handle('sandbox:stop', async (_e, payload) => {
8547
+ const folder = String((payload && payload.folder) || '');
8548
+ sbLoadMap();
8549
+ const sid = sbSessions.get(folder);
8550
+ const q = sbQueue.get(folder);
8551
+ if (q) {
8552
+ clearTimeout(q.timer);
8553
+ sbQueue.delete(folder);
8554
+ }
8555
+ const wasBusy = sid ? engine.isBusy(sid) : false;
8556
+ let r = false;
8557
+ if (wasBusy) {
8558
+ try { r = engine.interrupt(sid, 'kullanıcı Sandbox panelinden ■ ile durdurdu'); } catch {}
8559
+ }
8560
+ return { ok: true, wasBusy, interrupted: r };
8561
+ });
8562
+
8563
+ ipcMain.handle('sandbox:new', async (_e, payload) => {
8564
+ const folder = String((payload && payload.folder) || '');
8565
+ sbLoadMap();
8566
+ const sid = sbSessions.get(folder);
8567
+ if (sid && engine.isBusy(sid)) return { ok: false, error: 'mesaj sürüyor — önce ■ ile durdur' };
8568
+ const q = sbQueue.get(folder);
8569
+ if (q) {
8570
+ clearTimeout(q.timer);
8571
+ sbQueue.delete(folder);
8572
+ }
8573
+ if (sid) {
8574
+ try { engine.deleteSession(sid); } catch {}
8575
+ sbSessions.delete(folder);
8576
+ sbSaveMap();
8577
+ }
8578
+ return { ok: true };
8579
+ });
8580
+
8581
+ /* repo klasörünü diskten sil (oturum kaydıyla birlikte temizlenir) */
8582
+ ipcMain.handle('sandbox:remove', async (_e, payload) => {
8583
+ const folder = String((payload && payload.folder) || '');
8584
+ if (!folder.startsWith(sandboxRoot() + path.sep)) return { ok: false, error: 'geçersiz klasör' };
8585
+ sbLoadMap();
8586
+ const sid = sbSessions.get(folder);
8587
+ if (sid && engine.isBusy(sid)) return { ok: false, error: 'repo çalışıyor — önce ■ ile durdur' };
8588
+ try { fs.rmSync(folder, { recursive: true, force: true }); } catch (e) {
8589
+ return { ok: false, error: String((e && e.message) || e) };
8590
+ }
8591
+ if (sid) {
8592
+ try { engine.deleteSession(sid); } catch {}
8593
+ sbSessions.delete(folder);
8594
+ sbSaveMap();
8595
+ }
8596
+ return { ok: true };
8597
+ });
8598
+
8599
+ ipcMain.handle('sandbox:reveal', async (_e, payload) => {
8600
+ const folder = String((payload && payload.folder) || '');
8601
+ if (!folder.startsWith(sandboxRoot() + path.sep)) return { ok: false, error: 'geçersiz klasör' };
8602
+ try { require('electron').shell.openPath(folder); return { ok: true }; } catch (e) {
8603
+ return { ok: false, error: String((e && e.message) || e) };
8604
+ }
8605
+ });
8606
+
8607
+ /* seçili reponun oturum geçmişini panele döndürür (yeni oturum yoksa oluşturur) */
8608
+ ipcMain.handle('sandbox:open', async (_e, payload) => {
8609
+ const folder = String((payload && payload.folder) || '');
8610
+ if (!folder.startsWith(sandboxRoot() + path.sep)) return { ok: false, error: 'geçersiz klasör' };
8611
+ if (!engine) return { ok: false, error: 'ajan hazır değil' };
8612
+ const s = sandboxGetSession(folder);
8613
+ s.workspace = folder;
8614
+ engine.cache.set(s.id, s);
8615
+ const msgs = [];
8616
+ for (const m of s.messages || []) {
8617
+ if (m.tool_calls) continue;
8618
+ const txt = bcMsgText(m);
8619
+ if (!txt) continue;
8620
+ msgs.push({ role: m.role === 'assistant' ? 'assistant' : 'user', text: txt.slice(0, 4000) });
8621
+ }
8622
+ return {
8623
+ ok: true,
8624
+ sessionId: s.id,
8625
+ busy: !!engine.isBusy(s.id),
8626
+ messages: msgs.slice(-200),
8627
+ };
8628
+ });
8629
+
8630
+ /* Sandbox dosya konsolu: seçili repo klasörünün ağacı (filePanel yeniden kullanılır) */
8631
+ ipcMain.handle('sandbox:tree', (_e, payload) => {
8632
+ const folder = String((payload && payload.folder) || '');
8633
+ if (!folder.startsWith(sandboxRoot() + path.sep)) return { ok: false, error: 'geçersiz klasör' };
8634
+ const p = path.resolve(folder, String((payload && payload.rel) || ''));
8635
+ if (p !== folder && !p.startsWith(folder + path.sep)) return { ok: false, error: 'geçersiz yol' };
8636
+ try {
8637
+ const entries = fs.readdirSync(p, { withFileTypes: true })
8638
+ .filter((e) => e.name !== 'node_modules' && e.name !== '.git')
8639
+ .map((e) => {
8640
+ let size = 0;
8641
+ if (!e.isDirectory()) {
8642
+ try { size = fs.statSync(path.join(p, e.name)).size; } catch {}
8643
+ }
8644
+ return { name: e.name, dir: e.isDirectory(), size };
8645
+ })
8646
+ .sort((a, b) => (a.dir === b.dir ? a.name.localeCompare(b.name) : a.dir ? -1 : 1));
8647
+ return { ok: true, workspace: folder, entries };
8648
+ } catch (e) {
8649
+ return { ok: false, error: String((e && e.message) || e) };
8650
+ }
8651
+ });
8652
+
8653
+ /* Sandbox ÇALIŞTIRICI: repo klasöründe uzun süreli komut (npm start/dev vb.)
8654
+ YÖNETİLEN SÜREÇ — çıktı satır satır panele akar; ■ ile ağaç-kullanı durdurulur.
8655
+ Dev server adresi çıktıdan yakalanır → 'Tarayıcıda aç' aktifleşir.
8656
+ Aynı fonksiyon engine.sbRunHook olarak da bağlanır — ajanın panel_run
8657
+ aracı turu KİLİTLEMEDEN süreç başlatır (Kur/Başlat butonları bunu kullanır). */
8658
+ const sbRunProc = { proc: null, folder: '', url: '' };
8659
+
8660
+ function sbRunEmit(data) {
8661
+ try {
8662
+ if (win && !win.isDestroyed()) win.webContents.send('agent:event', { type: 'sb-run', data: String(data || '') });
8663
+ } catch {}
8664
+ }
8665
+
8666
+ function sbRunStartManaged(folder, cmd) {
8667
+ folder = String(folder || '');
8668
+ cmd = String(cmd || '').trim();
8669
+ if (!folder.startsWith(sandboxRoot() + path.sep)) return { ok: false, error: 'geçersiz klasör' };
8670
+ if (!cmd) return { ok: false, error: 'komut boş' };
8671
+ if (sbRunProc.proc) return { ok: false, error: 'ÇALIŞTIR panelinde zaten bir süreç çalışıyor — önce ■ ile durdur' };
8672
+ const { spawn } = require('child_process');
8673
+ try {
8674
+ const proc = spawn('cmd.exe', ['/d', '/s', '/c', cmd], {
8675
+ cwd: folder,
8676
+ windowsHide: true,
8677
+ env: { ...process.env, FORCE_COLOR: '0', NO_COLOR: '1' },
8678
+ });
8679
+ sbRunProc.proc = proc;
8680
+ sbRunProc.folder = folder;
8681
+ sbRunProc.url = '';
8682
+ const onLine = (buf) => {
8683
+ for (const piece of String(buf).split(/\r?\n/)) {
8684
+ if (!piece.trim()) continue;
8685
+ sbRunEmit(piece);
8686
+ /* dev server adresi yakala (sadece localhost) */
8687
+ const m = /https?:\/\/(?:localhost|127\.0\.0\.1)(?::\d{2,5})?[^\s'"<>]*/i.exec(piece);
8688
+ if (m && !sbRunProc.url) {
8689
+ sbRunProc.url = m[0].replace(/[)\].,;:'"]+$/, '');
8690
+ try {
8691
+ if (win && !win.isDestroyed()) win.webContents.send('agent:event', { type: 'sb-run-url', url: sbRunProc.url });
8692
+ } catch {}
8693
+ }
8694
+ }
8695
+ };
8696
+ proc.stdout.on('data', onLine);
8697
+ proc.stderr.on('data', onLine);
8698
+ proc.on('close', (code) => {
8699
+ sbRunEmit('[süreç bitti' + (code != null ? ' — kod ' + code : '') + ']');
8700
+ sbRunProc.proc = null;
8701
+ try {
8702
+ if (win && !win.isDestroyed()) win.webContents.send('agent:event', { type: 'sb-run-end' });
8703
+ } catch {}
8704
+ });
8705
+ sbRunEmit('▶ ' + cmd + ' (klasör: ' + path.basename(folder) + ')');
8706
+ return {
8707
+ ok: true,
8708
+ note: 'süreç ÇALIŞTIR panelinde başlatıldı — çıktı orada canlı akar; durdurmak için paneldeki ■. Kullanıcıya adres varsa bildir.',
8709
+ };
8710
+ } catch (e) {
8711
+ sbRunProc.proc = null;
8712
+ return { ok: false, error: String((e && e.message) || e) };
8713
+ }
8714
+ }
8715
+
8716
+ ipcMain.handle('sandbox:run', async (_e, payload) => {
8717
+ const folder = String((payload && payload.folder) || '');
8718
+ const cmd = String((payload && payload.cmd) || '').trim();
8719
+ return sbRunStartManaged(folder, cmd);
8720
+ });
8721
+
8722
+ ipcMain.handle('sandbox:runstop', async () => {
8723
+ const p = sbRunProc.proc;
8724
+ if (!p) return { ok: true, wasRunning: false };
8725
+ sbRunProc.proc = null;
8726
+ try {
8727
+ /* cmd.exe /c zinciriyle çocuklar (node/vite) geride kalmasın — ağaç kesimi */
8728
+ const { exec } = require('child_process');
8729
+ if (process.platform === 'win32') exec('taskkill /pid ' + p.pid + ' /T /F', { windowsHide: true });
8730
+ else p.kill('SIGTERM');
8731
+ } catch {}
8732
+ return { ok: true, wasRunning: true };
8733
+ });
8734
+
8735
+ ipcMain.handle('sandbox:openurl', async (_e, url) => {
8736
+ const u = String(url || '').trim();
8737
+ if (!/^https?:\/\/(?:localhost|127\.0\.0\.1)(?::\d{2,5})?(?:\/|$)/i.test(u)) return { ok: false, error: 'yalnız localhost adresleri açılabilir' };
8738
+ try {
8739
+ setBrowserOpen(true, true);
8740
+ browser.view.webContents.loadURL(u).catch(() => {});
8741
+ return { ok: true, url: u };
8742
+ } catch (e) {
8743
+ return { ok: false, error: String((e && e.message) || e) };
8744
+ }
8745
+ });
8746
+
8229
8747
  /* Sağ tık menüsü: HTML dosyasını dahili tarayıcıda GÖRÜNÜR aç */
8230
8748
  /* ---------- BC DAHİLİ STATİK SUNUCU ----------
8231
8749
  Beast Code çıktısı ASLA file:// ile açılmaz: ES modülleri, fetch, ServiceWorker
@@ -8236,6 +8754,23 @@ const bcStatic = { server: null, root: '', base: '' };
8236
8754
  /* MOBİL PROJE önizleme: npm run web (expo start --web) süreci + canlı adres */
8237
8755
  const bcMobile = { proc: null, url: '', root: '' };
8238
8756
 
8757
+ /* ---------- EXPO GO QR (yalnız Beast Code) ----------
8758
+ Ajan expo start çıktısında exp://<LAN-IP>:<port> yazınca QR'e çevrilir ve
8759
+ renderer'a { type:'bc-exp-qr', qr, url } düşer — telefonda Expo Go okutulur. */
8760
+ let bcLastExpUrl = '';
8761
+ let bcLastExpQr = '';
8762
+
8763
+ function bcExpQrMake(url) {
8764
+ if (!url || url === bcLastExpUrl) return Promise.resolve();
8765
+ bcLastExpUrl = url;
8766
+ return QRCode.toDataURL(url, { width: 240, margin: 1, errorCorrectionLevel: 'M' }).then((qr) => {
8767
+ bcLastExpQr = qr;
8768
+ if (win && !win.isDestroyed()) win.webContents.send('agent:event', { type: 'bc-exp-qr', qr, url });
8769
+ });
8770
+ }
8771
+
8772
+ ipcMain.handle('ide:expqr', () => ({ ok: true, qr: bcLastExpQr, url: bcLastExpUrl }));
8773
+
8239
8774
  /* workspace bir MOBİL UYGULAMA projesi mi? (expo / react-native / app.json) */
8240
8775
  function ideMobileProject(root) {
8241
8776
  try {
@@ -8260,6 +8795,49 @@ function ideMobileProject(root) {
8260
8795
  }
8261
8796
  }
8262
8797
 
8798
+ /* Beast Code PROJE TİPİ algılama: preview butonu ajana hangi talimatı vereceğini
8799
+ bununla seçer. kind: expo | react-native | web | static | unknown */
8800
+ function ideProjectInfo(root) {
8801
+ const out = { kind: 'unknown', name: '', cmd: '' };
8802
+ try {
8803
+ const pkgPath = path.join(root, 'package.json');
8804
+ let pkg = null;
8805
+ try { if (fs.existsSync(pkgPath)) pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); } catch {}
8806
+ if (!pkg) {
8807
+ out.kind = fs.existsSync(path.join(root, 'index.html')) ? 'static' : 'unknown';
8808
+ return out;
8809
+ }
8810
+ out.name = String(pkg.name || '');
8811
+ const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
8812
+ const scripts = pkg.scripts || {};
8813
+ const hasAppJson = fs.existsSync(path.join(root, 'app.json')) || fs.existsSync(path.join(root, 'app.config.js'));
8814
+ if (deps.expo || (hasAppJson && (deps['react-native'] || deps['react-dom']))) {
8815
+ out.kind = 'expo';
8816
+ out.cmd = scripts.web ? 'npm run web' : deps.expo ? 'npx expo start --web' : String(scripts.start || '');
8817
+ } else if (deps['react-native']) {
8818
+ out.kind = 'react-native';
8819
+ out.cmd = String(scripts.start || scripts.ios || '') || 'npx react-native start';
8820
+ } else if (deps.next) {
8821
+ out.kind = 'web';
8822
+ out.cmd = String(scripts.dev || scripts.start || '') || 'npx next dev';
8823
+ } else if (deps.vite || deps['react-scripts'] || deps['@angular/core'] || deps['vue']) {
8824
+ out.kind = 'web';
8825
+ out.cmd = String(scripts.dev || scripts.start || scripts.serve || '');
8826
+ } else if (scripts.dev) {
8827
+ out.kind = 'web';
8828
+ out.cmd = 'npm run dev';
8829
+ } else if (scripts.start) {
8830
+ out.kind = 'web';
8831
+ out.cmd = 'npm start';
8832
+ } else {
8833
+ out.kind = fs.existsSync(path.join(root, 'index.html')) ? 'static' : 'unknown';
8834
+ }
8835
+ } catch {}
8836
+ return out;
8837
+ }
8838
+
8839
+ ipcMain.handle('ide:projinfo', () => ({ ok: true, ...ideProjectInfo(ideRoot()) }));
8840
+
8263
8841
  function killMobilePreview() {
8264
8842
  const p = bcMobile.proc;
8265
8843
  if (p) {
@@ -8510,6 +9088,7 @@ ipcMain.handle('custom:set', (_e, list) => {
8510
9088
  settings.customProviders = Array.isArray(list) ? list : [];
8511
9089
  saveSettings();
8512
9090
  engine.setCustomProviders(settings.customProviders);
9091
+ headroom.syncProviders(); /* headroom açıksa proxy setini yeni listeye uydur */
8513
9092
  return engine.publicState();
8514
9093
  });
8515
9094
 
@@ -8641,8 +9220,8 @@ async function fetchZenFreeModels() {
8641
9220
  if (free.length) return free;
8642
9221
  }
8643
9222
  } catch {}
8644
- /* API cevap vermezse bilinen free liste */
8645
- return ['big-pickle', 'deepseek-v4-flash-free', 'mimo-v2.5-free', 'hy3-free', 'nemotron-3-ultra-free', 'nemotron-3.5-lightning-free', 'laguna-s-2.1-free'];
9223
+ /* API cevap vermezse bilinen free liste (09/2026 güncel — eski adlar düşürüldü) */
9224
+ return ['big-pickle', 'deepseek-v4-flash-free', 'mimo-v2.5-free', 'muse-spark-1.3-contributor-free', 'muse-spark-1.2-contributor-free', 'ling-3.0-flash-fin-free', 'nemotron-3-ultra-free', 'nemotron-3.5-lightning-free'];
8646
9225
  }
8647
9226
 
8648
9227
  function upsertZenEnv(token) {