beast-agent 2.7.1 → 2.7.3

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.7.1",
4
+ "version": "2.7.3",
5
5
  "description": "Ultra-fast local agent shell for Windows.",
6
6
  "author": "algokodcom (AlgoKod)",
7
7
  "license": "MIT",
@@ -2350,6 +2350,9 @@ class Engine {
2350
2350
  ) {
2351
2351
  lastMsg.content = (lastMsg.content + '\n\n' + body).slice(0, USER_MAX);
2352
2352
  this._rewriteLastMsg(s.id, lastMsg.content, null, 'assistant');
2353
+ /* UI'ya bildir: birleştirme yeni bubble basmaz — son asistan balonu
2354
+ güncellenir (yoksa proaktif not ekranda hiç görünmezdi) */
2355
+ this.emit({ type: 'message-merge', sessionId: s.id, message: lastMsg });
2353
2356
  this.emit({ type: 'sessions' });
2354
2357
  return true;
2355
2358
  }
package/src/main.js CHANGED
@@ -234,6 +234,7 @@ let waSingleSid = null; // KANAL TEK OTURUMU: WhatsApp için BİR oturum — sil
234
234
  let waHistory = new Map(); // jid -> [sid,...] bu sohbete ait tüm oturumlar
235
235
  let waBcMode = new Set(); // jid -> BeastCode modu AKTİF (WhatsApp'tan uzaktan kodlama)
236
236
  let waJidPn = new Map(); // jid -> gerçek telefon numarası (LID fallback için)
237
+ let waLastActiveJid = ''; // en son mesaj gelen WA sohbeti — kanal tek oturumunda cevap/dosya hedefi
237
238
  const WA_HISTORY_CAP = 20;
238
239
  let tg = null;
239
240
  let tgChats = new Map(); // telegram chatId -> aktif session id (hepsi KANAL TEK OTURUMUNA bağlı)
@@ -1759,6 +1760,19 @@ function ensureWaSession(jid) {
1759
1760
  return waSingleSid;
1760
1761
  }
1761
1762
 
1763
+ /* Kanal tek oturumunda cevap/dosya hedefi: bu oturuma bağlı sohbetler arasında
1764
+ EN SON mesaj gelen önceliklidir (kullanıcı o pencerede bekliyor); o bilgi
1765
+ yoksa (restart vb.) ilk bağlı sohbet kullanılır. null = bağlı sohbet yok. */
1766
+ function waReplyJid(sid) {
1767
+ try {
1768
+ if (waLastActiveJid && waChats.get(waLastActiveJid) === sid) return waLastActiveJid;
1769
+ } catch {}
1770
+ for (const [j, s] of waChats) {
1771
+ if (s === sid) return j;
1772
+ }
1773
+ return null;
1774
+ }
1775
+
1762
1776
  /* ---------- GRUP BAĞLAM AKIŞI (mentionOnly + seeAll) ----------
1763
1777
  Bot grubun tüm konuşmasını okur ama CEVAP ÜRETMEZ; mesajlar sessizce
1764
1778
  oturum geçmişine bağlam olarak düşer. @mention gelince bot tüm bu
@@ -2311,6 +2325,7 @@ async function processWaMessage(jid, payload, senderNum, requeues = 0) {
2311
2325
  }
2312
2326
  /* KANAL TEK OTURUMU: eskisi silinmedikçe hep aynı oturumdan devam */
2313
2327
  sid = ensureWaSession(jid);
2328
+ waLastActiveJid = String(jid); // cevap/dosya hedefi: en son yazan sohbet
2314
2329
  // Cevap verilecek — karşı telefonda "yazıyor…" göstergesi (medya işlenene dek sürer)
2315
2330
  wa.setComposing(jid, true);
2316
2331
  // Kişi bazlı granül izin: all/web/read/chat
