beast-agent 2.10.0 → 2.12.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
@@ -75,6 +75,73 @@ function emitUpdateEvent() {
75
75
  } catch {}
76
76
  }
77
77
 
78
+ /* /version ORTAK METNİ — tüm kanallar (desktop/WA/TG/DC): güncel sürüm +
79
+ npm'deki en son sürüm (yeni varsa vurgulu; 10dk cache'li getNpmLatest). */
80
+ async function versionText(waStyle) {
81
+ const cur = beastVersion();
82
+ const latest = await getNpmLatest();
83
+ const b = waStyle ? '*' : '**';
84
+ if (latest && isNewerVersion(latest, cur)) {
85
+ return (
86
+ `${b}Beast Agent v${cur}${b} — ${b}yeni sürüm var: v${latest}${b}\n` +
87
+ `Güncelleme: ${b}/update${b} (kontrol) · ${b}/update now${b} (hemen kur)`
88
+ );
89
+ }
90
+ if (latest) {
91
+ return `${b}Beast Agent v${cur}${b} — güncel ✓\nGüncelleme kontrolü: ${b}/update${b} · hemen kur: ${b}/update now${b}`;
92
+ }
93
+ return `${b}Beast Agent v${cur}${b}\nGüncelleme: ${b}/update${b} (yeni sürüm kontrolü) · ${b}/update now${b} (hemen kur)`;
94
+ }
95
+
96
+ /* SÜRÜM GÜNCELLEMESİ BİLGİSİ: sürüm değiştiyse İLK AÇILIŞTA kullanıcı'nın
97
+ olduğu yere kısa bilgi düşer — en güncel oturum (desktop) + o oturuma
98
+ BĞLI kanal (WA/TG/DC; hangisindeyse oraya). settings.lastRunVersion
99
+ yazıldığı için sürüm başına YALNIZ BİR KEZ çalışır. */
100
+ function notifyVersionUpdate() {
101
+ const cur = beastVersion();
102
+ const prev = String(settings.lastRunVersion || '');
103
+ settings.lastRunVersion = cur;
104
+ saveSettings();
105
+ if (!prev || prev === cur) return; /* ilk kurulum ya da sürüm değişmedi */
106
+ const waStyleTxt =
107
+ `🔄 *Beast Agent güncellendi:* v${prev} → *v${cur}*\n` +
108
+ `Yenilikler: GitHub Releases · Sürüm: /version · Güncelleme: /update`;
109
+ let sid = '';
110
+ try { sid = reuseOrLatestSession(''); } catch {}
111
+ /* desktop: en güncel oturuma bilgi balonu */
112
+ try {
113
+ if (win && !win.isDestroyed() && sid) {
114
+ const mdTxt = waStyleTxt.replace(/\*/g, '**');
115
+ win.webContents.send('agent:event', { sessionId: sid, type: 'message', message: { role: 'assistant', content: mdTxt } });
116
+ win.webContents.send('agent:event', { sessionId: sid, type: 'done', usage: null });
117
+ }
118
+ } catch {}
119
+ /* kanallar: SADECE bu oturuma bağlı kanal (kullanıcı oradaydı) */
120
+ (async () => {
121
+ try {
122
+ if (wa && wa.connected) {
123
+ const own = waOwnerNum();
124
+ const jid = own ? own + '@s.whatsapp.net' : '';
125
+ if (jid && [...waChats.entries()].some(([j, s]) => j === jid && String(s) === String(sid))) {
126
+ await sendWaSafe(jid, waStyleTxt);
127
+ }
128
+ }
129
+ } catch {}
130
+ try {
131
+ if (tg && tg.connected) {
132
+ const hit = [...tgChats.entries()].find(([, s]) => String(s) === String(sid));
133
+ if (hit) await sendTgSafe(hit[0], waStyleTxt);
134
+ }
135
+ } catch {}
136
+ try {
137
+ if (dc && dc.connected) {
138
+ const hit = [...dcChats.entries()].find(([, s]) => String(s) === String(sid));
139
+ if (hit) await sendDcSafe(hit[0], waStyleTxt);
140
+ }
141
+ } catch {}
142
+ })();
143
+ }
144
+
78
145
  function replyUpdate(text) {
79
146
  try {
80
147
  for (const sid of updateReplies.sids) desktopEcho(sid, '/update', text);
@@ -1623,7 +1690,8 @@ async function tryWaSlash(jid, rawText, senderNum, payload0) {
1623
1690
  out = 'Bu komut yalnız BeastCode modunda çalışır — önce /beastcode yaz.';
1624
1691
  }
1625
1692
  } else if (cmd === 'version') {
1626
- out = `*Beast Agent v${beastVersion()}*\nGüncelleme için: /update (kurulum hazır olunca /update now)`;
1693
+ /* npm'deki en son sürümü de göster */
1694
+ out = await versionText(true);
1627
1695
  } else {
1628
1696
  out = `Bilinmeyen komut: /${cmd}\nListe için /help yaz.`;
1629
1697
  }
@@ -2643,6 +2711,14 @@ async function processTgMessage(chatId, payload, requeues = 0) {
2643
2711
  tgLog(`skip flush: izinli eşleşme yok (sender=${payload.senderId || '?'})`);
2644
2712
  return;
2645
2713
  }
2714
+ /* /version HER KANALDA: sürüm + npm'deki en son sürüm (izin kontrolü üstte) */
2715
+ const tgRaw = String((payload && payload.text) || '').trim();
2716
+ if (tgRaw === '/version' || tgRaw.startsWith('/version ')) {
2717
+ versionText(true)
2718
+ .then((txt) => sendTgSafe(chatId, txt))
2719
+ .catch(() => {});
2720
+ return;
2721
+ }
2646
2722
  /* OPENCODE STEER: meşgulse bekleme — engine.send tampona ekler, koşan tur
2647
2723
  sonraki istekte görür (WA ile aynı mantık) */
2648
2724
  let sid = tgChats.get(chatId) || tgSingleSid;
@@ -2854,6 +2930,14 @@ async function processDcMessage(channelId, payload) {
2854
2930
  dcLog(`skip flush: izinli eşleşme yok (sender=${payload.senderId || '?'})`);
2855
2931
  return;
2856
2932
  }
2933
+ /* /version HER KANALDA: sürüm + npm'deki en son sürüm (izin kontrolü üstte) */
2934
+ const dcRaw = String((payload && payload.text) || '').trim();
2935
+ if (dcRaw === '/version' || dcRaw.startsWith('/version ')) {
2936
+ versionText(true)
2937
+ .then((txt) => sendDcSafe(channelId, txt))
2938
+ .catch(() => {});
2939
+ return;
2940
+ }
2857
2941
  /* OPENCODE STEER: meşgulse bekleme — engine.send tampona ekler, koşan tur
2858
2942
  sonraki istekte görür (WA/TG ile aynı mantık) */
2859
2943
  let sid = dcChats.get(channelId) || dcSingleSid;
@@ -3019,6 +3103,7 @@ function reloadBackend() {
3019
3103
  flushDesktopOnDone(ev); /* biriken desktop mesajlarını sıraya bas */
3020
3104
  bcFlushOnDone(ev); /* Beast Code kuyruğunu iş bitiminde boşalt */
3021
3105
  stFlushOnDone(ev); /* Beast Studio kuyruğunu iş bitiminde boşalt */
3106
+ sbFlushOnDone(ev); /* Sandbox kuyruğunu iş bitiminde boşalt */
3022
3107
  /* BC canlı önizleme: ajan bir dev server başlattıysa adresi yakala —
3023
3108
  preview butonu ve otomatik açılış DAİMA bu sunucuyu öncelikli kullanır */
3024
3109
  if (ev.type === 'bc-preview' && /^https?:\/\/(?:localhost|127\.0\.0\.1)(?::\d{2,5})?/i.test(String(ev.url || ''))) {
@@ -3200,6 +3285,9 @@ function reloadBackend() {
3200
3285
  }
3201
3286
  },
3202
3287
  });
