beast-agent 1.8.0 → 2.0.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 (52) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +130 -128
  3. package/bin/beast-agent.js +137 -137
  4. package/package.json +1 -1
  5. package/scripts/fix-electron.js +138 -138
  6. package/scripts/release.js +137 -131
  7. package/scripts/swap-electron.js +40 -40
  8. package/src/agent/agentdefs.js +122 -122
  9. package/src/agent/bots.js +580 -580
  10. package/src/agent/bus.js +389 -389
  11. package/src/agent/computeruse.js +200 -200
  12. package/src/agent/config.js +284 -284
  13. package/src/agent/discord.js +332 -332
  14. package/src/agent/engine.js +85 -11
  15. package/src/agent/kb.js +123 -123
  16. package/src/agent/llm.js +430 -430
  17. package/src/agent/logger.js +90 -90
  18. package/src/agent/mcp.js +427 -0
  19. package/src/agent/mem0.js +605 -605
  20. package/src/agent/memory.js +427 -427
  21. package/src/agent/mqueue.js +124 -124
  22. package/src/agent/pdf.js +20 -20
  23. package/src/agent/research.js +133 -133
  24. package/src/agent/scripts/news.py +113 -113
  25. package/src/agent/scripts/stealthsearch.py +30 -30
  26. package/src/agent/scripts/websearch.py +225 -225
  27. package/src/agent/searxng.js +325 -325
  28. package/src/agent/seeds/brainstorming/SKILL.md +90 -90
  29. package/src/agent/seeds/dispatching-parallel-agents/SKILL.md +120 -120
  30. package/src/agent/seeds/executing-plans/SKILL.md +60 -60
  31. package/src/agent/seeds/subagent-driven-development/SKILL.md +167 -167
  32. package/src/agent/seeds/systematic-debugging/SKILL.md +131 -131
  33. package/src/agent/seeds/test-driven-development/SKILL.md +152 -152
  34. package/src/agent/seeds/verification-before-completion/SKILL.md +63 -63
  35. package/src/agent/seeds/writing-plans/SKILL.md +162 -162
  36. package/src/agent/seeds/writing-skills/SKILL.md +229 -229
  37. package/src/agent/skills.js +652 -651
  38. package/src/agent/store.js +378 -378
  39. package/src/agent/telegram.js +155 -155
  40. package/src/agent/tokens.js +39 -39
  41. package/src/agent/usage.js +125 -125
  42. package/src/agent/watchers.js +312 -312
  43. package/src/agent/watext.js +80 -80
  44. package/src/agent/whatsapp.js +555 -555
  45. package/src/cron.js +255 -255
  46. package/src/main.js +385 -6
  47. package/src/preload.js +13 -0
  48. package/src/renderer/browserPreload.js +73 -73
  49. package/src/renderer/i18n.js +38 -0
  50. package/src/renderer/index.html +448 -407
  51. package/src/renderer/renderer.js +897 -42
  52. package/src/renderer/style.css +275 -11