@@ -2985,6 +3000,7 @@ function reloadBackend() {
2985
3000
  emit: (ev) => {
2986
3001
  if (win && !win.isDestroyed()) win.webContents.send('agent:event', ev);
2987
3002
  try { empatiRememberFromEvent(ev); } catch {} /* empati hafızası: sohbet → kayıt + ilgi öğrenme */
3003
+ empatiFlushInjectsOnDone(ev); /* tur ortasında kuyruğa düşen proaktif bildirimleri iş bitince geçmişe bas */
2988
3004
  flushDesktopOnDone(ev); /* biriken desktop mesajlarını sıraya bas */
2989
3005
  bcFlushOnDone(ev); /* Beast Code kuyruğunu iş bitiminde boşalt */
2990
3006
  stFlushOnDone(ev); /* Beast Studio kuyruğunu iş bitiminde boşalt */
@@ -2998,13 +3014,13 @@ function reloadBackend() {
2998
3014
  Spam olmasın: sohbet başına en az 7 sn'de bir tek satır. */
2999
3015
  if (ev.type === 'tool-start' && wa && wa.connected) {
3000
3016
  try {
3001
- const hit = [...waChats.entries()].find(([, s]) => s === ev.sessionId);
3002
- if (hit) {
3017
+ const pingJid = waReplyJid(ev.sessionId);
3018
+ if (pingJid) {
3003
3019
  const now = Date.now();
3004
3020
  const last = lastWaToolPing.get(ev.sessionId) || 0;
3005
3021
  if (now - last >= 7000) {
3006
3022
  lastWaToolPing.set(ev.sessionId, now);
3007
- sendWaSafe(hit[0], '\u203A ' + String(waToolLine(ev.name, ev.args || {})).replace(/^\u203A /, '')).catch(() => {});
3023
+ sendWaSafe(pingJid, '\u203A ' + String(waToolLine(ev.name, ev.args || {})).replace(/^\u203A /, '')).catch(() => {});
3008
3024
  }
3009
3025
  }
3010
3026
  } catch {}
@@ -3017,8 +3033,8 @@ function reloadBackend() {
3017
3033
  const isInterim = Array.isArray(ev.message.tool_calls) && ev.message.tool_calls.length > 0;
3018
3034
  if (mtxt && isInterim) {
3019
3035
  try {
3020
- const hit = [...waChats.entries()].find(([, s]) => s === ev.sessionId);
3021
- if (hit) sendWaSafe(hit[0], mtxt).catch(() => {});
3036
+ const interJid = waReplyJid(ev.sessionId);
3037
+ if (interJid) sendWaSafe(interJid, mtxt).catch(() => {});
3022
3038
  } catch {}
3023
3039
  }
3024
3040
  }
@@ -3042,9 +3058,8 @@ function reloadBackend() {
3042
3058
  }
3043
3059
  // WhatsApp oturumlarının son cevabını geri gönder (metin + opsiyonel ses)
3044
3060
  if ((ev.type === 'done' || ev.type === 'error') && wa && wa.connected) {
3045
- const hit = [...waChats.entries()].find(([, s]) => s === ev.sessionId);
3046
- if (hit) {
3047
- const wajid = hit[0];
3061
+ const wajid = waReplyJid(ev.sessionId);
3062
+ if (wajid) {
3048
3063
  (async () => {
3049
3064
  try {
3050
3065
  if (ev.type === 'error') {
@@ -3762,8 +3777,9 @@ function setBrowserOpen(v, forceVisible) {
3762
3777
  // AÇMA — görünürlük: forceVisible true/false ise onu uygula;
3763
3778
  // belirtilmemişse kullanıcı tercihi belirler. Tarayıcı gizleme ÖZELLİĞİ
3764
3779
  // (settings.browserHide) kapalıysa tercih ne olursa olsun tarayıcı GÖRÜNÜR açılır.
3765
- // PARALEL AJANLAR (bg oturum) her zaman forceVisible=false ile çağırır →
3766
- // tarayıcı gizli modda çalışır, kullanıcı ekranı ve ajan konsolu rahatsız edilmez.
3780
+ // AJAN gezinmeleri setBrowserOpenForAgent üzerinden gelir: gizleme özelliği
3781
+ // kapalıysa görünür, özellik + göz kapalıyken gizli çalışır (kullanıcı ekranı
3782
+ // rahatsız edilmez; tarayıcı düğmesi gizli paneli görünür kılar).
3767
3783
  browser.open = true;
3768
3784
  browser.visible =
3769
3785
  forceVisible === true ? true : forceVisible === false ? false : !browserHeadlessPref();
@@ -3796,13 +3812,15 @@ function browserGate(job) {
3796
3812
  return p;
3797
3813
  }
3798
3814
  /* AJAN TARAYICI STRATEJİSİ — TÜM oturumlar (ana sohbet + WhatsApp botları + paralel ajanlar):
3799
- ajan tarayıcıyı KENDİ açıyorsa hep GİZLİ açılır — panel ekrana fırlamaz,
3800
- Paralel Ajan Konsolu kapanmaz. Kullanıcı izlemek isterse tarayıcı düğmesine
3801
- basar (browser:toggle gizli paneli görünür kılar). Zaten açıksa (kullanıcı
3802
- paneli açık tutuyorsa) görünürlüğe dokunulmaz. */
3815
+ GİZLEME ÖZELLİĞİ (Ayarlar → Web Arama) KAPALIYSA ajan tarayıcıyı GİZLİ açamaz —
3816
+ dahili panel GÖRÜNÜR açılır, kullanıcı aramayı canlı izler. Özellik açıksa göz
3817
+ durumuna bakılır: göz açık (headless tercihi kapalı) → görünür, göz kapalı →
3818
+ gizli. Mobil önizleme varsa siluet canlı kalsın diye her koşulda görünür.
3819
+ Zaten açıksa (kullanıcı paneli açık tutuyorsa) görünürlüğe dokunulmaz. */
3803
3820
  function setBrowserOpenForAgent() {
3804
- /* mobil önizleme açıkken ajan gezinmeleri GÖRÜNÜR açılır — siluet canlı kalsın */
3805
- if (!browser.open) setBrowserOpen(true, browser.mobile ? true : false);
3821
+ if (!browser.open) {
3822
+ setBrowserOpen(true, !browserHideEnabled() || !browserHeadlessPref() || browser.mobile);
3823
+ }
3806
3824
  }
3807
3825
 
3808
3826
  async function browserNavigate(raw, signal, ctx) {
@@ -6455,11 +6473,51 @@ function empatiSourceLineWa(ev) {
6455
6473
  return title ? '🔗 ' + cap(title, 80) : '';
6456
6474
  }
6457
6475
 
6476
+ /* Enjeksiyon kuyruğu: oturum TUR ORTASINDAYKEN (ajan bir işle uğraşırken)
6477
+ gelen proaktif bildirimi sessizce DÜŞÜRME — eskisi tam olarak buydu:
6478
+ "bazen ajan konudan haber olmuyor". Metin kuyrukta bekler, oturumun
6479
+ işi bitince ('done'/'error') geçmişe asistan mesajı olarak yazılır;
6480
+ model bir sonraki turda "ben bu bildirimi attım" der, kullanıcı sorusuna
6481
+ hazırdır. */
6482
+ const empatiInjectQueue = new Map(); // sid -> [text]
6483
+ const EMPATI_INJECT_QUEUE_CAP = 20;
6484
+
6458
6485
  function empatiInjectToSession(sid, out) {
6459
- try {
6460
- if (!sid || engine.isBusy(sid)) return; /* tur ortasında geçmişi karıştırma — bildirim kanala zaten gitti */
6461
- engine.injectAssistant(sid, out);
6462
- } catch {}
6486
+ const target = String(sid || '');
6487
+ const body = String(out || '');
6488
+ if (!target || !body.trim()) return;
6489
+ let busy = false;
6490
+ try { busy = engine.isBusy(target); } catch {}
6491
+ if (busy) {
6492
+ let q = empatiInjectQueue.get(target);
6493
+ if (!q) {
6494
+ q = [];
6495
+ empatiInjectQueue.set(target, q);
6496
+ }
6497
+ if (q.length < EMPATI_INJECT_QUEUE_CAP) {
6498
+ q.push(body);
6499
+ empatiLog(`bildirim enjeksiyonu kuyrukta (ajan tur sürüyor) sid=${target} bekleyen=${q.length}`);
6500
+ }
6501
+ return;
6502
+ }
6503
+ try { engine.injectAssistant(target, body); } catch {}
6504
+ }
6505
+
6506
+ /* iş bitince (done/error) biriken proaktif enjeksiyonları geçmişe bas.
6507
+ 'done' eventi isBusy temizlenmeden ÖNCE gelir (engine.js: ctrl delete
6508
+ finally bloğunda) — flushDesktopOnDone gibi macrotask'a ertele. */
6509
+ function empatiFlushInjectsOnDone(ev) {
6510
+ if (!ev || !ev.sessionId || (ev.type !== 'done' && ev.type !== 'error')) return;
6511
+ const q = empatiInjectQueue.get(String(ev.sessionId));
6512
+ if (!q || !q.length) return;
6513
+ empatiInjectQueue.delete(String(ev.sessionId));
6514
+ setTimeout(() => {
6515
+ /* empatiInjectToSession üzerinden: arada yeni tur başladıysa yeniden kuyruğa düşer */
6516
+ for (const text of q) {
6517
+ try { empatiInjectToSession(String(ev.sessionId), text); } catch {}
6518
+ }
6519
+ empatiLog(`kuyrukta bekleyen ${q.length} proaktif bildirim geçmişe işlendi sid=${ev.sessionId}`);
6520
+ }, 250);
6463
6521
  }
6464
6522
 
6465
6523
  /* kanala giden metin + Beast'in İÇ bağlamı ayrıdır: kullanıcı linki silse bile
@@ -6618,12 +6676,16 @@ function empatiNotify(text, ev) {
6618
6676
  for (const fn of senders) {
6619
6677
  try { fn(); sent++; } catch {}
6620
6678
  }
6621
- /* hiçbir entegrasyona yazılamadıysa yalnız masaüstü chat UI'a düş —
6622
- metin güncel sohbete de asistan mesajı olarak işlenir */
6679
+ /* MASAÜSTÜ CHAT UI: entegrasyon kullanılsa da kullanılmasa da proaktif mesaj
6680
+ sohbette MESAJ olarak görünür (asistan balonu) + toast çıkar. Ajan da
6681
+ desktop oturumu geçmişinde attığı bildirimi GÖRÜR — hangi kanala gittiğine
6682
+ bakmaksızın konudan haberdar olur. Kanal oturumlarına ise BAĞLAM'lı `inject`
6683
+ gider (kanal kullanıcısı ham BAĞLAM bloğunu görmez, ajan görür). */
6623
6684
  try {
6624
- if (!sent && win && !win.isDestroyed()) {
6625
- empatiInjectToSession(empatiDesktopSid(), inject);
6626
- win.webContents.send('agent:event', { type: 'proactive', id: ev.id, level: ev.level, title: ev.title, text: out });
6685
+ if (win && !win.isDestroyed()) {
6686
+ const dsid = empatiDesktopSid();
6687
+ empatiInjectToSession(dsid, out);
6688
+ win.webContents.send('agent:event', { type: 'proactive', sessionId: dsid, id: ev.id, level: ev.level, title: ev.title, text: out });
6627
6689
  }
6628
6690
  } catch {}
6629
6691
  }
@@ -6761,7 +6823,8 @@ ipcMain.handle('cron:runNow', (_e, id) => {
6761
6823
  /* ---------------- tarayıcı IPC ---------------- */
6762
6824
  ipcMain.handle('browser:toggle', () => {
6763
6825
  /* gizli çalışan ajan paneli varsa düğme ONU GÖRÜNÜR yapar; değilse aç/kapa.
6764
- (ajanlar tarayıcıyı hep gizli açar — kullanıcı izlemek isterse buradan gösterir) */
6826
+ (ajan gizleme özelliği + göz kapalıyken gizli çalışır — kullanıcı izlemek
6827
+ isterse buradan gösterir) */
6765
6828
  if (browser.open && !browser.visible) {
6766
6829
  setBrowserOpen(true, true);
6767
6830
  return { open: browser.open, visible: browser.visible };
@@ -8876,8 +8939,42 @@ ipcMain.handle('screen:capture', async () => {
8876
8939
  });
8877
8940
 
8878
8941
  /* #26 ajanın send_file aracı: dosyayı doğru kanala (WA veya chat) ulaştırır.
8879
- Paralel ajan işiyse parent sohbete, masaüstünde dosya kartı olarak düşer. */
8942
+ Paralel ajan işiyse parent sohbete, masaüstünde dosya kartı olarak düşer.
8943
+ WhatsApp hedefinde resim/video/ses → medya mesajı, PDF/belge → doğru
8944
+ mimetype ile belge mesajı olarak gider; LID adresi patlarsa gerçek numaraya
8945
+ (@s.whatsapp.net) tek kez düşülür, yine başarısızsa ajan'a HATA döner. */
8880
8946
  const FILE_IMAGE_EXT = new Set(['.png', '.jpg', '.jpeg', '.webp', '.gif']);
8947
+ const FILE_MIME = {
8948
+ '.pdf': 'application/pdf',
8949
+ '.doc': 'application/msword',
8950
+ '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
8951
+ '.xls': 'application/vnd.ms-excel',
8952
+ '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
8953
+ '.ppt': 'application/vnd.ms-powerpoint',
8954
+ '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
8955
+ '.txt': 'text/plain',
8956
+ '.csv': 'text/csv',
8957
+ '.md': 'text/markdown',
8958
+ '.json': 'application/json',
8959
+ '.html': 'text/html',
8960
+ '.zip': 'application/zip',
8961
+ '.rar': 'application/vnd.rar',
8962
+ '.7z': 'application/x-7z-compressed',
8963
+ '.mp3': 'audio/mpeg',
8964
+ '.m4a': 'audio/mp4',
8965
+ '.wav': 'audio/wav',
8966
+ '.ogg': 'audio/ogg',
8967
+ '.opus': 'audio/ogg; codecs=opus',
8968
+ '.mp4': 'video/mp4',
8969
+ '.mkv': 'video/x-matroska',
8970
+ '.webm': 'video/webm',
8971
+ '.svg': 'image/svg+xml',
8972
+ '.apk': 'application/vnd.android.package-archive',
8973
+ };
8974
+ function mimeForFile(ext) {
8975
+ return FILE_MIME[String(ext || '').toLowerCase()] || 'application/octet-stream';
8976
+ }
8977
+
8881
8978
  async function deliverFile(sessionId, filePath, caption) {
8882
8979
  try {
8883
8980
  const abs = path.isAbsolute(filePath) ? filePath : path.join(engine.workspace || process.cwd(), filePath);
@@ -8889,17 +8986,34 @@ async function deliverFile(sessionId, filePath, caption) {
8889
8986
  const job = engine.listBgJobs().find((j) => j.id === sid);
8890
8987
  if (job && job.parentId) sid = job.parentId; // paralel ajan → parent sohbet
8891
8988
  } catch {}
8892
- /* WhatsApp hedefi: bu oturum bir WA sohbetine bağlıysa oraya gönder */
8893
- let jid = null;
8894
- for (const [j, s] of waChats) {
8895
- if (s === sid) { jid = j; break; }
8896
- }
8989
+ /* WhatsApp hedefi: bu oturum bir WA sohbetine bağlıysa oraya gönder.
8990
+ Birden çok sohbet aynı oturuma bağlıysa EN SON yazan kazanır. */
8991
+ const jid = waReplyJid(sid);
8897
8992
  if (jid && wa) {
8898
8993
  const buf = fs.readFileSync(abs);
8899
- const ok = FILE_IMAGE_EXT.has(ext)
8900
- ? await wa.sendImage(jid, buf, caption || name)
8901
- : await wa.sendFile(jid, buf, name, caption);
8994
+ const sendMedia = async (target) =>
8995
+ FILE_IMAGE_EXT.has(ext)
8996
+ ? await wa.sendImage(target, buf, caption || name)
8997
+ : await wa.sendFile(target, buf, name, caption, mimeForFile(ext));
8998
+ let ok = false;
8999
+ try { ok = !!(await sendMedia(jid)); } catch {}
9000
+ if (!ok) {
9001
+ /* LID/adres fallback: bilinen gerçek numaraya (@s.whatsapp.net) dene */
9002
+ const pn = waJidPn.get(jid);
9003
+ const pnJid = pn ? `${pn}@s.whatsapp.net` : '';
9004
+ if (pnJid && pnJid !== jid) {
9005
+ try { ok = !!(await sendMedia(pnJid)); } catch {}
9006
+ if (ok) waLog(`dosya fallback → ${waPrettyJid(pnJid)} (lid/pn denemelerinden sonra)`);
9007
+ }
9008
+ }
8902
9009
  if (ok) return { ok: true, channel: 'whatsapp', name };
9010
+ /* WA hedefi var ama gönderim başarısız — sessizce "masaüstüne düştü"
9011
+ SANMA: ajan durumu bilsin, kullanıcıya söyleyip tekrar deneyebilsin */
9012
+ waLog(`dosya gönderim HATA → ${waPrettyJid(jid)} "${name}"`);
9013
+ return {
9014
+ ok: false,
9015
+ error: `whatsapp gönderimi başarısız: ${name} — bağlantı/medya hatası; biraz sonra tekrar dene ya da kullanıcıya söyle`,
9016
+ };
8903
9017
  }
8904
9018
  /* masaüstü: sohbete dosya kartı bas */
8905
9019
  if (win && !win.isDestroyed()) {
@@ -5019,6 +5019,19 @@ function onEvent(ev) {
5019
5019
  if (!hasTools) closeChatToolGroup();
5020
5020
  }
5021
5021
  break;
5022
+ case 'message-merge':
5023
+ /* proaktif not son asistan mesajına BİRLEŞTİRİLDİ (injectAssistant) —
5024
+ yeni bubble basma; son asistan balonunun içeriğini güncelle */
5025
+ {
5026
+ const bubbles = els.msgs.querySelectorAll('.msg-assistant .md');
5027
+ const el = bubbles[bubbles.length - 1];
5028
+ const txt = String((ev.message && ev.message.content) || '');
5029
+ if (el && txt) {
5030
+ el.innerHTML = md(txt);
5031
+ scrollDown();
5032
+ }
5033
+ }
5034
+ break;
5022
5035
  case 'token':
5023
5036
  ensureStreamBubble();
5024
5037
  streamRaw += ev.delta;