3288
+ /* PANEL RUN köprüsü: ajanın panel_run aracı → ÇALIŞTIR panelindeki yönetilen
3289
+ süreç koşucusu (sandbox:run IPC ile aynı makine). */
3290
+ engine.sbRunHook = sbRunStartManaged;
3203
3291
  return engine.publicState();
3204
3292
  }
3205
3293
 
@@ -3413,6 +3501,12 @@ app.whenReady().then(() => {
3413
3501
  ensureDc().start().catch((e) => dcLog('autostart failed: ' + String((e && e.message) || e)));
3414
3502
  }
3415
3503
 
3504
+ /* SÜRÜM GÜNCELLENDİYSE ilk açılışta bilgi: kanalların bağlanmasına 5 sn tanı,
3505
+ sonra kullanıcının olduğu yere (en güncel oturum + bağlı kanal) düşer */
3506
+ setTimeout(() => {
3507
+ try { notifyVersionUpdate(); } catch {}
3508
+ }, 5000);
3509
+
3416
3510
  app.on('activate', () => {
3417
3511
  if (BrowserWindow.getAllWindows().length === 0) createWindow();
3418
3512
  });
@@ -4808,7 +4902,10 @@ ipcMain.handle('agent:send', (_e, { sessionId, text }) => {
4808
4902
  return true;
4809
4903
  }
4810
4904
  if (t === '/version') {
4811
- desktopEcho(sessionId, t, `**Beast Agent v${beastVersion()}**\nGüncelleme: **/update** (yeni sürüm kontrolü) · **/update now** (hemen kur)`);
4905
+ /* npm registry'den en son sürümü de göster (10dk cache'li) */
4906
+ versionText(false)
4907
+ .then((txt) => desktopEcho(sessionId, t, txt))
4908
+ .catch(() => desktopEcho(sessionId, t, `**Beast Agent v${beastVersion()}**`));
4812
4909
  return true;
4813
4910
  }
4814
4911
  if (t === '/help') {
@@ -7366,9 +7463,10 @@ ipcMain.handle('beastcode:send', async (_e, payload) => {
7366
7463
  return { ok: true, sessionId: s.id, mode: s.bcMode };
7367
7464
  }
7368
7465
  if (busy) {
7369
- /* ajan çalışıyor → kuyruğa al; iş bitince (done/error) toplu gider */
7370
- const q = bcQueuePush(ws, text, attachments);
7371
- return { ok: true, queued: true, count: q.msgs.length, sessionId: s.id };
7466
+ /* OPENCODE STEER: koşan tur varken mesaj kuyrukta BEKLEMEZ — engine.send
7467
+ konuşmaya ekler, koşan tur sonraki istekte görür (ana chat ile aynı) */
7468
+ engine.send(s.id, attachments.length ? { text, attachments } : text, { userAction: true });
7469
+ return { ok: true, sessionId: s.id, steered: true };
7372
7470
  }
7373
7471
  /* boşta: kısa pencere — hızlı ard arda mesajlar tek işte birleşir */
7374
7472
  const q = bcQueuePush(ws, text, attachments);
@@ -7652,8 +7750,9 @@ ipcMain.handle('studio:send', async (_e, payload) => {
7652
7750
  return { ok: true, sessionId: s.id, mode: s.bcMode };
7653
7751
  }
7654
7752
  if (busy) {
7655
- const q = stQueuePush(ws, text, attachments);
7656
- return { ok: true, queued: true, count: q.msgs.length, sessionId: s.id };
7753
+ /* OPENCODE STEER: koşan tur varken mesaj kuyrukta beklemez — konuşmaya eklenir */
7754
+ engine.send(s.id, attachments.length ? { text, attachments } : text, { userAction: true });
7755
+ return { ok: true, sessionId: s.id, steered: true };
7657
7756
  }
7658
7757
  const q = stQueuePush(ws, text, attachments);
7659
7758
  clearTimeout(q.timer);
@@ -8133,6 +8232,454 @@ ipcMain.handle('studio:delete', async (_e, rel) => {
8133
8232
  }
8134
8233
  });
8135
8234
 
8235
+ /* Sağ tık menüsü: HTML dosyasını dahili tarayıcıda GÖRÜNÜR aç */
8236
+ /* ---------- BEAST SANDBOX: GitHub repolarını indir & içinde çalış ----------
8237
+ Her repo KENDİ klasöründe (~/Beast-Sandbox/<ad>) ve KENDİ gizli oturumunda
8238
+ koşar (klasör bazlı, bcCode disiplinli — Beast Code motorunun aynısı; ajanın
8239
+ run_command/read/write araçları repo klasöründe çalışır). Ana sohbet VE
8240
+ Code/Studio panellerinden tamamen ayrıdır; ana chat solda yerinde kalır. */
8241
+ const sbSessions = new Map(); /* klasör → sessionId */
8242
+ const SB_DEBOUNCE_MS = 900;
8243
+ const sbQueue = new Map(); /* klasör → { timer, msgs[] } */
8244
+
8245
+ function sandboxRoot() {
8246
+ const dir = path.join(app.getPath('home'), 'Beast-Sandbox');
8247
+ try { fs.mkdirSync(dir, { recursive: true }); } catch {}
8248
+ return dir;
8249
+ }
8250
+
8251
+ function sbSessionFile() {
8252
+ return path.join(beastDir(), 'sandbox-sessions.json');
8253
+ }
8254
+
8255
+ let sbMapLoaded = false;
8256
+ function sbLoadMap() {
8257
+ if (sbMapLoaded) return;
8258
+ sbMapLoaded = true;
8259
+ try {
8260
+ const raw = JSON.parse(fs.readFileSync(sbSessionFile(), 'utf8'));
8261
+ for (const [k, v] of Object.entries(raw || {})) {
8262
+ if (k && typeof v === 'string') sbSessions.set(k, v);
8263
+ }
8264
+ } catch {}
8265
+ }
8266
+
8267
+ function sbSaveMap() {
8268
+ try {
8269
+ fs.writeFileSync(sbSessionFile(), JSON.stringify(Object.fromEntries(sbSessions), null, 2));
8270
+ } catch {}
8271
+ }
8272
+
8273
+ function sandboxGetSession(folder) {
8274
+ sbLoadMap();
8275
+ let sid = sbSessions.get(folder);
8276
+ if (sid) {
8277
+ try {
8278
+ const s = engine.cache.get(sid) || engine._load(sid);
8279
+ if (s) return s;
8280
+ } catch {}
8281
+ sbSessions.delete(folder);
8282
+ }
8283
+ const s = engine._load(engine.createSession().id);
8284
+ s.messages = s.messages || [];
8285
+ s.bgTitle = 'Beast Sandbox'; /* _view.isBg → sohbet geçmişinde gizli */
8286
+ s.bcCode = true; /* todo disiplini + hızlı iş kapanışı (engine) */
8287
+ s.sbSandbox = true; /* engine: SANDBOX sistem bloğu — ajan açık kaynak repoda olduğunu bilir */
8288
+ try {
8289
+ fs.appendFileSync(
8290
+ engine._file(s.id),
8291
+ JSON.stringify({ t: 'meta2', bgOf: '', title: 'Beast Sandbox', at: new Date().toISOString() }) + '\n'
8292
+ );
8293
+ } catch {}
8294
+ bcMarkWs(s, folder); /* klasör bağı oturum dosyasına yazılır */
8295
+ engine.cache.set(s.id, s);
8296
+ sbSessions.set(folder, s.id);
8297
+ sbSaveMap();
8298
+ return s;
8299
+ }
8300
+
8301
+ function sbQueuePush(ws, text, attachments) {
8302
+ let q = sbQueue.get(ws);
8303
+ if (!q) {
8304
+ q = { timer: null, msgs: [] };
8305
+ sbQueue.set(ws, q);
8306
+ }
8307
+ q.msgs.push({
8308
+ text,
8309
+ attachments: Array.isArray(attachments) && attachments.length ? attachments : undefined,
8310
+ });
8311
+ return q;
8312
+ }
8313
+
8314
+ function sbFlush(folder) {
8315
+ const q = sbQueue.get(folder);
8316
+ if (!q || !q.msgs.length) return;
8317
+ const s = sandboxGetSession(folder);
8318
+ if (engine.isBusy(s.id)) return; /* steer sistemine rağmen kuyruğu koru */
8319
+ let merged = '';
8320
+ let mergedAtts = null;
8321
+ for (const m of q.msgs) {
8322
+ if (m.text) merged += (merged ? '\n' : '') + m.text;
8323
+ if (!mergedAtts && Array.isArray(m.attachments) && m.attachments.length) mergedAtts = m.attachments;
8324
+ }
8325
+ if (!merged.trim() && !mergedAtts) {
8326
+ sbQueue.delete(folder);
8327
+ clearTimeout(q.timer);
8328
+ return;
8329
+ }
8330
+ s.workspace = folder;
8331
+ s.bcCode = true;
8332
+ s.sbSandbox = true;
8333
+ engine.cache.set(s.id, s);
8334
+ const payload = mergedAtts ? { text: merged, attachments: mergedAtts } : merged;
8335
+ engine.send(s.id, payload, { userAction: true });
8336
+ sbQueue.delete(folder);
8337
+ clearTimeout(q.timer);
8338
+ }
8339
+
8340
+ function sbFlushOnDone(ev) {
8341
+ if (!ev || (ev.type !== 'done' && ev.type !== 'error') || !ev.sessionId) return;
8342
+ for (const [folder, sid] of sbSessions) {
8343
+ if (String(sid) === String(ev.sessionId) && sbQueue.has(folder)) {
8344
+ setTimeout(() => { try { sbFlush(folder); } catch {} }, 150);
8345
+ }
8346
+ }
8347
+ }
8348
+
8349
+ /* repo adresten (url | owner/repo) normalize et */
8350
+ function sbRepoFromInput(input) {
8351
+ let s = String(input || '').trim().replace(/\.git$/, '');
8352
+ if (!s) return null;
8353
+ const m = /^(?:https?:\/\/)?(?:www\.)?github\.com\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)/i.exec(s);
8354
+ if (m) return { url: 'https://github.com/' + m[1] + '/' + m[2] + '.git', name: m[2] };
8355
+ if (/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(s)) {
8356
+ const parts = s.split('/');
8357
+ return { url: 'https://github.com/' + parts[0] + '/' + parts[1] + '.git', name: parts[1] };
8358
+ }
8359
+ return null;
8360
+ }
8361
+
8362
+ ipcMain.handle('sandbox:list', async () => {
8363
+ const root = sandboxRoot();
8364
+ const items = [];
8365
+ try {
8366
+ sbLoadMap();
8367
+ for (const e of fs.readdirSync(root, { withFileTypes: true })) {
8368
+ if (!e.isDirectory() || e.name.startsWith('.')) continue;
8369
+ const folder = path.join(root, e.name);
8370
+ let sid = '';
8371
+ let busy = false;
8372
+ const known = sbSessions.get(folder);
8373
+ if (known) {
8374
+ sid = String(known);
8375
+ try { busy = engine.isBusy(known); } catch {}
8376
+ }
8377
+ items.push({ name: e.name, folder, sid, busy });
8378
+ }
8379
+ } catch {}
8380
+ return { ok: true, root, items };
8381
+ });
8382
+
8383
+ ipcMain.handle('sandbox:clone', async (_e, input) => {
8384
+ const repo = sbRepoFromInput(input);
8385
+ if (!repo) return { ok: false, error: 'github.com/owner/repo, owner/repo ya da tam .git adresi gir' };
8386
+ const root = sandboxRoot();
8387
+ const base = String(repo.name || 'repo').replace(/[\\/:*?"<>|]/g, '_').slice(0, 60) || 'repo';
8388
+ let dest = path.join(root, base);
8389
+ let n = 2;
8390
+ while (fs.existsSync(dest)) dest = path.join(root, base + '-' + n++);
8391
+ const finalName = path.basename(dest);
8392
+ return new Promise((resolve) => {
8393
+ const { spawn } = require('child_process');
8394
+ let settled = false;
8395
+ let lastPct = -1;
8396
+ let lastEmit = 0;
8397
+ let errText = '';
8398
+ /* git ilerlemeyi stderr'a \r ile AYNI satıra yazar (Receiving objects: %34 …)
8399
+ — her karede yüzde yakalanır, panele canlı akar */
8400
+ const onChunk = (buf) => {
8401
+ const txt = String(buf);
8402
+ errText += txt;
8403
+ const frames = txt.split(/\r/);
8404
+ const cur = (frames[frames.length - 1] || '').trim();
8405
+ const m = /(\d{1,3})%/.exec(cur);
8406
+ const pct = m ? Math.min(99, Number(m[1])) : null;
8407
+ const now = Date.now();
8408
+ if (cur && (pct !== null ? pct !== lastPct : now - lastEmit > 400)) {
8409
+ lastPct = pct != null ? pct : lastPct;
8410
+ lastEmit = now;
8411
+ try {
8412
+ if (win && !win.isDestroyed()) {
8413
+ win.webContents.send('agent:event', { type: 'sb-clone', pct, text: cur.slice(0, 90) });
8414
+ }
8415
+ } catch {}
8416
+ }
8417
+ };
8418
+ let proc;
8419
+ try {
8420
+ proc = spawn('git', ['clone', '--progress', repo.url, dest], { windowsHide: true });
8421
+ } catch (e) {
8422
+ return resolve({ ok: false, error: 'git başlatılamadı: ' + String((e && e.message) || e) });
8423
+ }
8424
+ proc.stdout.on('data', onChunk);
8425
+ proc.stderr.on('data', onChunk);
8426
+ const done = (err) => {
8427
+ if (settled) return;
8428
+ settled = true;
8429
+ try {
8430
+ if (win && !win.isDestroyed()) win.webContents.send('agent:event', { type: 'sb-clone', pct: null, done: !err });
8431
+ } catch {}
8432
+ if (err) {
8433
+ try { fs.rmSync(dest, { recursive: true, force: true }); } catch {}
8434
+ const estr = errText || String((err && err.message) || err);
8435
+ const msg = err && err.code === 'ENOENT'
8436
+ ? 'git kurulu değil — https://git-scm.com adresinden kur'
8437
+ : /not found|does not exist|Could not/i.test(estr)
8438
+ ? 'repo bulunamadı ya da erişilemedi (gizli repo mu?)'
8439
+ : /Authentication|403|Permission/i.test(estr)
8440
+ ? 'erişim reddedildi (gizli repo ya da ağ sorunu)'
8441
+ : (estr.split('\n').filter((l) => l.trim()).pop() || 'klonlama başarısız').slice(0, 200);
8442
+ resolve({ ok: false, error: msg });
8443
+ return;
8444
+ }
8445
+ log.info('main', 'sandbox: klonlandı ' + repo.url + ' → ' + finalName);
8446
+ resolve({ ok: true, name: finalName, folder: dest });
8447
+ };
8448
+ proc.on('error', done);
8449
+ proc.on('close', (code) => done(code === 0 ? null : { code }));
8450
+ /* güvenlik tavanı: 10 dk */
8451
+ setTimeout(() => {
8452
+ if (!settled) {
8453
+ try { proc.kill(); } catch {}
8454
+ done(new Error('zaman aşımı'));
8455
+ }
8456
+ }, 600000);
8457
+ });
8458
+ });
8459
+
8460
+ ipcMain.handle('sandbox:send', async (_e, payload) => {
8461
+ const text = String((payload && payload.msg) || '').trim();
8462
+ const attachments = Array.isArray(payload && payload.attachments) ? payload.attachments : [];
8463
+ const folder = String((payload && payload.folder) || '');
8464
+ if (!folder) return { ok: false, error: 'repo seçilmedi' };
8465
+ if (!folder.startsWith(sandboxRoot() + path.sep)) return { ok: false, error: 'geçersiz klasör' };
8466
+ if (!text && !attachments.length) return { ok: false, error: 'boş mesaj' };
8467
+ if (!engine) return { ok: false, error: 'ajan hazır değil' };
8468
+ if (!engine.publicState().hasModel) return { ok: false, error: 'model yok — Ayarlar → Provider sekmesinden ekle' };
8469
+ const s = sandboxGetSession(folder);
8470
+ s.workspace = folder;
8471
+ s.bcCode = true;
8472
+ s.sbSandbox = true;
8473
+ engine.cache.set(s.id, s);
8474
+ const busy = engine.isBusy(s.id);
8475
+ const q = sbQueuePush(folder, text, attachments);
8476
+ if (busy) return { ok: true, queued: true, count: q.msgs.length, sessionId: s.id };
8477
+ clearTimeout(q.timer);
8478
+ q.timer = setTimeout(() => { try { sbFlush(folder); } catch {} }, SB_DEBOUNCE_MS);
8479
+ return { ok: true, sessionId: s.id, pending: true };
8480
+ });
8481
+
8482
+ ipcMain.handle('sandbox:stop', async (_e, payload) => {
8483
+ const folder = String((payload && payload.folder) || '');
8484
+ sbLoadMap();
8485
+ const sid = sbSessions.get(folder);
8486
+ const q = sbQueue.get(folder);
8487
+ if (q) {
8488
+ clearTimeout(q.timer);
8489
+ sbQueue.delete(folder);
8490
+ }
8491
+ const wasBusy = sid ? engine.isBusy(sid) : false;
8492
+ let r = false;
8493
+ if (wasBusy) {
8494
+ try { r = engine.interrupt(sid, 'kullanıcı Sandbox panelinden ■ ile durdurdu'); } catch {}
8495
+ }
8496
+ return { ok: true, wasBusy, interrupted: r };
8497
+ });
8498
+
8499
+ ipcMain.handle('sandbox:new', async (_e, payload) => {
8500
+ const folder = String((payload && payload.folder) || '');
8501
+ sbLoadMap();
8502
+ const sid = sbSessions.get(folder);
8503
+ if (sid && engine.isBusy(sid)) return { ok: false, error: 'mesaj sürüyor — önce ■ ile durdur' };
8504
+ const q = sbQueue.get(folder);
8505
+ if (q) {
8506
+ clearTimeout(q.timer);
8507
+ sbQueue.delete(folder);
8508
+ }
8509
+ if (sid) {
8510
+ try { engine.deleteSession(sid); } catch {}
8511
+ sbSessions.delete(folder);
8512
+ sbSaveMap();
8513
+ }
8514
+ return { ok: true };
8515
+ });
8516
+
8517
+ /* repo klasörünü diskten sil (oturum kaydıyla birlikte temizlenir) */
8518
+ ipcMain.handle('sandbox:remove', async (_e, payload) => {
8519
+ const folder = String((payload && payload.folder) || '');
8520
+ if (!folder.startsWith(sandboxRoot() + path.sep)) return { ok: false, error: 'geçersiz klasör' };
8521
+ sbLoadMap();
8522
+ const sid = sbSessions.get(folder);
8523
+ if (sid && engine.isBusy(sid)) return { ok: false, error: 'repo çalışıyor — önce ■ ile durdur' };
8524
+ try { fs.rmSync(folder, { recursive: true, force: true }); } catch (e) {
8525
+ return { ok: false, error: String((e && e.message) || e) };
8526
+ }
8527
+ if (sid) {
8528
+ try { engine.deleteSession(sid); } catch {}
8529
+ sbSessions.delete(folder);
8530
+ sbSaveMap();
8531
+ }
8532
+ return { ok: true };
8533
+ });
8534
+
8535
+ ipcMain.handle('sandbox:reveal', async (_e, payload) => {
8536
+ const folder = String((payload && payload.folder) || '');
8537
+ if (!folder.startsWith(sandboxRoot() + path.sep)) return { ok: false, error: 'geçersiz klasör' };
8538
+ try { require('electron').shell.openPath(folder); return { ok: true }; } catch (e) {
8539
+ return { ok: false, error: String((e && e.message) || e) };
8540
+ }
8541
+ });
8542
+
8543
+ /* seçili reponun oturum geçmişini panele döndürür (yeni oturum yoksa oluşturur) */
8544
+ ipcMain.handle('sandbox:open', async (_e, payload) => {
8545
+ const folder = String((payload && payload.folder) || '');
8546
+ if (!folder.startsWith(sandboxRoot() + path.sep)) return { ok: false, error: 'geçersiz klasör' };
8547
+ if (!engine) return { ok: false, error: 'ajan hazır değil' };
8548
+ const s = sandboxGetSession(folder);
8549
+ s.workspace = folder;
8550
+ engine.cache.set(s.id, s);
8551
+ const msgs = [];
8552
+ for (const m of s.messages || []) {
8553
+ if (m.tool_calls) continue;
8554
+ const txt = bcMsgText(m);
8555
+ if (!txt) continue;
8556
+ msgs.push({ role: m.role === 'assistant' ? 'assistant' : 'user', text: txt.slice(0, 4000) });
8557
+ }
8558
+ return {
8559
+ ok: true,
8560
+ sessionId: s.id,
8561
+ busy: !!engine.isBusy(s.id),
8562
+ messages: msgs.slice(-200),
8563
+ };
8564
+ });
8565
+
8566
+ /* Sandbox dosya konsolu: seçili repo klasörünün ağacı (filePanel yeniden kullanılır) */
8567
+ ipcMain.handle('sandbox:tree', (_e, payload) => {
8568
+ const folder = String((payload && payload.folder) || '');
8569
+ if (!folder.startsWith(sandboxRoot() + path.sep)) return { ok: false, error: 'geçersiz klasör' };
8570
+ const p = path.resolve(folder, String((payload && payload.rel) || ''));
8571
+ if (p !== folder && !p.startsWith(folder + path.sep)) return { ok: false, error: 'geçersiz yol' };
8572
+ try {
8573
+ const entries = fs.readdirSync(p, { withFileTypes: true })
8574
+ .filter((e) => e.name !== 'node_modules' && e.name !== '.git')
8575
+ .map((e) => {
8576
+ let size = 0;
8577
+ if (!e.isDirectory()) {
8578
+ try { size = fs.statSync(path.join(p, e.name)).size; } catch {}
8579
+ }
8580
+ return { name: e.name, dir: e.isDirectory(), size };
8581
+ })
8582
+ .sort((a, b) => (a.dir === b.dir ? a.name.localeCompare(b.name) : a.dir ? -1 : 1));
8583
+ return { ok: true, workspace: folder, entries };
8584
+ } catch (e) {
8585
+ return { ok: false, error: String((e && e.message) || e) };
8586
+ }
8587
+ });
8588
+
8589
+ /* Sandbox ÇALIŞTIRICI: repo klasöründe uzun süreli komut (npm start/dev vb.)
8590
+ YÖNETİLEN SÜREÇ — çıktı satır satır panele akar; ■ ile ağaç-kullanı durdurulur.
8591
+ Dev server adresi çıktıdan yakalanır → 'Tarayıcıda aç' aktifleşir.
8592
+ Aynı fonksiyon engine.sbRunHook olarak da bağlanır — ajanın panel_run
8593
+ aracı turu KİLİTLEMEDEN süreç başlatır (Kur/Başlat butonları bunu kullanır). */
8594
+ const sbRunProc = { proc: null, folder: '', url: '' };
8595
+
8596
+ function sbRunEmit(data) {
8597
+ try {
8598
+ if (win && !win.isDestroyed()) win.webContents.send('agent:event', { type: 'sb-run', data: String(data || '') });
8599
+ } catch {}
8600
+ }
8601
+
8602
+ function sbRunStartManaged(folder, cmd) {
8603
+ folder = String(folder || '');
8604
+ cmd = String(cmd || '').trim();
8605
+ if (!folder.startsWith(sandboxRoot() + path.sep)) return { ok: false, error: 'geçersiz klasör' };
8606
+ if (!cmd) return { ok: false, error: 'komut boş' };
8607
+ if (sbRunProc.proc) return { ok: false, error: 'ÇALIŞTIR panelinde zaten bir süreç çalışıyor — önce ■ ile durdur' };
8608
+ const { spawn } = require('child_process');
8609
+ try {
8610
+ const proc = spawn('cmd.exe', ['/d', '/s', '/c', cmd], {
8611
+ cwd: folder,
8612
+ windowsHide: true,
8613
+ env: { ...process.env, FORCE_COLOR: '0', NO_COLOR: '1' },
8614
+ });
8615
+ sbRunProc.proc = proc;
8616
+ sbRunProc.folder = folder;
8617
+ sbRunProc.url = '';
8618
+ const onLine = (buf) => {
8619
+ for (const piece of String(buf).split(/\r?\n/)) {
8620
+ if (!piece.trim()) continue;
8621
+ sbRunEmit(piece);
8622
+ /* dev server adresi yakala (sadece localhost) */
8623
+ const m = /https?:\/\/(?:localhost|127\.0\.0\.1)(?::\d{2,5})?[^\s'"<>]*/i.exec(piece);
8624
+ if (m && !sbRunProc.url) {
8625
+ sbRunProc.url = m[0].replace(/[)\].,;:'"]+$/, '');
8626
+ try {
8627
+ if (win && !win.isDestroyed()) win.webContents.send('agent:event', { type: 'sb-run-url', url: sbRunProc.url });
8628
+ } catch {}
8629
+ }
8630
+ }
8631
+ };
8632
+ proc.stdout.on('data', onLine);
8633
+ proc.stderr.on('data', onLine);
8634
+ proc.on('close', (code) => {
8635
+ sbRunEmit('[süreç bitti' + (code != null ? ' — kod ' + code : '') + ']');
8636
+ sbRunProc.proc = null;
8637
+ try {
8638
+ if (win && !win.isDestroyed()) win.webContents.send('agent:event', { type: 'sb-run-end' });
8639
+ } catch {}
8640
+ });
8641
+ sbRunEmit('▶ ' + cmd + ' (klasör: ' + path.basename(folder) + ')');
8642
+ return {
8643
+ ok: true,
8644
+ note: 'süreç ÇALIŞTIR panelinde başlatıldı — çıktı orada canlı akar; durdurmak için paneldeki ■. Kullanıcıya adres varsa bildir.',
8645
+ };
8646
+ } catch (e) {
8647
+ sbRunProc.proc = null;
8648
+ return { ok: false, error: String((e && e.message) || e) };
8649
+ }
8650
+ }
8651
+
8652
+ ipcMain.handle('sandbox:run', async (_e, payload) => {
8653
+ const folder = String((payload && payload.folder) || '');
8654
+ const cmd = String((payload && payload.cmd) || '').trim();
8655
+ return sbRunStartManaged(folder, cmd);
8656
+ });
8657
+
8658
+ ipcMain.handle('sandbox:runstop', async () => {
8659
+ const p = sbRunProc.proc;
8660
+ if (!p) return { ok: true, wasRunning: false };
8661
+ sbRunProc.proc = null;
8662
+ try {
8663
+ /* cmd.exe /c zinciriyle çocuklar (node/vite) geride kalmasın — ağaç kesimi */
8664
+ const { exec } = require('child_process');
8665
+ if (process.platform === 'win32') exec('taskkill /pid ' + p.pid + ' /T /F', { windowsHide: true });
8666
+ else p.kill('SIGTERM');
8667
+ } catch {}
8668
+ return { ok: true, wasRunning: true };
8669
+ });
8670
+
8671
+ ipcMain.handle('sandbox:openurl', async (_e, url) => {
8672
+ const u = String(url || '').trim();
8673
+ if (!/^https?:\/\/(?:localhost|127\.0\.0\.1)(?::\d{2,5})?(?:\/|$)/i.test(u)) return { ok: false, error: 'yalnız localhost adresleri açılabilir' };
8674
+ try {
8675
+ setBrowserOpen(true, true);
8676
+ browser.view.webContents.loadURL(u).catch(() => {});
8677
+ return { ok: true, url: u };
8678
+ } catch (e) {
8679
+ return { ok: false, error: String((e && e.message) || e) };
8680
+ }
8681
+ });
8682
+
8136
8683
  /* Sağ tık menüsü: HTML dosyasını dahili tarayıcıda GÖRÜNÜR aç */
8137
8684
  /* ---------- BC DAHİLİ STATİK SUNUCU ----------
8138
8685
  Beast Code çıktısı ASLA file:// ile açılmaz: ES modülleri, fetch, ServiceWorker
@@ -8548,8 +9095,8 @@ async function fetchZenFreeModels() {
8548
9095
  if (free.length) return free;
8549
9096
  }
8550
9097
  } catch {}
8551
- /* API cevap vermezse bilinen free liste */
8552
- 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'];
9098
+ /* API cevap vermezse bilinen free liste (09/2026 güncel — eski adlar düşürüldü) */
9099
+ 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'];
8553
9100
  }
8554
9101
 
8555
9102
  function upsertZenEnv(token) {
package/src/preload.js CHANGED
@@ -139,6 +139,19 @@ contextBridge.exposeInMainWorld('beast', {
139
139
  fetchModels: (opts) => ipcRenderer.invoke('custom:fetchModels', opts),
140
140
  builtinProviders: () => ipcRenderer.invoke('providers:builtin'),
141
141
  zenOneClick: () => ipcRenderer.invoke('zen:oneClick'),
142
+
143
+ sandboxList: () => ipcRenderer.invoke('sandbox:list'),
144
+ sandboxClone: (input) => ipcRenderer.invoke('sandbox:clone', input),
145
+ sandboxSend: (payload) => ipcRenderer.invoke('sandbox:send', payload),
146
+ sandboxStop: (payload) => ipcRenderer.invoke('sandbox:stop', payload),
147
+ sandboxNew: (payload) => ipcRenderer.invoke('sandbox:new', payload),
148
+ sandboxRemove: (payload) => ipcRenderer.invoke('sandbox:remove', payload),
149
+ sandboxReveal: (payload) => ipcRenderer.invoke('sandbox:reveal', payload),
150
+ sandboxOpen: (payload) => ipcRenderer.invoke('sandbox:open', payload),
151
+ sandboxTree: (payload) => ipcRenderer.invoke('sandbox:tree', payload),
152
+ sandboxRun: (payload) => ipcRenderer.invoke('sandbox:run', payload),
153
+ sandboxRunStop: () => ipcRenderer.invoke('sandbox:runstop'),
154
+ sandboxOpenUrl: (url) => ipcRenderer.invoke('sandbox:openurl', url),
142
155
  waStart: () => ipcRenderer.invoke('wa:start'),
143
156
  waStop: () => ipcRenderer.invoke('wa:stop'),
144
157
  waReset: () => ipcRenderer.invoke('wa:reset'),
@@ -579,6 +579,7 @@
579
579
  tipGithub: 'GitHub Trending — repoları favorile, chate at, ajana araştır',
580
580
  tipIde: 'IDE Modu — dosyalar + chat + preview',
581
581
  tipStudio: 'Beast Studio — video yapma/düzenleme modu',
582
+ tipSandbox: 'Beast Sandbox — GitHub reposu indir & içinde çalış',
582
583
  tipCodeHide: 'Editörü kapat',
583
584
  tipCodeShow: 'Editörü göster',
584
585
  ide_files: 'DOSYALAR',
@@ -1268,6 +1269,7 @@
1268
1269
  tipGithub: 'GitHub Trending — favorite repos, send to chat, ask the agent',
1269
1270
  tipIde: 'IDE Mode — files + chat + preview',
1270
1271
  tipStudio: 'Beast Studio — video making/editing mode',
1272
+ tipSandbox: 'Beast Sandbox — download a GitHub repo & work inside it',
1271
1273
  tipCodeHide: 'Hide editor',
1272
1274
  tipCodeShow: 'Show editor',
1273
1275
  ide_files: 'FILES',