beast-agent 2.11.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "beast-agent",
3
3
  "productName": "Beast Agent",
4
- "version": "2.11.0",
4
+ "version": "2.12.0",
5
5
  "description": "Ultra-fast local agent shell for Windows.",
6
6
  "author": "algokodcom (AlgoKod)",
7
7
  "license": "MIT",
@@ -1480,6 +1480,9 @@ class Engine {
1480
1480
  'Sen BEAST CODE\u2019sun — IDE panelinde çalışan, OpenCode disiplinli hızlı bir kodlama ajanı. VS Code gibi çevik ol.\n' +
1481
1481
  'GENEL AMAÇLI KODLAMA AJANISIN — tek bir proje türüne sınırlı değilsin: web, backend/API, mobil, masaüstü, CLI, kütüphane, script, oyun, veri, DevOps, gömülü… İstenen neyse o. İstenmeyen şeyi (ör. site değilken site) kendiliğinden ÜRETME.\n' +
1482
1482
  `Çalışma klasörü: ${(session && session.workspace) || this.workspace}\n` +
1483
+ ((session && session.sbSandbox)
1484
+ ? 'BEAST SANDBOX MODUNDASIN: GitHub\u2019dan indirilmiş AÇIK KAYNAK bir repo klasöründesin (Beast-Sandbox). Bu kodun sahibi değilsin — hazır bir dış proje; repo sahibinin konvansiyonlarına uy, gereksiz yeniden biçimlendirme yapma. İşe README + paket bildirim dosyasıyla başla (list_dir/glob); bağımlılıklar kurulu değilse önce kurulum komutunu çalıştır (npm install vb.). UZUN SÜRELİ sunucu/süreç (npm start, npm run dev, python app.py vb.) run_command ile ASLA başlatma — tur kilitlenir; kullanıcı çalıştırmak isterse panel_run aracıyla ÇALIŞTIR paneline bırakın (çıktı orada canlı akar, ■ ile durur) ve adresi kullanıcıya bildir. panel_run yalnız BİR süreç çalıştırır.\n'
1485
+ : '') +
1483
1486
  `Yerel zaman: ${localDate} ${localTime}\n` +
1484
1487
  `${modeBlock}\n` +
1485
1488
  'STACK TESPİTİ: işe başlarken workspace\u2019i tanı — package.json, go.mod, Cargo.toml, requirements.txt/pyproject.toml, pom.xml/build.gradle, *.csproj, pubspec.yaml, Gemfile, composer.json vb. işaret dosyalarına bak (list_dir + glob). Dili, framework\u2019ü, konvansiyonları ve build/test/run komutlarını PROJE belirler; her işte aynı teknolojiye itme, mevcut stack\u2019e uy.\n' +
@@ -3106,6 +3109,11 @@ class Engine {
3106
3109
  if (!session || !session.bcCode) toolsList = await mcp.mergeTools(toolsList);
3107
3110
  /* Beast Apps: kurulu app'lerin araçları (app__<id>__<tool>) modele açılır */
3108
3111
  toolsList = apps.mergeTools(toolsList);
3112
+ /* panel_run yalnız SANDBOX oturumlarında görünsün — diğer panellerde
3113
+ modelin alet çantasında olmasın (hook olmadan çalışmaz) */
3114
+ if (!(session && session.sbSandbox)) {
3115
+ toolsList = toolsList.filter((t) => !(t && t.function && t.function.name === 'panel_run'));
3116
+ }
3109
3117
  /* opencode agent.ts port: özel ajan tanımı — prompt/model/araç/steps */
3110
3118
  const adef = this._agentDefFor(session, !!session.bgJob);
3111
3119
  /* Beast Code: todo_write açıklaması "3+ adım" kısıtı içerir ve model küçük
@@ -4229,6 +4237,22 @@ const skills = require('./skills');
4229
4237
  }
4230
4238
  /* GERİ ALMA GÜNLÜĞÜ: dosya yazımından ÖNCE eski içerik kayda geçer */
4231
4239
  this._journalBefore(sessionId, name, args);
4240
+ /* PANEL RUN: uzun süreli süreç ÇALIŞTIR paneline devredilir — tur
4241
+ BLOKLAMAZ (main'deki yönetilen süreç koşucusuna köprü). */
4242
+ if (name === 'panel_run') {
4243
+ const cmd = String((args && args.command) || '').trim();
4244
+ if (!cmd) return JSON.stringify({ ok: false, error: 'komut boş' });
4245
+ if (typeof this.sbRunHook !== 'function') {
4246
+ return JSON.stringify({ ok: false, error: 'panel koşucusu yok — bu araç yalnız Sandbox panelinde çalışır' });
4247
+ }
4248
+ let r;
4249
+ try {
4250
+ r = this.sbRunHook(this._sessionWorkspace(sessionId), cmd);
4251
+ } catch (e) {
4252
+ r = { ok: false, error: String((e && e.message) || e) };
4253
+ }
4254
+ return JSON.stringify(r);
4255
+ }
4232
4256
  /* MCP: dış server araçları (mcp__<server>__<tool>) — tools/call'a köprülenir */
4233
4257
  if (String(name).startsWith('mcp__')) {
4234
4258
  return JSON.stringify(await mcp.call(name, args, signal));
@@ -1488,6 +1488,21 @@ const definitions = [
1488
1488
  },
1489
1489
  },
1490
1490
  },
1491
+ {
1492
+ type: 'function',
1493
+ function: {
1494
+ name: 'panel_run',
1495
+ description:
1496
+ 'Starts a LONG-RUNNING process (dev server, npm start, npm run dev, python app.py, cargo run…) in the managed RUN PANEL instead of blocking this turn. Returns immediately after spawn; output streams live to the panel, the user can stop it with ■ there. Use this when the user asks to RUN/START the project or a server. NEVER use for short one-shot commands (build/test/lint) — use run_command for those. Only one managed process can run at a time.',
1497
+ parameters: {
1498
+ type: 'object',
1499
+ properties: {
1500
+ command: { type: 'string', description: 'Shell command to start, e.g. "npm start" or "npm run dev"' },
1501
+ },
1502
+ required: ['command'],
1503
+ },
1504
+ },
1505
+ },
1491
1506
  {
1492
1507
  type: 'function',
1493
1508
  function: {
package/src/main.js CHANGED
@@ -3103,6 +3103,7 @@ function reloadBackend() {
3103
3103
  flushDesktopOnDone(ev); /* biriken desktop mesajlarını sıraya bas */
3104
3104
  bcFlushOnDone(ev); /* Beast Code kuyruğunu iş bitiminde boşalt */
3105
3105
  stFlushOnDone(ev); /* Beast Studio kuyruğunu iş bitiminde boşalt */
3106
+ sbFlushOnDone(ev); /* Sandbox kuyruğunu iş bitiminde boşalt */
3106
3107
  /* BC canlı önizleme: ajan bir dev server başlattıysa adresi yakala —
3107
3108
  preview butonu ve otomatik açılış DAİMA bu sunucuyu öncelikli kullanır */
3108
3109
  if (ev.type === 'bc-preview' && /^https?:\/\/(?:localhost|127\.0\.0\.1)(?::\d{2,5})?/i.test(String(ev.url || ''))) {
@@ -3284,6 +3285,9 @@ function reloadBackend() {
3284
3285
  }
3285
3286
  },
3286
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;
3287
3291
  return engine.publicState();
3288
3292
  }
3289
3293
 
@@ -7459,9 +7463,10 @@ ipcMain.handle('beastcode:send', async (_e, payload) => {
7459
7463
  return { ok: true, sessionId: s.id, mode: s.bcMode };
7460
7464
  }
7461
7465
  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 };
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 };
7465
7470
  }
7466
7471
  /* boşta: kısa pencere — hızlı ard arda mesajlar tek işte birleşir */
7467
7472
  const q = bcQueuePush(ws, text, attachments);
@@ -7745,8 +7750,9 @@ ipcMain.handle('studio:send', async (_e, payload) => {
7745
7750
  return { ok: true, sessionId: s.id, mode: s.bcMode };
7746
7751
  }
7747
7752
  if (busy) {
7748
- const q = stQueuePush(ws, text, attachments);
7749
- 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 };
7750
7756
  }
7751
7757
  const q = stQueuePush(ws, text, attachments);
7752
7758
  clearTimeout(q.timer);
@@ -8226,6 +8232,454 @@ ipcMain.handle('studio:delete', async (_e, rel) => {
8226
8232
  }
8227
8233
  });
8228
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
+
8229
8683
  /* Sağ tık menüsü: HTML dosyasını dahili tarayıcıda GÖRÜNÜR aç */
8230
8684
  /* ---------- BC DAHİLİ STATİK SUNUCU ----------
8231
8685
  Beast Code çıktısı ASLA file:// ile açılmaz: ES modülleri, fetch, ServiceWorker
@@ -8641,8 +9095,8 @@ async function fetchZenFreeModels() {
8641
9095
  if (free.length) return free;
8642
9096
  }
8643
9097
  } 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'];
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'];
8646
9100
  }
8647
9101
 
8648
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',