beast-agent 2.5.1 → 2.6.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/main.js +167 -69
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "beast-agent",
3
3
  "productName": "Beast Agent",
4
- "version": "2.5.1",
4
+ "version": "2.6.0",
5
5
  "description": "Ultra-fast local agent shell for Windows.",
6
6
  "author": "algokodcom (AlgoKod)",
7
7
  "license": "MIT",
package/src/main.js CHANGED
@@ -229,17 +229,20 @@ startHealthServer(); /* splash/boot aşamasından itibaren /health ayakta */
229
229
  try { setSearchChain(settings.searchChain); } catch {}
230
230
  try { setTinyfishKey(settings.tinyfishKey || null); } catch {}
231
231
  let wa = null;
232
- let waChats = new Map(); // jid -> aktif session id
232
+ let waChats = new Map(); // jid -> aktif session id (hepsi KANAL TEK OTURUMUNA bağlı)
233
+ let waSingleSid = null; // KANAL TEK OTURUMU: WhatsApp için BİR oturum — silinmedikçe hep aynı
233
234
  let waHistory = new Map(); // jid -> [sid,...] bu sohbete ait tüm oturumlar
234
235
  let waBcMode = new Set(); // jid -> BeastCode modu AKTİF (WhatsApp'tan uzaktan kodlama)
235
236
  let waJidPn = new Map(); // jid -> gerçek telefon numarası (LID fallback için)
236
237
  const WA_HISTORY_CAP = 20;
237
238
  let tg = null;
238
- let tgChats = new Map(); // telegram chatId -> aktif session id
239
+ let tgChats = new Map(); // telegram chatId -> aktif session id (hepsi KANAL TEK OTURUMUNA bağlı)
240
+ let tgSingleSid = null; // KANAL TEK OTURUMU: Telegram için BİR oturum
239
241
  let tgHistory = new Map(); // chatId -> [sid,...]
240
242
  const TG_HISTORY_CAP = 20;
241
243
  let dc = null;
242
- let dcChats = new Map(); // discord channelId -> aktif session id
244
+ let dcChats = new Map(); // discord channelId -> aktif session id (hepsi KANAL TEK OTURUMUNA bağlı)
245
+ let dcSingleSid = null; // KANAL TEK OTURUMU: Discord için BİR oturum
243
246
  let dcHistory = new Map(); // channelId -> [sid,...]
244
247
  const DC_HISTORY_CAP = 20;
245
248
  const DC_CHATS_FILE = path.join(APP_DIR, 'dc-chats.json');
@@ -278,6 +281,8 @@ try {
278
281
  if (!h.includes(s)) h.push(s);
279
282
  waHistory.set(j, h.slice(-WA_HISTORY_CAP));
280
283
  }
284
+ /* kanal tek oturumu: kayıtlı aktif id (geçersizse ensure yeniden seçer) */
285
+ if (typeof raw.active === 'string' && raw.active) waSingleSid = raw.active;
281
286
  /* BeastCode modu: hangi sohbetler uzaktan kodlama yapıyor */
282
287
  if (Array.isArray(raw.bcMode)) {
283
288
  for (const j of raw.bcMode) if (typeof j === 'string') waBcMode.add(j);
@@ -293,6 +298,7 @@ function saveWaChats() {
293
298
  chats: Object.fromEntries(waChats),
294
299
  history: Object.fromEntries([...waHistory.entries()].map(([j, a]) => [j, a.slice(-WA_HISTORY_CAP)])),
295
300
  bcMode: [...waBcMode],
301
+ active: waSingleSid || '',
296
302
  })
297
303
  );
298
304
  } catch {}
