beast-agent 0.25.4 → 0.25.6

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": "0.25.4",
4
+ "version": "0.25.6",
5
5
  "description": "Ultra-fast local agent shell for Windows.",
6
6
  "author": "algokodcom (AlgoKod)",
7
7
  "license": "MIT",
@@ -6,9 +6,10 @@
6
6
 
7
7
  const fs = require('fs');
8
8
  const path = require('path');
9
- const { chatStream, chatOnce } = require('./llm');
9
+ const { chatStream, chatStreamAuto, chatOnce } = require('./llm');
10
10
  const { chatCompletionsUrl } = require('./config');
11
11
  const tools = require('./tools');
12
+ const research = require('./research');
12
13
  const memory = require('./memory');
13
14
  const skills = require('./skills');
14
15
  const { estTokens, estMsgTokens } = require('./tokens');
@@ -608,7 +609,7 @@ class Engine {
608
609
  }
609
610
  }
610
611
  }
611
- const res = await chatStream(
612
+ const res = await chatStreamAuto(
612
613
  c,
613
614
  { messages: msgs, tools: activeTools, reasoningEffort: this._thinkEffortFor(session) },
614
615
  {
@@ -1300,6 +1301,7 @@ class Engine {
1300
1301
  'CONUŞMA ODAĞI SENDE KALSIN: kullanıcı seninle konuşurken iş çıkmışsa — uzun da olsa UFACIK da (tek komutluk dizin listesi, tek dosya okuma, tek arama…) — run_background ile PARALEL ajana devret; ana sohbet hiçbir işi beklemez; bittiğinde özet otomatik düşer.',
1301
1302
  'Python işleri için python_run kullan: küçük betikler inline code ile; tekrarlayan işler %APPDATA%\\beast\\scripts klasöründeki dosyalarla (ör. news.py = RSS haber toplayıcı: args ["--limit","8","--json"]). Python kurulu olmasa bile ilk çağrıda taşınabilir gömülü runtime otomatik iner.',
1302
1303
  'PYTHON DURUMU: makinede sistem Python\'u görünmese bile ŞAŞIRMA ve "python yok" DEME — python_run aracı kendi taşınabilir runtime\'ını (%APPDATA%\\beast\\py\\python.exe) otomatik indirir/kullanır ve bu klasör run_command PATH\'inde önceliklidir; yani run_command içinde de `python` çalışır. Ham Google/Bing scrape yerine önce web_search aracını kullan (zaten Python çoklu-motor + Exa destekli), script gerektiğinde python_run yaz.',
1304
+ 'PDF ÇIKTI KURALI: PDF üretirken pip\u2019ten pdf paketi (fpdf, fpdf2, markdown-pdf, weasyprint, reportlab vb.) KURMA/KULLANMA — bunlar Türkçe karakterleri bozar. Doğru kit Node tarafında ZATEN kurulu: `pdf-lib` + `@pdf-lib/fontkit` (Türkçe font gömme) ve `pdfkit`. python_run ile DEĞİL; write_file ile .js script yazıp run_command ile `node script.js` çalıştır. md→pdf çevirici YOKTUR ve kurulmaz: kullanıcıya rapor/özet/belge çıktısı vereceksen .md dosyası gönderme — aynı içeriği DOĞRUDAN pdf-lib/pdfkit ile PDF olarak üret ve send_file ile o PDF\u2019i gönder. Ayrıntılı örnekler: pdf skill\u2019i (SKILL.md).',
1303
1305
  'Eski bir hafıza kaydına ihtiyacın olursa memory_search ile ara; kalıcı bilgi/birikim için kb_search kullan, yeni bilgi öğrenirsen kb_add ile kaynak belirt.',
1304
1306
  'Bir oturumda 3+ kez memory_write yaptıysan iş bitince memory_hygiene çağır (duplike/eskime temizliği).',
1305
1307
  'BEAST KAYNAK KORUMASI: Beast\u2019in kendi kurulum/kaynak kod klasörü KİLİTLİDİR — oraya dosya yazamaz, silamaz, komut/betikle değiştiremezsin; okumak serbest. Kullanıcı kodu ancak kendi eliyle dışarıdan değiştirir; böyle bir istek gelirse yapamayacağını söyle ve kullanıcının elle yapması için yol göster.',
@@ -2116,6 +2118,7 @@ class Engine {
2116
2118
  `- Döngülü işleri (çok URL/dosya/sayfa, tekrarlı parse-hesap) TEK python_run betiğinde topluca bitir.\n` +
2117
2119
  `- Web için web_search kullan (zincir dahili tarayıcıyla başlar — gerçek Chromium ile Google); tek aramada bulunamazsa veya çok kaynaklı derin araştırma gerekiyorsa deep_search kullan (çoklu sorgu paralel + gizli tarayıcıda tam sayfa okuma). Sayfa açma/göstermenin VARSAYILANI DAHİLİ tarayıcıdır: browser_open → browser_snapshot → browser_click/type/read. Kullanıcı açıkça dış tarayıcı (chrome/firefox/başka/normal/kendi tarayıcım) istediyse run_command ile \`start "" <url>\` çalıştır. Görseli göremiyorsan metni ocr_read ile oku (source:"browser").\n` +
2118
2120
  `- Bağımsız araç çağrılarını aynı turda PARALEL ver.\n` +
2121
+ `- PDF gerekirse pip\u2019ten paket kurma (Türkçe bozar); Node\u2019un kurulu \`pdf-lib\`+fontkit\u2019iyle .js script yazıp \`node\` ile çalıştır. md→pdf çevirme: belge çıktısını doğrudan PDF olarak üret.\n` +
2119
2122
  `- ARAŞTIRMA SINIRI: 3-5 kaynak yeter; süre hedefi ~3 dakika. 2-3 denemede bulunamayan bilgiyi BIRAK — bulabildiğin kısmi sonucu raporla ve neyi bulamadığını yaz. Kapalı/gizli içerik peşinde koşma.\n` +
2120
2123
  FORMAT_RULES + '\n' +
2121
2124
  `SON ÇIKTI: 3-5 satırlık net sonuç raporu, madde madde. Soru sorma, sohbet etme.`
@@ -2695,7 +2698,7 @@ class Engine {
2695
2698
  ];
2696
2699
  try {
2697
2700
  for (let turn = 0; turn < SUB_MAX_TURNS; turn++) {
2698
- const res = await chatStream(
2701
+ const res = await chatStreamAuto(
2699
2702
  sel,
2700
2703
  { messages: [{ role: 'system', content: system }, ...msgs.slice(1)], tools: subTools, reasoningEffort: this._thinkEffort() },
2701
2704
  { signal: ctrl.signal }
@@ -2720,6 +2723,40 @@ class Engine {
2720
2723
  }
2721
2724
  }
2722
2725
 
2726
+ /* web_search zinciri — web_search VE deep_search aynı zinciri kullanır:
2727
+ 1) dahili tarayıcı (DİREK GOOGLE — ücretsiz + AI cevabı)
2728
+ 2) TinyFish (tarayıcı sorun çıkarsa HEMEN devreye girer — anahtar girildiyse)
2729
+ 3) python çoklu-motor (ddgs / DDG+Bing+Mojeek)
2730
+ 4) Exa (anahtar girildiyse, son çare)
2731
+ Tarayıcıda CAPTCHA/"olağandışı trafik"/boş sonuç algılanırsa 10 dk boyunca
2732
+ tarayıcı ATLANIR — her arama TinyFish'ten başlar, süre dolunca tarayıcı geri döner. */
2733
+ async _webSearchChain(q, n, sessionId, signal) {
2734
+ let r = null;
2735
+ let banned = false;
2736
+ const browserUsable = this.browser && typeof this.browser.search === 'function';
2737
+ if (Date.now() < (this._webSearchBrowserBanUntil || 0)) banned = true;
2738
+ if (browserUsable && !banned) {
2739
+ try { r = await this.browser.search(q, signal, { sessionId }); } catch {}
2740
+ if (!r || !r.ok || !(r.results || []).length) {
2741
+ /* tarayıcı sorunlu — 10 dk atla, alternatif zincir hemen devrede */
2742
+ this._webSearchBrowserBanUntil = Date.now() + 10 * 60 * 1000;
2743
+ r = null;
2744
+ banned = true;
2745
+ }
2746
+ }
2747
+ if (!r || !r.ok || !(r.results || []).length) {
2748
+ try { r = await tools.tinyfishSearch(q, n, signal); } catch {}
2749
+ }
2750
+ if (!r || !r.ok || !(r.results || []).length) {
2751
+ try {
2752
+ r = JSON.parse(await tools.exec('web_search', { query: q, max_results: n }, { cwd: this._sessionWorkspace(sessionId), signal }));
2753
+ } catch {}
2754
+ }
2755
+ const out = r || { ok: false, error: 'web arama başarısız — tüm motorlar boş döndü' };
2756
+ if (out && out.ok && banned) out.note = 'tarayıcı araması 10 dk askıda (CAPTCHA/trafik) — TinyFish/alternatif zincir kullanıldı';
2757
+ return out;
2758
+ }
2759
+
2723
2760
  /* Dahili OCR aracı: görüntüden metin çıkarır (görsel desteklemeyen modeller için).
2724
2761
  Kaynak: 'browser' (dahili tarayıcı görüntüsü, varsayılan) | 'screen' (masaüstü) | dosya yolu. */
2725
2762
  async _ocrRead(args, signal, sessionId) {
@@ -3070,49 +3107,26 @@ class Engine {
3070
3107
  }
3071
3108
  if (name === 'deep_search') {
3072
3109
  /* agentic derin araştırma: çoklu sorgu + gizli tarayıcıda sayfa okuma.
3073
- Hook (main process) yoksa tools.exec'teki arama-tek yol devreye girer. */
3110
+ ARAMA zinciri web_search ile BİREBİR AYNI eski sürüm yalnız python
3111
+ motorlarını (ddgs/DDG/Bing) kullanıyordu; CAPTCHA/engel sonucu motorlar
3112
+ boş dönünce "tüm motorlar boş döndü" hatası veriyordu. Artık önce
3113
+ DAHİLİ TARAYICI (Google) denenir, sonra TinyFish → python → Exa. */
3074
3114
  try {
3075
3115
  emitSafe(this, sessionId, { type: 'status', status: 'derin araştırma: çoklu sorgu + gizli sayfa okuma' });
3076
3116
  } catch {}
3077
- const run = this.research && typeof this.research.deepSearch === 'function'
3078
- ? this.research.deepSearch(args || {}, signal)
3079
- : tools.exec('deep_search', args || {}, { cwd: this._sessionWorkspace(sessionId), signal });
3080
- return JSON.stringify(await run);
3117
+ const deps = {
3118
+ search: (q) => this._webSearchChain(q, 10, sessionId, signal),
3119
+ };
3120
+ if (this.research && typeof this.research.readPage === 'function') {
3121
+ deps.readPage = (u) => this.research.readPage(u, signal);
3122
+ }
3123
+ return JSON.stringify(await research.deepSearch(args || {}, deps, signal));
3081
3124
  }
3082
3125
  if (name === 'web_search') {
3083
- /* SIRA: 1) dahili tarayıcı (DİREK GOOGLE — ücretsiz + AI cevabı)
3084
- 2) TinyFish (tarayıcı sorun çıkarsa HEMEN devreye girer — anahtar girildiyse)
3085
- 3) python çoklu-motor (ddgs / DDG+Bing+Mojeek)
3086
- 4) Exa (anahtar girildiyse, son çare)
3087
- Tarayıcıda CAPTCHA/"olağandışı trafik"/boş sonuç algılanırsa 10 dk boyunca
3088
- tarayıcı ATLANIR — her arama TinyFish'ten başlar, süre dolunca tarayıcı geri döner. */
3089
3126
  const q = String((args && args.query) || '');
3090
3127
  const n = Number((args && args.max_results) || 8);
3091
- let r = null;
3092
- let banned = false;
3093
- const browserUsable = this.browser && typeof this.browser.search === 'function';
3094
- if (Date.now() < (this._webSearchBrowserBanUntil || 0)) banned = true;
3095
- if (browserUsable && !banned) {
3096
- try { r = await this.browser.search(q, signal, { sessionId }); } catch {}
3097
- if (!r || !r.ok || !(r.results || []).length) {
3098
- /* tarayıcı sorunlu — 10 dk atla, alternatif zincir hemen devrede */
3099
- this._webSearchBrowserBanUntil = Date.now() + 10 * 60 * 1000;
3100
- r = null;
3101
- banned = true;
3102
- }
3103
- }
3104
- if (!r || !r.ok || !(r.results || []).length) {
3105
- try { r = await tools.tinyfishSearch(q, n, signal); } catch {}
3106
- }
3107
- if (!r || !r.ok || !(r.results || []).length) {
3108
- try {
3109
- r = JSON.parse(await tools.exec('web_search', args, { cwd: this._sessionWorkspace(sessionId), signal }));
3110
- } catch {}
3111
- }
3112
- const out = r || { ok: false, error: 'web arama başarısız — tüm motorlar boş döndü' };
3113
- if (out && out.ok && banned) out.note = 'tarayıcı araması 10 dk askıda (CAPTCHA/trafik) — TinyFish/alternatif zincir kullanıldı';
3114
- return JSON.stringify(out);
3115
- }
3128
+ return JSON.stringify(await this._webSearchChain(q, n, sessionId, signal));
3129
+ }
3116
3130
  if (name === 'ocr_read') {
3117
3131
  return JSON.stringify(await this._ocrRead(args || {}, signal, sessionId));
3118
3132
  }
package/src/agent/llm.js CHANGED
@@ -188,6 +188,51 @@ async function streamOnce(sel, body, { signal, onDelta, onRetry } = {}, omitReas
188
188
  return { content, reasoning, toolCalls, usage, finishReason };
189
189
  }
190
190
 
191
+ /* usage toplama: devam turları gerçek faturalamayı yansıtsın (prompt yeniden sayılır) */
192
+ function sumUsage(a, b) {
193
+ if (!a) return b || null;
194
+ if (!b) return a;
195
+ const out = {};
196
+ for (const k of new Set([...Object.keys(a), ...Object.keys(b)])) {
197
+ if (typeof a[k] === 'number' && typeof b[k] === 'number') out[k] = a[k] + b[k];
198
+ else out[k] = b[k] !== undefined ? b[k] : a[k];
199
+ }
200
+ return out;
201
+ }
202
+
203
+ /* OTOMATİK DEVAM: model cevabı çıkış limiti (finish_reason='length') yüzünden
204
+ kelime ortasında kesilirse — özellikle free modellerin düşük varsayılan
205
+ limitlerinde olur — yarım cümle kullanıcıya gitmesin. Kaldığı yerden devam
206
+ istenip metin birleştirilir (en fazla 2 devam turu). Araç çağrılı yanıtlarda
207
+ ve içerik hiç gelmediyse devreye girmez; orijinal mesaj dizisi DEĞİŞTİRİLMEZ. */
208
+ const CONTINUE_PROMPT =
209
+ 'Yanıtın çıkış limiti nedeniyle ortadan kesildi. Kaldığın yerden DEVAM ET — baştan başlama, özetleme, açıklama yapma; yalnızca kesilen kısmın DEVAMINI yaz.';
210
+
211
+ async function chatStreamAuto(sel, body, opts = {}) {
212
+ const first = await chatStream(sel, body, opts);
213
+ if (first.finishReason !== 'length' || !first.content || (first.toolCalls && first.toolCalls.length)) {
214
+ return first;
215
+ }
216
+ let full = first.content;
217
+ let last = first;
218
+ for (let i = 0; i < 2; i++) {
219
+ const msgs2 = body.messages.slice();
220
+ msgs2.push({ role: 'assistant', content: full });
221
+ msgs2.push({ role: 'user', content: CONTINUE_PROMPT });
222
+ let cont;
223
+ try {
224
+ cont = await chatStream(sel, { ...body, messages: msgs2 }, opts);
225
+ } catch {
226
+ break; // devam turu patlarsa elimizdeki kısmi yanıtla döneriz
227
+ }
228
+ if (!cont.content || (cont.toolCalls && cont.toolCalls.length)) break;
229
+ full += cont.content;
230
+ last = { ...cont, content: full, usage: sumUsage(first.usage, cont.usage) };
231
+ if (cont.finishReason !== 'length') break;
232
+ }
233
+ return last;
234
+ }
235
+
191
236
  async function chatOnce(sel, body, { signal, onRetry } = {}) {
192
237
  const res = await withRetries(
193
238
  async () => {
@@ -216,4 +261,4 @@ async function chatOnce(sel, body, { signal, onRetry } = {}) {
216
261
  };
217
262
  }
218
263
 
219
- module.exports = { chatStream, chatOnce, withRetries, friendlyError };
264
+ module.exports = { chatStream, chatStreamAuto, chatOnce, withRetries, friendlyError, sumUsage };
@@ -232,14 +232,21 @@ Zorunlu kurallar:
232
232
  },
233
233
  {
234
234
  folder: 'pdf',
235
+ force: true,
235
236
  body: `---
236
237
  name: pdf
237
238
  description: PDF işleme rehberi — metin çıkarma (pdf-parse), Türkçe karakterli PDF oluşturma/değiştirme (pdf-lib/pdfkit), sayfa→görsel (pdf-to-img) ve taranmış PDF için OCR (tesseract.js). Kullanıcı bir .pdf dosyasından bahsederse ÖNCE bu skill'i oku.
238
- version: 1.0.0
239
+ version: 1.1.0
239
240
  ---
240
241
 
241
242
  # PDF İşleme Rehberi
242
243
 
244
+ ## EN KRİTİK KURALLAR (önce bunlar)
245
+
246
+ 1. Türkçe karakterli PDF için pip\u2019ten paket KURMA/KULLANMA (fpdf, fpdf2, markdown-pdf, weasyprint, reportlab vb.) — bunlar ğ ş ı İ karakterlerini bozar. Doğru kit Node\u2019ta ZATEN kurulu: \`pdf-lib\` + \`@pdf-lib/fontkit\` ve \`pdfkit\`. Script\u2019i write_file ile .js yaz, run_command ile \`node script.js\` çalıştır (python_run İLE DEĞİL).
247
+ 2. md→pdf çevirici YOKTUR ve kurulmaz — kullanıcıya rapor/özet/belge çıktısı vereceksen .md gönderme; aynı içeriği doğrudan PDF olarak ÜRET ve o dosyayı send_file ile gönder.
248
+ 3. \`pdf-parse\` ile \`pdf-to-img\` aynı process\u2019e yüklenmez (aşağıdaki çakışma kuralı).
249
+
243
250
  Kurulu paketler ve rolleri:
244
251
 
245
252
  | İstek | Paket |
@@ -501,21 +501,27 @@ async function fetchWithTimeout(url, opts = {}, timeoutMs = 20000, signal) {
501
501
  async function webSearch(query, { maxResults = 8, signal } = {}) {
502
502
  const q = String(query || '').trim().slice(0, 400);
503
503
  if (!q) return { ok: false, error: 'boş sorgu' };
504
- const res = await fetchWithTimeout(
505
- 'https://html.duckduckgo.com/html/',
506
- {
507
- method: 'POST',
508
- headers: {
509
- 'Content-Type': 'application/x-www-form-urlencoded',
510
- 'User-Agent': UA,
511
- Accept: 'text/html',
504
+ let res;
505
+ try {
506
+ res = await fetchWithTimeout(
507
+ 'https://html.duckduckgo.com/html/',
508
+ {
509
+ method: 'POST',
510
+ headers: {
511
+ 'Content-Type': 'application/x-www-form-urlencoded',
512
+ 'User-Agent': UA,
513
+ Accept: 'text/html',
514
+ },
515
+ body: new URLSearchParams({ q }).toString(),
512
516
  },
513
- body: new URLSearchParams({ q }).toString(),
514
- },
515
- 20000,
516
- signal
517
- );
518
- if (!res.ok) throw new Error(`DDG HTTP ${res.status}`);
517
+ 20000,
518
+ signal
519
+ );
520
+ } catch (e) {
521
+ /* ağ hatası/rate-limit exception fırlatmasın — zincir devam edebilsin */
522
+ return { ok: false, error: 'DDG erişilemedi: ' + String((e && e.message) || e) };
523
+ }
524
+ if (!res.ok) return { ok: false, error: `DDG HTTP ${res.status}` };
519
525
  const html = await res.text();
520
526
  const results = parseDdgResults(html, Math.min(Math.max(Number(maxResults) || 8, 1), 12));
521
527
  if (!results.length) {
package/src/main.js CHANGED
@@ -149,8 +149,7 @@ function startNpmUpdateWatch() {
149
149
  check();
150
150
  setInterval(check, 6 * 60 * 60 * 1000);
151
151
  }
152
- const { htmlToText, setExaKey, setTinyfishKey, webSearchFast } = require('./agent/tools');
153
- const research = require('./agent/research');
152
+ const { htmlToText, setExaKey, setTinyfishKey } = require('./agent/tools');
154
153
  const { waToolLine } = require('./agent/watext');
155
154
 
156
155
  /* #3 merkezî log sistemine process-seviye hataları da düşsün */
@@ -1184,7 +1183,7 @@ async function waFlush(jid) {
1184
1183
  if (payload.participant && !merged.participant) merged.participant = payload.participant;
1185
1184
  if (payload.participantPn && !merged.participantPn) merged.participantPn = payload.participantPn;
1186
1185
  if (payload.participantUsername && !merged.participantUsername) merged.participantUsername = payload.participantUsername;
1187
- if (payload.mentioned) merged.mentioned = true;
1186
+ if (payload.mentioned) merged.mentioned = true;
1188
1187
  if (!senderNum && sn) senderNum = sn;
1189
1188
  }
1190
1189
  try {
@@ -2100,13 +2099,9 @@ function reloadBackend() {
2100
2099
  act: (k, a, s) => browserAct(k, a, s),
2101
2100
  },
2102
2101
  research: {
2103
- /* deep_search hook'u: çoklu sorgu araması + GİZLİ tarayıcıda tam sayfa okuma.
2104
- Panel (browser.*) hiç kullanılmaz kullanıcı ekranı rahatsız edilmez. */
2105
- deepSearch: (args, signal) =>
2106
- research.deepSearch(args || {}, {
2107
- search: (q) => webSearchFast(q, { maxResults: 10, signal }),
2108
- readPage: (u) => researchRead(u, signal),
2109
- }, signal),
2102
+ /* deep_search'ün "sayfayı GİZLİ tarayıcıda açıp oku" parçası (Electron-only).
2103
+ Arama zinciri engine._webSearchChain içinde web_search ile birebir aynı. */
2104
+ readPage: (u, s) => researchRead(u, s),
2110
2105
  },
2111
2106
  emit: (ev) => {
2112
2107
  if (win && !win.isDestroyed()) win.webContents.send('agent:event', ev);
@@ -2598,26 +2593,13 @@ function browserGate(job) {
2598
2593
  __trafficTail = p.catch(() => {});
2599
2594
  return p;
2600
2595
  }
2601
- /* çağıran oturum paralel ajan mı? (run_background ile açılan bg oturumu)
2602
- tarayıcıyı GİZLİ kullan: panel ekrana fırlamaz, ajan konsolu kapanmaz */
2603
- function isBgBrowserCtx(ctx) {
2604
- const sid = ctx && ctx.sessionId ? String(ctx.sessionId) : '';
2605
- if (!sid) return false;
2606
- try {
2607
- return !!(engine && engine._bgJobs && engine._bgJobs.has(sid));
2608
- } catch {
2609
- return false;
2610
- }
2611
- }
2612
-
2613
- /* bg ajan için açma stratejisi: kapalıysa GİZLİ aç; zaten açıksa mevcut
2614
- görünürlüğe dokunma (kullanıcı paneli izliyorsa aniden saklamayalım) */
2615
- function setBrowserOpenForAgent(ctx) {
2616
- if (isBgBrowserCtx(ctx)) {
2617
- if (!browser.open) setBrowserOpen(true, false);
2618
- return;
2619
- }
2620
- setBrowserOpen(true);
2596
+ /* AJAN TARAYICI STRATEJİSİ TÜM oturumlar (ana sohbet + WhatsApp botları + paralel ajanlar):
2597
+ ajan tarayıcıyı KENDİ açıyorsa hep GİZLİ açılır panel ekrana fırlamaz,
2598
+ Paralel Ajan Konsolu kapanmaz. Kullanıcı izlemek isterse tarayıcı düğmesine
2599
+ basar (browser:toggle gizli paneli görünür kılar). Zaten açıksa (kullanıcı
2600
+ paneli açık tutuyorsa) görünürlüğe dokunulmaz. */
2601
+ function setBrowserOpenForAgent() {
2602
+ if (!browser.open) setBrowserOpen(true, false);
2621
2603
  }
2622
2604
 
2623
2605
  async function browserNavigate(raw, signal, ctx) {
@@ -2636,7 +2618,7 @@ async function browserNavigateNow(raw, signal, ctx) {
2636
2618
  ? 'https://' + url
2637
2619
  : 'https://duckduckgo.com/?q=' + encodeURIComponent(url);
2638
2620
  }
2639
- setBrowserOpenForAgent(ctx);
2621
+ setBrowserOpenForAgent();
2640
2622
  const wc = browser.view.webContents;
2641
2623
  await new Promise((resolve) => {
2642
2624
  let settled = false;
@@ -2863,7 +2845,7 @@ async function browserSearchNow(query, signal, ctx) {
2863
2845
  if (signal && signal.aborted) return null;
2864
2846
  const q = String(query || '').trim();
2865
2847
  if (!q) return null;
2866
- setBrowserOpenForAgent(ctx);
2848
+ setBrowserOpenForAgent();
2867
2849
  const wc = browser.view && browser.view.webContents;
2868
2850
  if (!wc) return null;
2869
2851
 
@@ -4430,7 +4412,7 @@ ipcMain.handle('update:install', () => {
4430
4412
  if (!isNpmMode()) return { ok: false, error: NPM_ONLY_TEXT };
4431
4413
  npmUpdateViaCmd();
4432
4414
  return { ok: true, npm: true };
4433
- });
4415
+ });
4434
4416
 
4435
4417
  ipcMain.handle('update:setAuto', (_e, cfg) => {
4436
4418
  if (cfg && typeof cfg.autoCheck === 'boolean') settings.autoCheckUpdate = cfg.autoCheck;
@@ -4665,7 +4647,12 @@ ipcMain.handle('cron:runNow', (_e, id) => {
4665
4647
 
4666
4648
  /* ---------------- tarayıcı IPC ---------------- */
4667
4649
  ipcMain.handle('browser:toggle', () => {
4668
- /* kullanıcı kendi açıyor mutlaka görünür */
4650
+ /* gizli çalışan ajan paneli varsa düğme ONU GÖRÜNÜR yapar; değilse aç/kapa.
4651
+ (ajanlar tarayıcıyı hep gizli açar — kullanıcı izlemek isterse buradan gösterir) */
4652
+ if (browser.open && !browser.visible) {
4653
+ setBrowserOpen(true, true);
4654
+ return { open: browser.open, visible: browser.visible };
4655
+ }
4669
4656
  setBrowserOpen(!browser.open, true);
4670
4657
  return { open: browser.open, visible: browser.visible };
4671
4658
  });
@@ -5322,12 +5309,15 @@ function runNpmGlobalInstall() {
5322
5309
  });
5323
5310
  }
5324
5311
 
5325
- /* auth login etkileşimlidir (tarayıcı açar) — kullanıcı görsün diye ayrı pencere */
5312
+ /* auth login etkileşimlidir (tarayıcı açar) — kullanıcı görsün diye ayrı pencere.
5313
+ -ExecutionPolicy Bypass: SADECE bu pencere için — Windows varsayılan "Restricted"
5314
+ politikası npm'in opencode.ps1 shim'ini bloklar ("running scripts is disabled")
5315
+ → taze makinede tek tık kurulum patlamasın; sistem politikası DEĞİŞMEZ. */
5326
5316
  function openZenAuthWindow() {
5327
5317
  try {
5328
5318
  spawn(
5329
5319
  'cmd.exe',
5330
- ['/c', 'start', '', 'powershell.exe', '-NoExit', '-Command', 'opencode auth login'],
5320
+ ['/c', 'start', '', 'powershell.exe', '-NoExit', '-ExecutionPolicy', 'Bypass', '-Command', 'opencode auth login'],
5331
5321
  { detached: true, windowsHide: false, stdio: 'ignore' }
5332
5322
  ).unref();
5333
5323
  return true;
package/tests/llm.test.js CHANGED
@@ -3,7 +3,7 @@
3
3
  require('./setup');
4
4
  const test = require('node:test');
5
5
  const assert = require('node:assert');
6
- const { withRetries, friendlyError } = require('../src/agent/llm');
6
+ const { withRetries, friendlyError, sumUsage, chatStreamAuto } = require('../src/agent/llm');
7
7
 
8
8
  test('withRetries: 503 sonrası başarılı isteği tekrarlar', async () => {
9
9
  let calls = 0;
@@ -54,3 +54,83 @@ test('friendlyError ipuçları içerir', () => {
54
54
  assert.match(m, /kullanılamıyor/);
55
55
  assert.match(m, /model seçici/);
56
56
  });
57
+
58
+ test('sumUsage: sayısal alanları toplar (devam turu faturalaması)', () => {
59
+ const u = sumUsage(
60
+ { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
61
+ { prompt_tokens: 8, completion_tokens: 3, total_tokens: 11 }
62
+ );
63
+ assert.deepEqual(u, { prompt_tokens: 18, completion_tokens: 8, total_tokens: 26 });
64
+ assert.equal(sumUsage(null, { total_tokens: 3 }).total_tokens, 3);
65
+ assert.equal(sumUsage({ total_tokens: 7 }, null).total_tokens, 7);
66
+ });
67
+
68
+ /* SSE mock sunucusu: finish_reason=length → otomatik devam turu açılır */
69
+ function sseServer(script) {
70
+ const http = require('node:http');
71
+ const calls = [];
72
+ const srv = http.createServer((req, res) => {
73
+ let body = '';
74
+ req.on('data', (c) => (body += c));
75
+ req.on('end', () => {
76
+ calls.push(JSON.parse(body).messages);
77
+ res.writeHead(200, { 'Content-Type': 'text/event-stream' });
78
+ for (const ev of script(calls.length, calls[calls.length - 1])) res.write(ev);
79
+ res.end();
80
+ });
81
+ });
82
+ return { srv, calls };
83
+ }
84
+
85
+ test('chatStreamAuto: length kesintisinde kaldığı yerden devam eder', async () => {
86
+ const { srv, calls } = sseServer((n) => {
87
+ if (n === 1) {
88
+ return [
89
+ 'data: ' + JSON.stringify({ choices: [{ delta: { content: 'Merhaba dun' } }] }) + '\n\n',
90
+ 'data: ' + JSON.stringify({ choices: [{ delta: { content: 'ya basladi' }, finish_reason: 'length' }] }) + '\n\n',
91
+ 'data: [DONE]\n\n',
92
+ ];
93
+ }
94
+ return [
95
+ 'data: ' + JSON.stringify({ choices: [{ delta: { content: 'yor devam' } }] }) + '\n\n',
96
+ 'data: ' + JSON.stringify({ choices: [{ delta: {}, finish_reason: 'stop' }] }) + '\n\n',
97
+ 'data: [DONE]\n\n',
98
+ ];
99
+ });
100
+ await new Promise((r) => srv.listen(0, '127.0.0.1', r));
101
+ try {
102
+ const port = srv.address().port;
103
+ const sel = { url: `http://127.0.0.1:${port}/v1/chat/completions`, key: 'k', model: 'm' };
104
+ const deltas = [];
105
+ const r = await chatStreamAuto(sel, { messages: [{ role: 'user', content: 'selam' }] }, { onDelta: (d) => deltas.push(d) });
106
+ assert.equal(r.content, 'Merhaba dunya basladiyor devam');
107
+ assert.equal(r.finishReason, 'stop');
108
+ assert.equal(calls.length, 2);
109
+ // 2. turda devam mesajları eklenmiş olmalı (orijinal dizi kopyalanır)
110
+ const m2 = calls[1];
111
+ assert.equal(m2[m2.length - 2].role, 'assistant');
112
+ assert.equal(m2[m2.length - 1].role, 'user');
113
+ assert.match(m2[m2.length - 1].content, /DEVAM/);
114
+ // streaming kesintisiz: 3 delta
115
+ assert.deepEqual(deltas.join(''), r.content);
116
+ } finally {
117
+ srv.close();
118
+ }
119
+ });
120
+
121
+ test('chatStreamAuto: normal (stop) yanıtta hiç devam turu açılmaz', async () => {
122
+ const { srv, calls } = sseServer(() => [
123
+ 'data: ' + JSON.stringify({ choices: [{ delta: { content: 'tam cevap' }, finish_reason: 'stop' }] }) + '\n\n',
124
+ 'data: [DONE]\n\n',
125
+ ]);
126
+ await new Promise((r) => srv.listen(0, '127.0.0.1', r));
127
+ try {
128
+ const port = srv.address().port;
129
+ const sel = { url: `http://127.0.0.1:${port}/v1/chat/completions`, key: 'k', model: 'm' };
130
+ const r = await chatStreamAuto(sel, { messages: [{ role: 'user', content: 'selam' }] });
131
+ assert.equal(r.content, 'tam cevap');
132
+ assert.equal(calls.length, 1);
133
+ } finally {
134
+ srv.close();
135
+ }
136
+ });
@@ -48,8 +48,9 @@ test('paketlenmiş python scriptleri mevcut ve tohumlanır', () => {
48
48
  /* canlı: python hızlı arama yolu (ağ varsa gerçek sonuç, yoksa JS fallback) */
49
49
  test('webSearchFast: python yolu ya da fallback sonuç döner', async function () {
50
50
  const r = await tools.webSearchFast('electron builder portable windows', { maxResults: 5 });
51
- assert.strictEqual(r.ok, true);
52
- if (!r.results.length) return this.skip('motorlar bu turda sonuç döndürmedi (ağ/rate-limit) — kablaj doğru');
51
+ /* rate-limit/CAPTCHA turlarında motorlar ok:false dönebilir — kablaj testi;
52
+ nazik davranınca sonuç şekli doğrulanır, aksi halde skip */
53
+ if (!r || !r.ok || !r.results.length) return this.skip('motorlar bu turda sonuç döndürmedi (ağ/rate-limit) — kablaj doğru');
53
54
  assert.ok(r.results[0].title && r.results[0].url);
54
55
  }, { timeout: 40000 });
55
56