@@ -1,124 +1,124 @@
1
- 'use strict';
2
-
3
- /* OFFLINE MESAJ KUYRUĞU (FEATURE 2)
4
- İnternet/elektrik kesintisinde WhatsApp mesajları kaybolmasın:
5
- - Gönderilemeyen mesaj dosyaya yazılır (%APPDATA%\beast\message_queue.json)
6
- - Bağlantı gelince sırayla gönderilir, gönderilenler kuyruktan silinir
7
- - Retry: 30sn → 1dk → 5dk → 15dk → 30dk backoff; 5 denemeden sonra "failed"
8
- - Dosya tabanlı olduğu için elektrik kesintisinde veri korunur */
9
-
10
- const fs = require('fs');
11
- const path = require('path');
12
- const os = require('os');
13
-
14
- const MAX_QUEUE = 100; // kuyruk üst sınırı — taşarsa eski (öncelikle işlenmiş) kayıtlar düşer
15
- const MAX_RETRY = 5;
16
- const BACKOFF_MS = [30e3, 60e3, 300e3, 900e3, 1800e3]; // 30sn, 1dk, 5dk, 15dk, 30dk
17
-
18
- function root() {
19
- if (process.env.BEAST_DATA) return process.env.BEAST_DATA;
20
- return process.env.APPDATA
21
- ? path.join(process.env.APPDATA, 'beast')
22
- : path.join(os.homedir(), 'AppData', 'Roaming', 'beast');
23
- }
24
-
25
- function file() {
26
- return path.join(root(), 'message_queue.json');
27
- }
28
-
29
- let CACHE = null;
30
-
31
- function load() {
32
- if (CACHE) return CACHE;
33
- try {
34
- const j = JSON.parse(fs.readFileSync(file(), 'utf8'));
35
- CACHE = Array.isArray(j.items) ? j.items : [];
36
- } catch {
37
- CACHE = [];
38
- }
39
- return CACHE;
40
- }
41
-
42
- function save() {
43
- try {
44
- fs.mkdirSync(root(), { recursive: true });
45
- const tmp = file() + '.tmp';
46
- fs.writeFileSync(tmp, JSON.stringify({ items: CACHE }, null, 2));
47
- fs.renameSync(tmp, file()); // atomik yazım — yarı kalmış dosya kirliliği olmaz
48
- } catch {}
49
- }
50
-
51
- /* Gönderilemeyen mesajı kuyruğa al */
52
- function add({ to, body }) {
53
- const list = load();
54
- const item = {
55
- id: 'q' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
56
- timestamp: new Date().toISOString(),
57
- to: String(to || ''),
58
- body: String(body || '').slice(0, 4000),
59
- status: 'pending',
60
- retry_count: 0,
61
- nextAt: 0, // hemen denenmeye hazır
62
- };
63
- list.push(item);
64
- /* taşma koruması: önce sent/failed kayıtlarını, sonra en eski pending'i düşür */
65
- while (list.length > MAX_QUEUE) {
66
- const idx = list.findIndex((x) => x.status === 'sent' || x.status === 'failed');
67
- if (idx >= 0) list.splice(idx, 1);
68
- else list.shift();
69
- }
70
- save();
71
- return item;
72
- }
73
-
74
- /* Şu an denenecek kuyruk öğeleri (backoff süresi dolmuş pending'ler, sırayla) */
75
- function due() {
76
- const now = Date.now();
77
- return load()
78
- .filter((x) => x.status === 'pending' && (x.nextAt || 0) <= now)
79
- .sort((a, b) => String(a.timestamp).localeCompare(String(b.timestamp)));
80
- }
81
-
82
- /* Gönderildi → kuyruktan sil */
83
- function markSent(id) {
84
- const list = load();
85
- const idx = list.findIndex((x) => x.id === id);
86
- if (idx >= 0) {
87
- list.splice(idx, 1);
88
- save();
89
- }
90
- }
91
-
92
- /* Deneme başarısız → backoff planla; 5 denemeden sonra "failed" (gonderilemedi) */
93
- function bumpRetry(id) {
94
- const it = load().find((x) => x.id === id);
95
- if (!it) return;
96
- it.retry_count = (it.retry_count || 0) + 1;
97
- if (it.retry_count >= MAX_RETRY) {
98
- it.status = 'failed';
99
- it.failedAt = new Date().toISOString();
100
- } else {
101
- it.nextAt = Date.now() + BACKOFF_MS[it.retry_count - 1];
102
- }
103
- save();
104
- }
105
-
106
- function pendingCount() {
107
- return load().filter((x) => x.status === 'pending').length;
108
- }
109
-
110
- function stats() {
111
- const l = load();
112
- return {
113
- pending: l.filter((x) => x.status === 'pending').length,
114
- failed: l.filter((x) => x.status === 'failed').length,
115
- total: l.length,
116
- };
117
- }
118
-
119
- /* bağlantı geri gelince testler için */
120
- function _reset() {
121
- CACHE = null;
122
- }
123
-
124
- module.exports = { MAX_QUEUE, MAX_RETRY, BACKOFF_MS, add, due, markSent, bumpRetry, pendingCount, stats, _reset, file };
1
+ 'use strict';
2
+
3
+ /* OFFLINE MESAJ KUYRUĞU (FEATURE 2)
4
+ İnternet/elektrik kesintisinde WhatsApp mesajları kaybolmasın:
5
+ - Gönderilemeyen mesaj dosyaya yazılır (%APPDATA%\beast\message_queue.json)
6
+ - Bağlantı gelince sırayla gönderilir, gönderilenler kuyruktan silinir
7
+ - Retry: 30sn → 1dk → 5dk → 15dk → 30dk backoff; 5 denemeden sonra "failed"
8
+ - Dosya tabanlı olduğu için elektrik kesintisinde veri korunur */
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const os = require('os');
13
+
14
+ const MAX_QUEUE = 100; // kuyruk üst sınırı — taşarsa eski (öncelikle işlenmiş) kayıtlar düşer
15
+ const MAX_RETRY = 5;
16
+ const BACKOFF_MS = [30e3, 60e3, 300e3, 900e3, 1800e3]; // 30sn, 1dk, 5dk, 15dk, 30dk
17
+
18
+ function root() {
19
+ if (process.env.BEAST_DATA) return process.env.BEAST_DATA;
20
+ return process.env.APPDATA
21
+ ? path.join(process.env.APPDATA, 'beast')
22
+ : path.join(os.homedir(), 'AppData', 'Roaming', 'beast');
23
+ }
24
+
25
+ function file() {
26
+ return path.join(root(), 'message_queue.json');
27
+ }
28
+
29
+ let CACHE = null;
30
+
31
+ function load() {
32
+ if (CACHE) return CACHE;
33
+ try {
34
+ const j = JSON.parse(fs.readFileSync(file(), 'utf8'));
35
+ CACHE = Array.isArray(j.items) ? j.items : [];
36
+ } catch {
37
+ CACHE = [];
38
+ }
39
+ return CACHE;
40
+ }
41
+
42
+ function save() {
43
+ try {
44
+ fs.mkdirSync(root(), { recursive: true });
45
+ const tmp = file() + '.tmp';
46
+ fs.writeFileSync(tmp, JSON.stringify({ items: CACHE }, null, 2));
47
+ fs.renameSync(tmp, file()); // atomik yazım — yarı kalmış dosya kirliliği olmaz
48
+ } catch {}
49
+ }
50
+
51
+ /* Gönderilemeyen mesajı kuyruğa al */
52
+ function add({ to, body }) {
53
+ const list = load();
54
+ const item = {
55
+ id: 'q' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
56
+ timestamp: new Date().toISOString(),
57
+ to: String(to || ''),
58
+ body: String(body || '').slice(0, 4000),
59
+ status: 'pending',
60
+ retry_count: 0,
61
+ nextAt: 0, // hemen denenmeye hazır
62
+ };
63
+ list.push(item);
64
+ /* taşma koruması: önce sent/failed kayıtlarını, sonra en eski pending'i düşür */
65
+ while (list.length > MAX_QUEUE) {
66
+ const idx = list.findIndex((x) => x.status === 'sent' || x.status === 'failed');
67
+ if (idx >= 0) list.splice(idx, 1);
68
+ else list.shift();
69
+ }
70
+ save();
71
+ return item;
72
+ }
73
+
74
+ /* Şu an denenecek kuyruk öğeleri (backoff süresi dolmuş pending'ler, sırayla) */
75
+ function due() {
76
+ const now = Date.now();
77
+ return load()
78
+ .filter((x) => x.status === 'pending' && (x.nextAt || 0) <= now)
79
+ .sort((a, b) => String(a.timestamp).localeCompare(String(b.timestamp)));
80
+ }
81
+
82
+ /* Gönderildi → kuyruktan sil */
83
+ function markSent(id) {
84
+ const list = load();
85
+ const idx = list.findIndex((x) => x.id === id);
86
+ if (idx >= 0) {
87
+ list.splice(idx, 1);
88
+ save();
89
+ }
90
+ }
91
+
92
+ /* Deneme başarısız → backoff planla; 5 denemeden sonra "failed" (gonderilemedi) */
93
+ function bumpRetry(id) {
94
+ const it = load().find((x) => x.id === id);
95
+ if (!it) return;
96
+ it.retry_count = (it.retry_count || 0) + 1;
97
+ if (it.retry_count >= MAX_RETRY) {
98
+ it.status = 'failed';
99
+ it.failedAt = new Date().toISOString();
100
+ } else {
101
+ it.nextAt = Date.now() + BACKOFF_MS[it.retry_count - 1];
102
+ }
103
+ save();
104
+ }
105
+
106
+ function pendingCount() {
107
+ return load().filter((x) => x.status === 'pending').length;
108
+ }
109
+
110
+ function stats() {
111
+ const l = load();
112
+ return {
113
+ pending: l.filter((x) => x.status === 'pending').length,
114
+ failed: l.filter((x) => x.status === 'failed').length,
115
+ total: l.length,
116
+ };
117
+ }
118
+
119
+ /* bağlantı geri gelince testler için */
120
+ function _reset() {
121
+ CACHE = null;
122
+ }
123
+
124
+ module.exports = { MAX_QUEUE, MAX_RETRY, BACKOFF_MS, add, due, markSent, bumpRetry, pendingCount, stats, _reset, file };
package/src/agent/pdf.js CHANGED
@@ -1,20 +1,20 @@
1
- 'use strict';
2
-
3
- /* Beast PDF yardımcısı: pdf-parse v2 ile düz metin çıkarımı.
4
- pdf-parse kendi pdfjs-dist@5.4.296 kopyasını kullanır (nested). Aynı process'te
5
- başka bir pdfjs sürümü yükleyen paket (örn. pdf-to-img) açılırsa globalThis
6
- worker cache çakışmasından "API version ... does not match Worker version"
7
- hatası çıkar — o yüzden render/OCR işleri AYRI node süreçlerinde yapılır
8
- (ayrıntı: pdf skill'i). */
9
-
10
- async function extract(buf) {
11
- const { PDFParse } = require('pdf-parse');
12
- const parser = new PDFParse({ data: new Uint8Array(buf) });
13
- try {
14
- return await parser.getText();
15
- } finally {
16
- await parser.destroy();
17
- }
18
- }
19
-
20
- module.exports = { extract };
1
+ 'use strict';
2
+
3
+ /* Beast PDF yardımcısı: pdf-parse v2 ile düz metin çıkarımı.
4
+ pdf-parse kendi pdfjs-dist@5.4.296 kopyasını kullanır (nested). Aynı process'te
5
+ başka bir pdfjs sürümü yükleyen paket (örn. pdf-to-img) açılırsa globalThis
6
+ worker cache çakışmasından "API version ... does not match Worker version"
7
+ hatası çıkar — o yüzden render/OCR işleri AYRI node süreçlerinde yapılır
8
+ (ayrıntı: pdf skill'i). */
9
+
10
+ async function extract(buf) {
11
+ const { PDFParse } = require('pdf-parse');
12
+ const parser = new PDFParse({ data: new Uint8Array(buf) });
13
+ try {
14
+ return await parser.getText();
15
+ } finally {
16
+ await parser.destroy();
17
+ }
18
+ }
19
+
20
+ module.exports = { extract };
@@ -1,133 +1,133 @@
1
- 'use strict';
2
-
3
- /* BEAST — deep_search: agentic araştırma katmanı.
4
- web_search snippet'te kalır; bu katman derine iner:
5
- 1) 1-4 sorgu varyantını PARALEL aratır (toplam süre ≈ tek arama)
6
- 2) Sonuçları URL bazlı tekilleştirip round-robin harmanlar (kaynak çeşitliliği)
7
- 3) İlk N sonucu GİZLİ gerçek tarayıcıda açıp tam metin okur
8
- (deps.readPage verilmişse; yoksa yalnız sonuç listesi döner)
9
- Saf modüldür: search/readPage enjekte edilir — test edilebilir. */
10
-
11
- /* İçerik okumada anlamsız/ikili dosyalar — tarayıcıda açmanın anlamı yok */
12
- const SKIP_FILE_RE = /\.(pdf|docx?|xlsx?|pptx?|zip|rar|7z|exe|dmg|apk|mp[34]|m4a|avi|mkv|webm|jpe?g|png|gif|webp|svg|ico|csv)([?#]|$)/i;
13
-
14
- /* URL anahtarı: scheme/www/slash/hash farklarını yok sayar — tekilleştirme için */
15
- function normKey(u) {
16
- try {
17
- const x = new URL(u);
18
- x.hash = '';
19
- let s = x.toString();
20
- s = s.replace(/^http:\/\//i, 'https://');
21
- s = s.replace(/^https:\/\/www\./i, 'https://');
22
- return s.replace(/\/+$/, '').toLowerCase();
23
- } catch {
24
- return String(u || '').toLowerCase();
25
- }
26
- }
27
-
28
- /* search() geri dönüşünü normalize eder: dizi YA DA {results:[...]} kabul;
29
- satır anahtarları title/url/snippet VEYA name/href/link/body/description olabilir */
30
- function normalizeRows(out) {
31
- if (!out) return [];
32
- const rows = Array.isArray(out) ? out : Array.isArray(out.results) ? out.results : [];
33
- return rows
34
- .map((r) => ({
35
- title: String((r && (r.title || r.name)) || '').replace(/\s+/g, ' ').trim(),
36
- url: String((r && (r.url || r.href || r.link)) || '').trim(),
37
- snippet: String((r && (r.snippet || r.body || r.description)) || '').replace(/\s+/g, ' ').trim(),
38
- }))
39
- .filter((r) => r.url && /^https?:\/\//i.test(r.url) && r.title);
40
- }
41
-
42
- /* Liste harmanı: her listenin i. elemanı sırayla alınır (round-robin) —
43
- tek motorun sonuçları öne çökmez, kaynak çeşitliliği korunur */
44
- function mergeResults(lists, limit) {
45
- const seen = new Set();
46
- const out = [];
47
- const max = lists.reduce((m, l) => Math.max(m, l.length), 0);
48
- for (let i = 0; i < max && out.length < limit; i++) {
49
- for (const list of lists) {
50
- if (out.length >= limit) break;
51
- const r = list[i];
52
- if (!r) continue;
53
- const key = normKey(r.url);
54
- if (seen.has(key)) continue;
55
- seen.add(key);
56
- out.push(r);
57
- }
58
- }
59
- return out;
60
- }
61
-
62
- /* Eşzamanlılığı sınırlı paralel map — hata tek elemanı bozar, diziyi bozmaz */
63
- function mapLimit(items, n, fn) {
64
- const out = new Array(items.length);
65
- let i = 0;
66
- const worker = async () => {
67
- while (i < items.length) {
68
- const idx = i++;
69
- try {
70
- out[idx] = await fn(items[idx], idx);
71
- } catch (e) {
72
- out[idx] = { ok: false, url: items[idx] && items[idx].url, error: String((e && e.message) || e) };
73
- }
74
- }
75
- };
76
- const workers = Array.from({ length: Math.max(1, Math.min(n, items.length)) }, worker);
77
- return Promise.all(workers).then(() => out);
78
- }
79
-
80
- async function deepSearch(args, deps, signal) {
81
- const a = args || {};
82
- let queries = a.queries;
83
- if (typeof queries === 'string') queries = [queries];
84
- if (!Array.isArray(queries) || !queries.length) {
85
- const q = String(a.query || '').trim();
86
- if (q) queries = [q];
87
- }
88
- queries = [...new Set((queries || []).map((s) => String(s || '').trim()).filter(Boolean))].slice(0, 4);
89
- if (!queries.length) return { ok: false, error: 'sorgu yok — queries: ["...", "..."] ver' };
90
- if (signal && signal.aborted) return { ok: false, error: 'iptal edildi', queries };
91
-
92
- const maxResults = Math.max(5, Math.min(25, Number(a.max_results) || 16));
93
- const readTop = Math.max(0, Math.min(6, Number(a.read_top != null ? a.read_top : (a.readTop != null ? a.readTop : 3))));
94
- const search = deps && typeof deps.search === 'function' ? deps.search : null;
95
- if (!search) return { ok: false, error: 'arama altyapısı yok', queries };
96
-
97
- /* 1) paralel arama — bir sorgu çökse diğerleri devam eder */
98
- const lists = await Promise.all(
99
- queries.map((q) =>
100
- Promise.resolve()
101
- .then(() => search(q))
102
- .then(normalizeRows)
103
- .catch(() => [])
104
- )
105
- );
106
- const results = mergeResults(lists, maxResults);
107
-
108
- /* 2) ilk N sonucu gizli tarayıcıda açıp oku (ikili dosyaları atla) */
109
- let pages = [];
110
- if (readTop && results.length && deps && typeof deps.readPage === 'function') {
111
- const targets = results.filter((r) => !SKIP_FILE_RE.test(r.url)).slice(0, readTop);
112
- if (targets.length) {
113
- pages = (await mapLimit(targets, 2, (r) => deps.readPage(r.url))).filter(Boolean);
114
- }
115
- }
116
- const anyPage = pages.some((p) => p && p.ok);
117
-
118
- return {
119
- ok: !!(results.length || anyPage),
120
- queries,
121
- results,
122
- ...(pages.length ? { pages } : {}),
123
- ...(results.length || anyPage
124
- ? {
125
- note: anyPage
126
- ? 'sayfalar gizli tarayıcıda açılıp okundu — excerpt\u2019ler pages içinde; bir sayfayı kullanıcıya da göstermek istersen browser_open ile panelde aç'
127
- : 'sonuç listesi hazır — tam metin okuma kapalı/başarısız; URL\u2019leri http_fetch ile okuyabilirsin',
128
- }
129
- : { error: 'tüm motorlar boş döndü — sorguları farklı ifade etmeyi dene' }),
130
- };
131
- }
132
-
133
- module.exports = { deepSearch, mergeResults, normalizeRows, normKey, mapLimit, SKIP_FILE_RE };
1
+ 'use strict';
2
+
3
+ /* BEAST — deep_search: agentic araştırma katmanı.
4
+ web_search snippet'te kalır; bu katman derine iner:
5
+ 1) 1-4 sorgu varyantını PARALEL aratır (toplam süre ≈ tek arama)
6
+ 2) Sonuçları URL bazlı tekilleştirip round-robin harmanlar (kaynak çeşitliliği)
7
+ 3) İlk N sonucu GİZLİ gerçek tarayıcıda açıp tam metin okur
8
+ (deps.readPage verilmişse; yoksa yalnız sonuç listesi döner)
9
+ Saf modüldür: search/readPage enjekte edilir — test edilebilir. */
10
+
11
+ /* İçerik okumada anlamsız/ikili dosyalar — tarayıcıda açmanın anlamı yok */
12
+ const SKIP_FILE_RE = /\.(pdf|docx?|xlsx?|pptx?|zip|rar|7z|exe|dmg|apk|mp[34]|m4a|avi|mkv|webm|jpe?g|png|gif|webp|svg|ico|csv)([?#]|$)/i;
13
+
14
+ /* URL anahtarı: scheme/www/slash/hash farklarını yok sayar — tekilleştirme için */
15
+ function normKey(u) {
16
+ try {
17
+ const x = new URL(u);
18
+ x.hash = '';
19
+ let s = x.toString();
20
+ s = s.replace(/^http:\/\//i, 'https://');
21
+ s = s.replace(/^https:\/\/www\./i, 'https://');
22
+ return s.replace(/\/+$/, '').toLowerCase();
23
+ } catch {
24
+ return String(u || '').toLowerCase();
25
+ }
26
+ }
27
+
28
+ /* search() geri dönüşünü normalize eder: dizi YA DA {results:[...]} kabul;
29
+ satır anahtarları title/url/snippet VEYA name/href/link/body/description olabilir */
30
+ function normalizeRows(out) {
31
+ if (!out) return [];
32
+ const rows = Array.isArray(out) ? out : Array.isArray(out.results) ? out.results : [];
33
+ return rows
34
+ .map((r) => ({
35
+ title: String((r && (r.title || r.name)) || '').replace(/\s+/g, ' ').trim(),
36
+ url: String((r && (r.url || r.href || r.link)) || '').trim(),
37
+ snippet: String((r && (r.snippet || r.body || r.description)) || '').replace(/\s+/g, ' ').trim(),
38
+ }))
39
+ .filter((r) => r.url && /^https?:\/\//i.test(r.url) && r.title);
40
+ }
41
+
42
+ /* Liste harmanı: her listenin i. elemanı sırayla alınır (round-robin) —
43
+ tek motorun sonuçları öne çökmez, kaynak çeşitliliği korunur */
44
+ function mergeResults(lists, limit) {
45
+ const seen = new Set();
46
+ const out = [];
47
+ const max = lists.reduce((m, l) => Math.max(m, l.length), 0);
48
+ for (let i = 0; i < max && out.length < limit; i++) {
49
+ for (const list of lists) {
50
+ if (out.length >= limit) break;
51
+ const r = list[i];
52
+ if (!r) continue;
53
+ const key = normKey(r.url);
54
+ if (seen.has(key)) continue;
55
+ seen.add(key);
56
+ out.push(r);
57
+ }
58
+ }
59
+ return out;
60
+ }
61
+
62
+ /* Eşzamanlılığı sınırlı paralel map — hata tek elemanı bozar, diziyi bozmaz */
63
+ function mapLimit(items, n, fn) {
64
+ const out = new Array(items.length);
65
+ let i = 0;
66
+ const worker = async () => {
67
+ while (i < items.length) {
68
+ const idx = i++;
69
+ try {
70
+ out[idx] = await fn(items[idx], idx);
71
+ } catch (e) {
72
+ out[idx] = { ok: false, url: items[idx] && items[idx].url, error: String((e && e.message) || e) };
73
+ }
74
+ }
75
+ };
76
+ const workers = Array.from({ length: Math.max(1, Math.min(n, items.length)) }, worker);
77
+ return Promise.all(workers).then(() => out);
78
+ }
79
+
80
+ async function deepSearch(args, deps, signal) {
81
+ const a = args || {};
82
+ let queries = a.queries;
83
+ if (typeof queries === 'string') queries = [queries];
84
+ if (!Array.isArray(queries) || !queries.length) {
85
+ const q = String(a.query || '').trim();
86
+ if (q) queries = [q];
87
+ }
88
+ queries = [...new Set((queries || []).map((s) => String(s || '').trim()).filter(Boolean))].slice(0, 4);
89
+ if (!queries.length) return { ok: false, error: 'sorgu yok — queries: ["...", "..."] ver' };
90
+ if (signal && signal.aborted) return { ok: false, error: 'iptal edildi', queries };
91
+
92
+ const maxResults = Math.max(5, Math.min(25, Number(a.max_results) || 16));
93
+ const readTop = Math.max(0, Math.min(6, Number(a.read_top != null ? a.read_top : (a.readTop != null ? a.readTop : 3))));
94
+ const search = deps && typeof deps.search === 'function' ? deps.search : null;
95
+ if (!search) return { ok: false, error: 'arama altyapısı yok', queries };
96
+
97
+ /* 1) paralel arama — bir sorgu çökse diğerleri devam eder */
98
+ const lists = await Promise.all(
99
+ queries.map((q) =>
100
+ Promise.resolve()
101
+ .then(() => search(q))
102
+ .then(normalizeRows)
103
+ .catch(() => [])
104
+ )
105
+ );
106
+ const results = mergeResults(lists, maxResults);
107
+
108
+ /* 2) ilk N sonucu gizli tarayıcıda açıp oku (ikili dosyaları atla) */
109
+ let pages = [];
110
+ if (readTop && results.length && deps && typeof deps.readPage === 'function') {
111
+ const targets = results.filter((r) => !SKIP_FILE_RE.test(r.url)).slice(0, readTop);
112
+ if (targets.length) {
113
+ pages = (await mapLimit(targets, 2, (r) => deps.readPage(r.url))).filter(Boolean);
114
+ }
115
+ }
116
+ const anyPage = pages.some((p) => p && p.ok);
117
+
118
+ return {
119
+ ok: !!(results.length || anyPage),
120
+ queries,
121
+ results,
122
+ ...(pages.length ? { pages } : {}),
123
+ ...(results.length || anyPage
124
+ ? {
125
+ note: anyPage
126
+ ? 'sayfalar gizli tarayıcıda açılıp okundu — excerpt\u2019ler pages içinde; bir sayfayı kullanıcıya da göstermek istersen browser_open ile panelde aç'
127
+ : 'sonuç listesi hazır — tam metin okuma kapalı/başarısız; URL\u2019leri http_fetch ile okuyabilirsin',
128
+ }
129
+ : { error: 'tüm motorlar boş döndü — sorguları farklı ifade etmeyi dene' }),
130
+ };
131
+ }
132
+
133
+ module.exports = { deepSearch, mergeResults, normalizeRows, normKey, mapLimit, SKIP_FILE_RE };