@@ -1268,12 +1274,26 @@ async function tryWaSlash(jid, rawText, senderNum, payload0) {
1268
1274
  if (cmd === 'help') {
1269
1275
  out = waSlashHelp();
1270
1276
  } else if (cmd === 'new') {
1277
+ /* /new: kanalın TEK oturumunu kapat, yenisini aç — aynı anda iki
1278
+ WhatsApp sohbet oturumu olmaz; eski oturum silinir */
1279
+ const oldSid = waSingleSid || waChats.get(jid) || '';
1280
+ if (oldSid) {
1281
+ let olds = null;
1282
+ try { olds = engine.cache.get(oldSid) || null; } catch {}
1283
+ if (!olds || !olds.bcCode) {
1284
+ /* Beast Code panel oturumuysa dokunma — yalnız pointer taşınır */
1285
+ try { engine.deleteSession(oldSid); } catch {}
1286
+ for (const [j, h] of waHistory.entries()) waHistory.set(j, h.filter((x) => x !== oldSid));
1287
+ }
1288
+ }
1271
1289
  const v = engine.createSession();
1290
+ waSingleSid = v.id;
1291
+ for (const j of [...waChats.keys()]) waChats.set(j, v.id);
1272
1292
  waChats.set(jid, v.id);
1273
1293
  waRememberSession(jid, v.id);
1274
1294
  saveWaChats();
1275
1295
  wa.setWatchJids([...waChats.keys()]);
1276
- out = `*Yeni oturum* \`${v.code}\` açıldı — anlatabilirsin.\nDiğer oturumlara geçiş: \`/open <kod>\``;
1296
+ out = `*Yeni oturum* \`${v.code}\` açıldı — eski oturum kapatıldı, artık buradan devam.\nVar olan başka oturuma geçiş: \`/open <kod>\``;
1277
1297
  } else if (cmd === 'open') {
1278
1298
  if (!arg) {
1279
1299
  out = 'Kullanım: `/open <kod>` — kodları görmek için /sessions';
@@ -1282,7 +1302,9 @@ async function tryWaSlash(jid, rawText, senderNum, payload0) {
1282
1302
  if (!hit) {
1283
1303
  out = `\`${arg.toUpperCase()}\` kodlu oturum bulunamadı. Listeyi görmek için /sessions`;
1284
1304
  } else {
1285
- waChats.set(jid, hit.id);
1305
+ /* /open: kanal TEK oturumunu bu oturuma çevirir — tüm jid'ler bağlanır */
1306
+ waSingleSid = hit.id;
1307
+ for (const j of [...waChats.keys()]) waChats.set(j, hit.id);
1286
1308
  waRememberSession(jid, hit.id);
1287
1309
  saveWaChats();
1288
1310
  wa.setWatchJids([...waChats.keys()]);
@@ -1564,14 +1586,15 @@ async function tryWaSlash(jid, rawText, senderNum, payload0) {
1564
1586
  try { engine.interrupt(sid, 'kullanıcı /beastagent ile sohbet moduna döndü'); } catch {}
1565
1587
  }
1566
1588
  waBcMode.delete(jid);
1567
- const v = engine.createSession();
1568
- waChats.set(jid, v.id);
1569
- waRememberSession(jid, v.id);
1589
+ /* KANAL TEK OTURUMU: yeni oturum AÇILMAZ — sohbet oturumuna dönülür
1590
+ (silinmediyse hep aynı; ilk kez ise burada oluşur) */
1591
+ const chatSid = ensureWaSession(jid);
1592
+ const chatS = engine.cache.get(chatSid);
1570
1593
  saveWaChats();
1571
1594
  if (wa) wa.setWatchJids([...waChats.keys()]);
1572
1595
  emitWaEventSafe({ type: 'bc-screen', on: false });
1573
1596
  out =
1574
- `*💬 Sohbet moduna dönüldü* — masaüstünde chat ekranı açıldı, yeni oturum \`${v.code}\`.\n` +
1597
+ `*💬 Sohbet moduna dönüldü* — masaüstünde chat ekranı açıldı, sohbet oturumu \`${(chatS && chatS.code) || '?'}\`.\n` +
1575
1598
  `Kodlar duruyor: \`${waBcWorkspace()}\`\n` +
1576
1599
  `Tekrar kodlamak için: /beastcode`;
1577
1600
  }
@@ -1682,19 +1705,58 @@ function waGroupSenderInfo(payload) {
1682
1705
  return { label, name: (hitP && hitP.name) || '', isOwner: !!(hitP && hitP.owner) };
1683
1706
  }
1684
1707
 
1685
- /* Oturum yoksa oluştur (processWaMessage ile aynı kalıp) */
1708
+ /* ---------- KANAL TEK OTURUMU (WA/TG/DC) ----------
1709
+ Her kanalın BİR aktif sohbet oturumu vardır; eskisi silinmedikçe hep
1710
+ AYNI oturumdan devam edilir — Beast kendi kendine oturum kapatıp
1711
+ yenisiyle değiştirmez. Yenisi yalnızca: (1) kayıtlı oturum dosyası
1712
+ yoksa (kullanıcı UI'dan sildi), (2) kullanıcı WA'da /new yazarsa.
1713
+ Masaüstü chat UI'daki "+ Yeni Sohbet" bu kuralın dışındadır. */
1714
+ function sessionFileAlive(sid) {
1715
+ try {
1716
+ return !!sid && fs.existsSync(path.join(engine.sessionsDir, String(sid) + '.jsonl'));
1717
+ } catch {
1718
+ return false;
1719
+ }
1720
+ }
1721
+
1722
+ /* kanalın yaşayan tek oturumunu döndürür; geçersizse eski bağlamdan devralır,
1723
+ o da yoksa (ilk kullanım) yeni açar ve tüm jid'leri ona bağlar */
1686
1724
  function ensureWaSession(jid) {
1687
- let sid = waChats.get(jid);
1688
- if (!sid) {
1689
- const v = engine.createSession();
1690
- sid = v.id;
1691
- waChats.set(jid, sid);
1725
+ if (!waSingleSid || !sessionFileAlive(waSingleSid)) {
1726
+ let best = '';
1727
+ let bestTs = '';
1728
+ for (const c of new Set([...waChats.values(), waSingleSid])) {
1729
+ if (!c || !sessionFileAlive(c)) continue;
1730
+ let ts = '';
1731
+ try {
1732
+ const s = engine.cache.get(c) || engine._load(c);
1733
+ ts = String((s && s.updatedAt) || '');
1734
+ } catch {}
1735
+ if (!best || ts > bestTs) {
1736
+ best = c;
1737
+ bestTs = ts;
1738
+ }
1739
+ }
1740
+ waSingleSid = best || engine.createSession().id;
1741
+ saveWaChats();
1742
+ }
1743
+ let dirty = false;
1744
+ for (const [j, s] of waChats.entries()) {
1745
+ if (s !== waSingleSid) {
1746
+ waChats.set(j, waSingleSid);
1747
+ dirty = true;
1748
+ }
1749
+ }
1750
+ if (jid != null && waChats.get(String(jid)) !== waSingleSid) {
1751
+ waChats.set(String(jid), waSingleSid);
1752
+ dirty = true;
1753
+ }
1754
+ waRememberSession(String(jid), waSingleSid);
1755
+ if (dirty) {
1692
1756
  saveWaChats();
1693
1757
  if (wa) wa.setWatchJids([...waChats.keys()]);
1694
- } else {
1695
- waRememberSession(jid, sid);
1696
1758
  }
1697
- return sid;
1759
+ return waSingleSid;
1698
1760
  }
1699
1761
 
1700
1762
  /* ---------- GRUP BAĞLAM AKIŞI (mentionOnly + seeAll) ----------
@@ -2232,7 +2294,7 @@ async function processWaMessage(jid, payload, senderNum, requeues = 0) {
2232
2294
  waLog(`skip flush: izinli eşleşme yok (sender=+${senderNum || '?'})`);
2233
2295
  return;
2234
2296
  }
2235
- let sid = waChats.get(jid);
2297
+ let sid = waChats.get(jid) || waSingleSid;
2236
2298
  if (sid && engine.isBusy(sid)) {
2237
2299
  /* oturum hâlâ önceki işle uğraşıyor — KAYBETME: sınırı kaldırdık,
2238
2300
  iş bitene dek her debounce turunda yeniden deneriz; kullanıcıya
@@ -2246,16 +2308,8 @@ async function processWaMessage(jid, payload, senderNum, requeues = 0) {
2246
2308
  if (!isGroup && requeues > 0) {
2247
2309
  // retry sonrası izin yeniden kontrol edilir (yukarıda hit zaten alınıyor)
2248
2310
  }
2249
- if (!sid) {
2250
- const v = engine.createSession();
2251
- sid = v.id;
2252
- waChats.set(jid, sid);
2253
- waRememberSession(jid, sid);
2254
- saveWaChats();
2255
- if (wa) wa.setWatchJids([...waChats.keys()]);
2256
- } else {
2257
- waRememberSession(jid, sid);
2258
- }
2311
+ /* KANAL TEK OTURUMU: eskisi silinmedikçe hep aynı oturumdan devam */
2312
+ sid = ensureWaSession(jid);
2259
2313
  // Cevap verilecek — karşı telefonda "yazıyor…" göstergesi (medya işlenene dek sürer)
2260
2314
  wa.setComposing(jid, true);
2261
2315
  // Kişi bazlı granül izin: all/web/read/chat
@@ -2462,6 +2516,8 @@ function ensureWa() {
2462
2516
  if (Array.isArray(arr)) tgHistory.set(c, arr.filter((x) => typeof x === 'string').slice(-TG_HISTORY_CAP));
2463
2517
  }
2464
2518
  }
2519
+ /* kanal tek oturumu: kayıtlı aktif id (geçersizse ensure yeniden seçer) */
2520
+ if (raw && typeof raw.active === 'string' && raw.active) tgSingleSid = raw.active;
2465
2521
  for (const [c, s] of tgChats.entries()) {
2466
2522
  const h = tgHistory.get(c) || [];
2467
2523
  if (!h.includes(s)) h.push(s);
@@ -2477,6 +2533,7 @@ function saveTgChats() {
2477
2533
  JSON.stringify({
2478
2534
  chats: Object.fromEntries(tgChats),
2479
2535
  history: Object.fromEntries([...tgHistory.entries()].map(([c, a]) => [c, a.slice(-TG_HISTORY_CAP)])),
2536
+ active: tgSingleSid || '',
2480
2537
  })
2481
2538
  );
2482
2539
  } catch {}
@@ -2549,21 +2606,14 @@ async function processTgMessage(chatId, payload, requeues = 0) {
2549
2606
  tgLog(`skip flush: izinli eşleşme yok (sender=${payload.senderId || '?'})`);
2550
2607
  return;
2551
2608
  }
2552
- let sid = tgChats.get(chatId);
2609
+ let sid = tgChats.get(chatId) || tgSingleSid;
2553
2610
  if (sid && engine.isBusy(sid)) {
2554
2611
  /* oturum meşgul — WA ile aynı: kaybetme, iş bitene dek yeniden dene */
2555
2612
  await new Promise((r) => setTimeout(r, TG_DEBOUNCE_MS));
2556
2613
  return processTgMessage(chatId, payload, requeues + 1);
2557
2614
  }
2558
- if (!sid) {
2559
- const v = engine.createSession();
2560
- sid = v.id;
2561
- tgChats.set(chatId, sid);
2562
- tgRememberSession(chatId, sid);
2563
- saveTgChats();
2564
- } else {
2565
- tgRememberSession(chatId, sid);
2566
- }
2615
+ /* KANAL TEK OTURUMU: eskisi silinmedikçe hep aynı oturumdan devam */
2616
+ sid = ensureTgSession(chatId);
2567
2617
  /* Kişi bazlı granül izin: all/web/read/chat */
2568
2618
  let perm = hit.perm || (hit.lockdown ? 'chat' : 'all');
2569
2619
  engine.setSessionPerm(sid, perm);
@@ -2686,6 +2736,8 @@ function dcFind(senderId, username) {
2686
2736
  if (Array.isArray(arr)) dcHistory.set(c, arr.filter((x) => typeof x === 'string').slice(-DC_HISTORY_CAP));
2687
2737
  }
2688
2738
  }
2739
+ /* kanal tek oturumu: kayıtlı aktif id (geçersizse ensure yeniden seçer) */
2740
+ if (raw && typeof raw.active === 'string' && raw.active) dcSingleSid = raw.active;
2689
2741
  for (const [c, s] of dcChats.entries()) {
2690
2742
  const h = dcHistory.get(c) || [];
2691
2743
  if (!h.includes(s)) h.push(s);
@@ -2701,6 +2753,7 @@ function saveDcChats() {
2701
2753
  JSON.stringify({
2702
2754
  chats: Object.fromEntries(dcChats),
2703
2755
  history: Object.fromEntries([...dcHistory.entries()].map(([c, a]) => [c, a.slice(-DC_HISTORY_CAP)])),
2756
+ active: dcSingleSid || '',
2704
2757
  })
2705
2758
  );
2706
2759
  } catch {}
@@ -2767,21 +2820,14 @@ async function processDcMessage(channelId, payload) {
2767
2820
  dcLog(`skip flush: izinli eşleşme yok (sender=${payload.senderId || '?'})`);
2768
2821
  return;
2769
2822
  }
2770
- let sid = dcChats.get(channelId);
2823
+ let sid = dcChats.get(channelId) || dcSingleSid;
2771
2824
  if (sid && engine.isBusy(sid)) {
2772
2825
  /* oturum meşgul — WA/TG ile aynı: kaybetme, iş bitene dek yeniden dene */
2773
2826
  await new Promise((r) => setTimeout(r, DC_DEBOUNCE_MS));
2774
2827
  return processDcMessage(channelId, payload, 1);
2775
2828
  }
2776
- if (!sid) {
2777
- const v = engine.createSession();
2778
- sid = v.id;
2779
- dcChats.set(channelId, sid);
2780
- dcRememberSession(channelId, sid);
2781
- saveDcChats();
2782
- } else {
2783
- dcRememberSession(channelId, sid);
2784
- }
2829
+ /* KANAL TEK OTURUMU: eskisi silinmedikçe hep aynı oturumdan devam */
2830
+ sid = ensureDcSession(channelId);
2785
2831
  /* Kişi bazlı granül izin: all/web/read/chat */
2786
2832
  let perm = hit.perm || (hit.lockdown ? 'chat' : 'all');
2787
2833
  engine.setSessionPerm(sid, perm);
@@ -6330,13 +6376,19 @@ function empatiLlmCompose(prompt) {
6330
6376
  const PROACTIVE_MARK = '🫡 *Beast proaktif:*';
6331
6377
 
6332
6378
  /* bildirimin ALTINA YAZILAN KAYNAK: haberin gerçek linki varsa o, yoksa kaynak adı.
6333
- Kullanıcı "bu proje neymiş?" diye sorduğunda linkten bulabilsin. */
6379
+ Kullanıcı "bu proje neymiş?" diye sorduğunda linkten bulabilsin.
6380
+ TEK SATIR kuralı: satır sonu/boşluklar temizlenir, uzunsa sona "..." gelir. */
6334
6381
  function empatiSourceLine(ev) {
6335
6382
  if (!ev) return '';
6336
- const url = String(ev.url || '').trim();
6337
- if (/^https?:\/\//i.test(url)) return '🔗 ' + url;
6338
- const src = String(ev.source || '').trim();
6339
- return src ? '🔗 kaynak: ' + src.slice(0, 80) : '';
6383
+ const oneLine = (s) =>
6384
+ String(s || '').replace(/\s*\n+\s*/g, ' ').replace(/\s{2,}/g, ' ').trim();
6385
+ const ELLIPSIS = '...';
6386
+ const cap = (s, max) =>
6387
+ oneLine(s).length > max ? oneLine(s).slice(0, max - ELLIPSIS.length) + ELLIPSIS : oneLine(s);
6388
+ const url = oneLine(ev.url);
6389
+ if (/^https?:\/\//i.test(url)) return '🔗 ' + cap(url, 100);
6390
+ const src = oneLine(ev.source);
6391
+ return src ? '🔗 kaynak: ' + cap(src, 80) : '';
6340
6392
  }
6341
6393
 
6342
6394
  function empatiInjectToSession(sid, out) {
@@ -6360,28 +6412,74 @@ function empatiInjectText(ev, out) {
6360
6412
  );
6361
6413
  }
6362
6414
 
6363
- /* Oturum yoksa oluştur (processTgMessage/processDcMessage ile aynı kalıp) */
6415
+ /* kanalın yaşayan tek oturumu (WA kuralının aynısı — Telegram/Discord) */
6364
6416
  function ensureTgSession(chatId) {
6365
- let sid = tgChats.get(chatId);
6366
- if (!sid) {
6367
- const v = engine.createSession();
6368
- sid = v.id;
6369
- tgChats.set(chatId, sid);
6370
- tgRememberSession(chatId, sid);
6417
+ if (!tgSingleSid || !sessionFileAlive(tgSingleSid)) {
6418
+ let best = '';
6419
+ let bestTs = '';
6420
+ for (const c of new Set([...tgChats.values(), tgSingleSid])) {
6421
+ if (!c || !sessionFileAlive(c)) continue;
6422
+ let ts = '';
6423
+ try {
6424
+ const s = engine.cache.get(c) || engine._load(c);
6425
+ ts = String((s && s.updatedAt) || '');
6426
+ } catch {}
6427
+ if (!best || ts > bestTs) {
6428
+ best = c;
6429
+ bestTs = ts;
6430
+ }
6431
+ }
6432
+ tgSingleSid = best || engine.createSession().id;
6371
6433
  saveTgChats();
6372
6434
  }
6373
- return sid;
6435
+ let dirty = false;
6436
+ for (const [c, s] of tgChats.entries()) {
6437
+ if (s !== tgSingleSid) {
6438
+ tgChats.set(c, tgSingleSid);
6439
+ dirty = true;
6440
+ }
6441
+ }
6442
+ if (chatId != null && tgChats.get(String(chatId)) !== tgSingleSid) {
6443
+ tgChats.set(String(chatId), tgSingleSid);
6444
+ dirty = true;
6445
+ }
6446
+ tgRememberSession(String(chatId), tgSingleSid);
6447
+ if (dirty) saveTgChats();
6448
+ return tgSingleSid;
6374
6449
  }
6375
6450
  function ensureDcSession(channelId) {
6376
- let sid = dcChats.get(channelId);
6377
- if (!sid) {
6378
- const v = engine.createSession();
6379
- sid = v.id;
6380
- dcChats.set(channelId, sid);
6381
- dcRememberSession(channelId, sid);
6451
+ if (!dcSingleSid || !sessionFileAlive(dcSingleSid)) {
6452
+ let best = '';
6453
+ let bestTs = '';
6454
+ for (const c of new Set([...dcChats.values(), dcSingleSid])) {
6455
+ if (!c || !sessionFileAlive(c)) continue;
6456
+ let ts = '';
6457
+ try {
6458
+ const s = engine.cache.get(c) || engine._load(c);
6459
+ ts = String((s && s.updatedAt) || '');
6460
+ } catch {}
6461
+ if (!best || ts > bestTs) {
6462
+ best = c;
6463
+ bestTs = ts;
6464
+ }
6465
+ }
6466
+ dcSingleSid = best || engine.createSession().id;
6382
6467
  saveDcChats();
6383
6468
  }
6384
- return sid;
6469
+ let dirty = false;
6470
+ for (const [c, s] of dcChats.entries()) {
6471
+ if (s !== dcSingleSid) {
6472
+ dcChats.set(c, dcSingleSid);
6473
+ dirty = true;
6474
+ }
6475
+ }
6476
+ if (channelId != null && dcChats.get(String(channelId)) !== dcSingleSid) {
6477
+ dcChats.set(String(channelId), dcSingleSid);
6478
+ dirty = true;
6479
+ }
6480
+ dcRememberSession(String(channelId), dcSingleSid);
6481
+ if (dirty) saveDcChats();
6482
+ return dcSingleSid;
6385
6483
  }
6386
6484
 
6387
6485
  /* Masaüstü yedeği: en güncel (bg/bot-DM'siz, meşgul olmayan) sohbet — yoksa yeni */