beast-agent 0.15.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 (62) hide show
  1. package/.env.example +7 -0
  2. package/LICENSE +21 -0
  3. package/README.md +94 -0
  4. package/assets/app.ico +0 -0
  5. package/assets/tray.png +0 -0
  6. package/bin/beast-agent.js +23 -0
  7. package/config.example.yaml +26 -0
  8. package/eng.traineddata +0 -0
  9. package/package.json +101 -0
  10. package/scripts/swap-electron.js +40 -0
  11. package/src/agent/bus.js +384 -0
  12. package/src/agent/computeruse.js +200 -0
  13. package/src/agent/config.js +284 -0
  14. package/src/agent/engine.js +3118 -0
  15. package/src/agent/kb.js +123 -0
  16. package/src/agent/llm.js +219 -0
  17. package/src/agent/logger.js +90 -0
  18. package/src/agent/memory.js +383 -0
  19. package/src/agent/pdf.js +20 -0
  20. package/src/agent/scripts/__pycache__/websearch.cpython-312.pyc +0 -0
  21. package/src/agent/scripts/news.py +113 -0
  22. package/src/agent/scripts/websearch.py +225 -0
  23. package/src/agent/skills.js +522 -0
  24. package/src/agent/tokens.js +39 -0
  25. package/src/agent/tools.js +911 -0
  26. package/src/agent/usage.js +125 -0
  27. package/src/agent/watchers.js +312 -0
  28. package/src/agent/watext.js +75 -0
  29. package/src/agent/whatsapp.js +494 -0
  30. package/src/cron.js +245 -0
  31. package/src/main.js +3622 -0
  32. package/src/preload.js +122 -0
  33. package/src/renderer/browserPreload.js +73 -0
  34. package/src/renderer/i18n.js +757 -0
  35. package/src/renderer/index.html +227 -0
  36. package/src/renderer/renderer.js +3250 -0
  37. package/src/renderer/style.css +1559 -0
  38. package/tests/approval.test.js +56 -0
  39. package/tests/bg-jobs.test.js +315 -0
  40. package/tests/bus.test.js +46 -0
  41. package/tests/computeruse-context.test.js +69 -0
  42. package/tests/cron.test.js +95 -0
  43. package/tests/engine.test.js +140 -0
  44. package/tests/eval.test.js +91 -0
  45. package/tests/fallout.test.js +188 -0
  46. package/tests/llm.test.js +56 -0
  47. package/tests/memory.test.js +33 -0
  48. package/tests/memoryloop.test.js +26 -0
  49. package/tests/notegen.test.js +63 -0
  50. package/tests/owner.test.js +32 -0
  51. package/tests/python.test.js +75 -0
  52. package/tests/scenarios.json +81 -0
  53. package/tests/sessioncode.test.js +50 -0
  54. package/tests/setup.js +9 -0
  55. package/tests/tokens.test.js +42 -0
  56. package/tests/tools.test.js +84 -0
  57. package/tests/usage.test.js +44 -0
  58. package/tests/v11.test.js +76 -0
  59. package/tests/watchers.test.js +206 -0
  60. package/tests/watext.test.js +53 -0
  61. package/tests/whatsapp.test.js +103 -0
  62. package/tests/wherewasi.test.js +40 -0
@@ -0,0 +1,911 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+ const https = require('https');
7
+ const { execFile } = require('child_process');
8
+ const { spawn } = require('child_process');
9
+
10
+ const MAX_CMD_OUTPUT = 16000;
11
+ const MAX_FILE_CHARS = 200000;
12
+
13
+ function truncateMiddle(s, cap) {
14
+ if (s.length <= cap) return s;
15
+ const half = Math.floor((cap - 32) / 2);
16
+ return (
17
+ s.slice(0, half) +
18
+ `\n... [${s.length - cap} chars truncated] ...\n` +
19
+ s.slice(s.length - half)
20
+ );
21
+ }
22
+
23
+ function runCommand(command, cwd, timeoutMs = 90000, signal) {
24
+ return new Promise((resolve) => {
25
+ let settled = false;
26
+ let out = '';
27
+ let err = '';
28
+ const finish = (code, killed) => {
29
+ if (settled) return;
30
+ settled = true;
31
+ clearTimeout(timer);
32
+ const text =
33
+ (out ? out.trim() : '') +
34
+ (err ? (out ? '\n[stderr] ' : '') + err.trim() : '');
35
+ resolve({
36
+ ok: !killed && code === 0,
37
+ output: truncateMiddle(text || '(no output)', MAX_CMD_OUTPUT),
38
+ code,
39
+ });
40
+ };
41
+
42
+ const child = spawn(
43
+ 'powershell.exe',
44
+ ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', command],
45
+ { cwd: cwd || process.cwd(), windowsHide: true, env: envWithPathPrefix() }
46
+ );
47
+
48
+ const timer = setTimeout(() => {
49
+ try {
50
+ spawn('taskkill', ['/pid', String(child.pid), '/T', '/F'], { windowsHide: true });
51
+ } catch {}
52
+ finish(null, true);
53
+ // append notice
54
+ err += '\n[beast] command timed out';
55
+ }, timeoutMs);
56
+
57
+ if (signal) {
58
+ if (signal.aborted) return finish(null, true);
59
+ signal.addEventListener('abort', () => {
60
+ try {
61
+ spawn('taskkill', ['/pid', String(child.pid), '/T', '/F'], { windowsHide: true });
62
+ } catch {}
63
+ finish(null, true);
64
+ }, { once: true });
65
+ }
66
+
67
+ child.stdout.setEncoding('utf8');
68
+ child.stderr.setEncoding('utf8');
69
+ child.stdout.on('data', (d) => {
70
+ out += d;
71
+ if (out.length > MAX_CMD_OUTPUT * 4) out = out.slice(-MAX_CMD_OUTPUT * 2);
72
+ });
73
+ child.stderr.on('data', (d) => {
74
+ err += d;
75
+ if (err.length > MAX_CMD_OUTPUT * 4) err = err.slice(-MAX_CMD_OUTPUT * 2);
76
+ });
77
+ child.on('error', (e) => finish(1, false) || void (err += String(e.message)));
78
+ child.on('close', (code) => finish(code, false));
79
+ });
80
+ }
81
+
82
+ function safeResolve(p, cwd) {
83
+ const abs = path.isAbsolute(p) ? p : path.join(cwd || process.cwd(), p);
84
+ return path.normalize(abs);
85
+ }
86
+
87
+ /* ---------- Python altyapısı (#18) ----------
88
+ Sıra: BEAST_PYTHON env → sistem python/python3/py -3 → gömülü runtime
89
+ (%APPDATA%\beast\py — yoksa TEK SEFERLİK kendisi indirip açar).
90
+ Böylece ajanlar makinede Python kurulmasa bile python_run kullanabilir. */
91
+
92
+ const PYTHON_EMBED_URL = 'https://www.python.org/ftp/python/3.12.8/python-3.12.8-embed-amd64.zip';
93
+ const PY_PROBE_TIMEOUT = 6000;
94
+
95
+ function beastAppDir() {
96
+ const base = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
97
+ return path.join(base, 'beast');
98
+ }
99
+
100
+ function pythonScriptsDir() {
101
+ const d = path.join(beastAppDir(), 'scripts');
102
+ fs.mkdirSync(d, { recursive: true });
103
+ return d;
104
+ }
105
+
106
+ function embeddedPythonExe() {
107
+ return path.join(beastAppDir(), 'py', 'python.exe');
108
+ }
109
+
110
+ /* Gömülü runtime kuruluysa klasörünü PATH önüne koy: ajanın run_command'ında
111
+ `python` / `python.exe` da çalışsın (makinede Python kurulmuş olmasa bile). */
112
+ function pythonPathPrefix() {
113
+ try {
114
+ const d = path.dirname(embeddedPythonExe());
115
+ if (fs.existsSync(path.join(d, 'python.exe'))) return d + path.delimiter;
116
+ } catch {}
117
+ return '';
118
+ }
119
+
120
+ function envWithPathPrefix() {
121
+ const base = { ...process.env };
122
+ base.PATH = pythonPathPrefix() + (process.env.PATH || process.env.Path || '');
123
+ return base;
124
+ }
125
+
126
+ /* paketlenmiş python scriptleri (src/agent/scripts) → kullanıcı scripts klasörüne tohumla */
127
+ function bundledScriptPath(name) {
128
+ return path.join(__dirname, 'scripts', name);
129
+ }
130
+
131
+ function seedScript(name, force = false) {
132
+ const src = bundledScriptPath(name);
133
+ try {
134
+ const dest = path.join(pythonScriptsDir(), name);
135
+ /* force: paket içindeki güncel sürüm her zaman kazanır (sistem scripti) */
136
+ if (fs.existsSync(src) && (force || !fs.existsSync(dest))) {
137
+ fs.copyFileSync(src, dest);
138
+ }
139
+ return dest;
140
+ } catch {
141
+ return path.join(pythonScriptsDir(), name);
142
+ }
143
+ }
144
+
145
+ /* #20 hızlı web arama: ddgs kütüphanesi varsa onu kullan; yoksa python çoklu-motor
146
+ paralel; o da olmazsa JS fallback. Sonuç boş gelirse (CAPTCHA/engel) ddgs'yi
147
+ TEK SEFERLİK arka planda kurup tekrar dener. */
148
+ function pyExec(exe, args, timeoutMs) {
149
+ return new Promise((resolve) => {
150
+ try {
151
+ execFile(exe, args, { timeout: timeoutMs, windowsHide: true }, (err, stdout, stderr) =>
152
+ resolve({ err, out: String(stdout || '') + String(stderr || '') })
153
+ );
154
+ } catch (e) {
155
+ resolve({ err: e, out: '' });
156
+ }
157
+ });
158
+ }
159
+
160
+ let _ddgsTried = null; // Promise<boolean> — oturum başına tek kurulum denemesi
161
+
162
+ function ddgsImportOk(probe) {
163
+ /* hızlı yerel kontrol: kütüphane import edilebiliyor mu (ağ gerektirmez) */
164
+ return pyExec(probe.exe, ['-c', 'import ddgs'], 15000).then((r) => !r.err);
165
+ }
166
+
167
+ /* pip ile TEK SEFERLİK kurulum dener; uzun sürebilir — await ETME, arka planda bırak */
168
+ function ensureDdgs(probe) {
169
+ if (_ddgsTried) return _ddgsTried;
170
+ _ddgsTried = (async () => {
171
+ try {
172
+ if (await ddgsImportOk(probe)) return true;
173
+ const pip = await pyExec(probe.exe, ['-m', 'pip', '--version'], 15000);
174
+ if (pip.err) return false; // gömülü python'da pip yok — sessizce vazgeç
175
+ const inst = await pyExec(
176
+ probe.exe,
177
+ ['-m', 'pip', 'install', '--quiet', '--disable-pip-version-check', 'ddgs'],
178
+ 120000
179
+ );
180
+ if (inst.err) return false;
181
+ return await ddgsImportOk(probe);
182
+ } catch {
183
+ return false;
184
+ }
185
+ })();
186
+ return _ddgsTried;
187
+ }
188
+
189
+ function parseWsOutput(output) {
190
+ return String(output || '')
191
+ .split('\n')
192
+ .map((l) => l.trim())
193
+ .filter((l) => l.startsWith('{'))
194
+ .map((l) => {
195
+ try { return JSON.parse(l); } catch { return null; }
196
+ })
197
+ .filter(Boolean)
198
+ .map((x) => ({ title: String(x.title || ''), url: String(x.url || ''), snippet: String(x.snippet || ''), engine: String(x.engine || '') }));
199
+ }
200
+
201
+ async function webSearchFast(query, { maxResults = 8, signal } = {}) {
202
+ try {
203
+ const probe = await ensurePython({ allowDownload: true, signal });
204
+ const script = seedScript('websearch.py', true);
205
+ let r = await runPy(probe.exe, script, ['--json', '--limit', String(maxResults), query], process.cwd(), 20000, signal);
206
+ let results = parseWsOutput(r.output);
207
+ if (!results.length) {
208
+ /* muhtemel CAPTCHA/engel — ddgs kuruluysa hemen onunla tekrar dene;
209
+ kurulu değilse kurulumu arka plana bırak (bu çağrıyı bekletmez) */
210
+ if (await ddgsImportOk(probe)) {
211
+ r = await runPy(probe.exe, script, ['--json', '--limit', String(maxResults), query], process.cwd(), 20000, signal);
212
+ results = parseWsOutput(r.output);
213
+ } else {
214
+ ensureDdgs(probe).catch(() => {});
215
+ }
216
+ } else if (!results.some((x) => x.engine === 'ddgs')) {
217
+ /* arama çalışıyor ama ddgs henüz yok — arka planda sessiz kur, sonraki aramalar hızlanır */
218
+ ensureDdgs(probe).catch(() => {});
219
+ }
220
+ if (results.length) {
221
+ return { ok: true, engine: results.some((x) => x.engine === 'ddgs') ? 'ddgs' : 'python-multi', query, results };
222
+ }
223
+ } catch {}
224
+ /* JS fallback (tek motor DDG) */
225
+ return webSearch(query, { maxResults, signal });
226
+ }
227
+
228
+ let _pyProbe = null;
229
+
230
+ function probeInterpreter(exe, prefix) {
231
+ return new Promise((resolve) => {
232
+ try {
233
+ execFile(exe, [...(prefix || []), '--version'], { timeout: PY_PROBE_TIMEOUT, windowsHide: true }, (err, stdout, stderr) => {
234
+ if (err) return resolve(null);
235
+ const v = String(stdout || stderr || '').trim();
236
+ resolve(/Python\s*3/i.test(v) ? { exe, prefix: prefix || [], source: 'system', version: v } : null);
237
+ });
238
+ } catch {
239
+ resolve(null);
240
+ }
241
+ });
242
+ }
243
+
244
+ async function findSystemPython() {
245
+ if (process.env.BEAST_PYTHON && fs.existsSync(process.env.BEAST_PYTHON)) {
246
+ return { exe: process.env.BEAST_PYTHON, prefix: [], source: 'env' };
247
+ }
248
+ for (const cand of [
249
+ ['python.exe', []],
250
+ ['python3.exe', []],
251
+ ['py.exe', ['-3']],
252
+ ]) {
253
+ const r = await probeInterpreter(cand[0], cand[1]);
254
+ if (r) return r;
255
+ }
256
+ return null;
257
+ }
258
+
259
+ function httpsGetBuffer(url, redirectsLeft = 4, signal) {
260
+ return new Promise((resolve, reject) => {
261
+ const req = https.get(url, { headers: { 'User-Agent': 'BeastAgent/1.0 (+python bootstrap)' } }, (res) => {
262
+ if ([301, 302, 303, 307, 308].includes(res.statusCode) && res.headers.location && redirectsLeft > 0) {
263
+ res.resume();
264
+ return resolve(httpsGetBuffer(new URL(res.headers.location, url).toString(), redirectsLeft - 1, signal));
265
+ }
266
+ if (res.statusCode !== 200) {
267
+ res.resume();
268
+ return reject(new Error(`indirme başarısız: HTTP ${res.statusCode}`));
269
+ }
270
+ const chunks = [];
271
+ let size = 0;
272
+ res.on('data', (c) => {
273
+ size += c.length;
274
+ if (size > 80 * 1024 * 1024) {
275
+ req.destroy(new Error('dosya çok büyük'));
276
+ return;
277
+ }
278
+ chunks.push(c);
279
+ });
280
+ res.on('end', () => resolve(Buffer.concat(chunks)));
281
+ res.on('error', reject);
282
+ });
283
+ req.on('error', reject);
284
+ if (signal) {
285
+ if (signal.aborted) return reject(new Error('iptal'));
286
+ signal.addEventListener('abort', () => req.destroy(new Error('iptal')), { once: true });
287
+ }
288
+ });
289
+ }
290
+
291
+ async function installEmbeddedPython(signal) {
292
+ const dest = path.join(beastAppDir(), 'py');
293
+ fs.mkdirSync(dest, { recursive: true });
294
+ const zipPath = path.join(os.tmpdir(), 'beast-py-embed.zip');
295
+ const buf = await httpsGetBuffer(PYTHON_EMBED_URL, 4, signal);
296
+ fs.writeFileSync(zipPath, buf);
297
+ const r = await runCommand(
298
+ `Expand-Archive -LiteralPath "${zipPath}" -DestinationPath "${dest}" -Force`,
299
+ null,
300
+ 180000
301
+ );
302
+ try { fs.unlinkSync(zipPath); } catch {}
303
+ if (!r.ok || !fs.existsSync(embeddedPythonExe())) {
304
+ throw new Error('gömülü python kurulamadı: ' + String(r.output || '').slice(0, 200));
305
+ }
306
+ /* site-packages yolunu aç — pip ile kurulabilen paketler için ön hazırlık */
307
+ try {
308
+ const pth = path.join(dest, 'python312._pth');
309
+ if (fs.existsSync(pth)) {
310
+ const cur = fs.readFileSync(pth, 'utf8');
311
+ if (!/import site/.test(cur)) fs.writeFileSync(pth, cur.replace(/\n*$/, '\nimport site\n'), 'utf8');
312
+ }
313
+ } catch {}
314
+ return { exe: embeddedPythonExe(), prefix: [], source: 'embedded', version: '3.12.8 (gömülü)' };
315
+ }
316
+
317
+ async function ensurePython({ allowDownload = true, signal } = {}) {
318
+ if (_pyProbe) {
319
+ try { fs.accessSync(_pyProbe.exe); return _pyProbe; } catch { _pyProbe = null; }
320
+ }
321
+ const emb = embeddedPythonExe();
322
+ if (fs.existsSync(emb)) {
323
+ _pyProbe = { exe: emb, prefix: [], source: 'embedded' };
324
+ return _pyProbe;
325
+ }
326
+ const sys = await findSystemPython();
327
+ if (sys) {
328
+ _pyProbe = sys;
329
+ return sys;
330
+ }
331
+ if (!allowDownload) throw new Error('python bulunamadı ve otomatik kurulum kapalı');
332
+ _pyProbe = await installEmbeddedPython(signal);
333
+ return _pyProbe;
334
+ }
335
+
336
+ function runPy(exe, scriptPath, cliArgs, cwd, timeoutMs = 120000, signal) {
337
+ return new Promise((resolve) => {
338
+ let settled = false;
339
+ let out = '';
340
+ let err = '';
341
+ const startedAt = Date.now();
342
+ const finish = (code, killed) => {
343
+ if (settled) return;
344
+ settled = true;
345
+ clearTimeout(timer);
346
+ const text =
347
+ (out ? out.trim() : '') +
348
+ (err ? (out ? '\n[stderr] ' : '') + err.trim() : '');
349
+ resolve({
350
+ ok: !killed && code === 0,
351
+ code,
352
+ ms: Date.now() - startedAt,
353
+ output: truncateMiddle(text || '(no output)', MAX_CMD_OUTPUT),
354
+ });
355
+ };
356
+
357
+ let child;
358
+ try {
359
+ child = spawn(exe, [scriptPath, ...cliArgs], {
360
+ cwd: cwd || process.cwd(),
361
+ windowsHide: true,
362
+ env: { ...envWithPathPrefix(), PYTHONUTF8: '1', PYTHONIOENCODING: 'utf-8' },
363
+ });
364
+ } catch (e) {
365
+ err += String(e.message);
366
+ return finish(1, false);
367
+ }
368
+
369
+ const timer = setTimeout(() => {
370
+ try { spawn('taskkill', ['/pid', String(child.pid), '/T', '/F'], { windowsHide: true }); } catch {}
371
+ finish(null, true);
372
+ err += '\n[beast] python timed out';
373
+ }, timeoutMs);
374
+
375
+ if (signal) {
376
+ if (signal.aborted) return finish(null, true);
377
+ signal.addEventListener('abort', () => {
378
+ try { spawn('taskkill', ['/pid', String(child.pid), '/T', '/F'], { windowsHide: true }); } catch {}
379
+ finish(null, true);
380
+ }, { once: true });
381
+ }
382
+
383
+ child.stdout.setEncoding('utf8');
384
+ child.stderr.setEncoding('utf8');
385
+ child.stdout.on('data', (d) => {
386
+ out += d;
387
+ if (out.length > MAX_CMD_OUTPUT * 4) out = out.slice(-MAX_CMD_OUTPUT * 2);
388
+ });
389
+ child.stderr.on('data', (d) => {
390
+ err += d;
391
+ if (err.length > MAX_CMD_OUTPUT * 4) err = err.slice(-MAX_CMD_OUTPUT * 2);
392
+ });
393
+ child.on('error', (e) => { err += String(e.message); finish(1, false); });
394
+ child.on('close', (code) => finish(code, false));
395
+ });
396
+ }
397
+
398
+ /* ---------- web araçları ---------- */
399
+
400
+ const UA =
401
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126 Safari/537.36 BeastAgent/0.2';
402
+
403
+ const PRIVATE_HOST_RE =
404
+ /^(localhost|.*\.local|127\.|10\.|192\.168\.|169\.254\.|0\.|::1|\[::1\]|f[cd][0-9a-f]{2}:)/i;
405
+
406
+ function assertPublicHttpUrl(raw) {
407
+ let u;
408
+ try {
409
+ u = new URL(String(raw || ''));
410
+ } catch {
411
+ throw new Error('geçersiz URL');
412
+ }
413
+ if (u.protocol !== 'http:' && u.protocol !== 'https:') {
414
+ throw new Error('yalnızca http/https desteklenir');
415
+ }
416
+ const host = u.hostname;
417
+ if (PRIVATE_HOST_RE.test(host)) {
418
+ throw new Error('yerel/ağ içi adreslere erişim engellendi');
419
+ }
420
+ if (/^172\.(1[6-9]|2\d|3[01])\./.test(host)) {
421
+ throw new Error('yerel/ağ içi adreslere erişim engellendi');
422
+ }
423
+ if ((u.username || u.password)) {
424
+ throw new Error('URL içinde kullanıcı bilgisi desteklenmez');
425
+ }
426
+ return u.toString();
427
+ }
428
+
429
+ const ENTITIES = {
430
+ amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", nbsp: ' ', mdash: '—', ndash: '–',
431
+ hellip: '…', rsquo: '\u2019', lsquo: '\u2018', ldquo: '\u201c', rdquo: '\u201d',
432
+ };
433
+
434
+ function decodeEntities(s) {
435
+ return String(s || '')
436
+ .replace(/&#x([0-9a-f]+);/gi, (_m, h) => {
437
+ try { return String.fromCodePoint(parseInt(h, 16)); } catch { return ''; }
438
+ })
439
+ .replace(/&#(\d+);/g, (_m, d) => {
440
+ try { return String.fromCodePoint(Number(d)); } catch { return ''; }
441
+ })
442
+ .replace(/&([a-z]+);/gi, (m, name) => ENTITIES[name.toLowerCase()] ?? m);
443
+ }
444
+
445
+ function htmlToText(html) {
446
+ let t = String(html || '');
447
+ t = t.replace(/<!--[\s\S]*?-->/g, ' ');
448
+ t = t.replace(/<(script|style|noscript|svg|head)[\s\S]*?<\/\1>/gi, ' ');
449
+ t = t.replace(/<\/(p|div|li|h[1-6]|tr|section|article|br)>/gi, '\n');
450
+ t = t.replace(/<br\s*\/?>/gi, '\n');
451
+ t = t.replace(/<[^>]+>/g, ' ');
452
+ t = decodeEntities(t);
453
+ t = t.replace(/[ \t\f\v]+/g, ' ');
454
+ t = t.replace(/\s*\n\s*/g, '\n').replace(/\n{3,}/g, '\n\n');
455
+ return t.trim();
456
+ }
457
+
458
+ /* DuckDuckGo HTML sonuç ayrıştırıcı — test edilebilir saf fonksiyon */
459
+ function parseDdgResults(html, limit = 8) {
460
+ const out = [];
461
+ const re =
462
+ /<a\s+([^>]*\bresult__a\b[^>]*)>([\s\S]*?)<\/a>([\s\S]*?)(?=<a\s+[^>]*\bresult__a\b|$)/g;
463
+ let m;
464
+ while ((m = re.exec(html)) && out.length < limit) {
465
+ const attrs = m[1] || '';
466
+ const hrefM = attrs.match(/href="([^"]+)"/) || attrs.match(/href='([^']+)'/);
467
+ let href = hrefM ? hrefM[1] : '';
468
+ try {
469
+ const u = new URL(href, 'https://duckduckgo.com');
470
+ const uddg = u.searchParams.get('uddg');
471
+ href = uddg ? decodeURIComponent(uddg) : u.toString();
472
+ } catch {}
473
+ const title = decodeEntities(m[2] || '').replace(/<[^>]+>/g, '').trim();
474
+ const seg = m[3] || '';
475
+ const snM = seg.match(/<a\s+[^>]*result__snippet[^>]*>([\s\S]*?)<\/a>/);
476
+ const snippet = snM ? decodeEntities(snM[1]).replace(/<[^>]+>/g, '').trim() : '';
477
+ if (!title || !/^https?:\/\//i.test(href)) continue;
478
+ if (out.some((r) => r.url === href)) continue;
479
+ out.push({ title, url: href, snippet });
480
+ }
481
+ return out;
482
+ }
483
+
484
+ async function fetchWithTimeout(url, opts = {}, timeoutMs = 20000, signal) {
485
+ const ctrl = new AbortController();
486
+ const timer = setTimeout(() => ctrl.abort(new Error('zaman aşımı')), timeoutMs);
487
+ const onAbort = () => ctrl.abort(new Error('iptal'));
488
+ if (signal) {
489
+ if (signal.aborted) return Promise.reject(new Error('iptal'));
490
+ signal.addEventListener('abort', onAbort, { once: true });
491
+ }
492
+ try {
493
+ return await fetch(url, { ...opts, signal: ctrl.signal, redirect: 'follow' });
494
+ } finally {
495
+ clearTimeout(timer);
496
+ if (signal) signal.removeEventListener('abort', onAbort);
497
+ }
498
+ }
499
+
500
+ async function webSearch(query, { maxResults = 8, signal } = {}) {
501
+ const q = String(query || '').trim().slice(0, 400);
502
+ if (!q) return { ok: false, error: 'boş sorgu' };
503
+ const res = await fetchWithTimeout(
504
+ 'https://html.duckduckgo.com/html/',
505
+ {
506
+ method: 'POST',
507
+ headers: {
508
+ 'Content-Type': 'application/x-www-form-urlencoded',
509
+ 'User-Agent': UA,
510
+ Accept: 'text/html',
511
+ },
512
+ body: new URLSearchParams({ q }).toString(),
513
+ },
514
+ 20000,
515
+ signal
516
+ );
517
+ if (!res.ok) throw new Error(`DDG HTTP ${res.status}`);
518
+ const html = await res.text();
519
+ const results = parseDdgResults(html, Math.min(Math.max(Number(maxResults) || 8, 1), 12));
520
+ if (!results.length) {
521
+ return { ok: true, query: q, results, note: 'sonuç yok ya da DDG yanıt biçimi değişti' };
522
+ }
523
+ return { ok: true, query: q, count: results.length, results };
524
+ }
525
+
526
+ const MAX_FETCH_CHARS = 9000;
527
+
528
+ async function httpFetch(url, { maxChars = MAX_FETCH_CHARS, signal } = {}) {
529
+ const safe = assertPublicHttpUrl(url);
530
+ const res = await fetchWithTimeout(
531
+ safe,
532
+ { headers: { 'User-Agent': UA, Accept: 'text/html,text/plain,application/json;q=0.9,*/*;q=0.5' } },
533
+ 20000,
534
+ signal
535
+ );
536
+ const ctype = String(res.headers.get('content-type') || '');
537
+ if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`);
538
+ const isTexty =
539
+ /text\/|json|xml|javascript/i.test(ctype) || ctype === '';
540
+ if (!isTexty) {
541
+ return { ok: true, url: safe, status: res.status, contentType: ctype, note: 'ikili/metin olmayan içerik indirilmedi' };
542
+ }
543
+ const cap = 400000;
544
+ let body = await res.text();
545
+ if (body.length > cap) body = body.slice(0, cap);
546
+ const text = /html/i.test(ctype) ? htmlToText(body) : body;
547
+ const truncated = text.length > maxChars;
548
+ return {
549
+ ok: true,
550
+ url: safe,
551
+ status: res.status,
552
+ contentType: ctype,
553
+ truncated,
554
+ content: text.slice(0, Math.max(1000, Math.min(Number(maxChars) || MAX_FETCH_CHARS, 50000))),
555
+ };
556
+ }
557
+
558
+ const definitions = [
559
+ {
560
+ type: 'function',
561
+ function: {
562
+ name: 'run_command',
563
+ description:
564
+ 'Run a PowerShell command on the user\'s Windows machine in the workspace directory and return combined stdout/stderr. Use for shell tasks, builds, git, installs, etc.',
565
+ parameters: {
566
+ type: 'object',
567
+ properties: {
568
+ command: { type: 'string', description: 'PowerShell command line to execute' },
569
+ timeout_ms: { type: 'number', description: 'Optional timeout in ms (default 90000)' },
570
+ },
571
+ required: ['command'],
572
+ },
573
+ },
574
+ },
575
+ {
576
+ type: 'function',
577
+ function: {
578
+ name: 'read_file',
579
+ description: 'Read a text file. Returns its content (truncated if huge).',
580
+ parameters: {
581
+ type: 'object',
582
+ properties: {
583
+ path: { type: 'string' },
584
+ },
585
+ required: ['path'],
586
+ },
587
+ },
588
+ },
589
+ {
590
+ type: 'function',
591
+ function: {
592
+ name: 'write_file',
593
+ description: 'Write (or overwrite) a text file with content. Creates parent directories.',
594
+ parameters: {
595
+ type: 'object',
596
+ properties: {
597
+ path: { type: 'string' },
598
+ content: { type: 'string' },
599
+ },
600
+ required: ['path', 'content'],
601
+ },
602
+ },
603
+ },
604
+ {
605
+ type: 'function',
606
+ function: {
607
+ name: 'list_dir',
608
+ description: 'List entries of a directory with sizes and types.',
609
+ parameters: {
610
+ type: 'object',
611
+ properties: {
612
+ path: { type: 'string', description: 'Defaults to workspace root' },
613
+ },
614
+ required: [],
615
+ },
616
+ },
617
+ },
618
+ {
619
+ type: 'function',
620
+ function: {
621
+ name: 'web_search',
622
+ description:
623
+ 'FAST web search with an automatic chain: built-in browser first (real Chromium searching GOOGLE directly with AI Mode — no bot protection; the response may include an `ai` field holding Google\'s own AI answer, ideal for "who/what is X" questions), then Python multi-engine (ddgs / DuckDuckGo+Bing+Mojeek parallel), then Exa API as last resort if a key is configured (Ayarlar → Web Arama). Returns {ai?, results[{title,url,snippet}]}. Use when fresh or external info is needed; skip for things you already know.',
624
+ parameters: {
625
+ type: 'object',
626
+ properties: {
627
+ query: { type: 'string' },
628
+ max_results: { type: 'number', description: '1-12, default 8' },
629
+ },
630
+ required: ['query'],
631
+ },
632
+ },
633
+ },
634
+ {
635
+ type: 'function',
636
+ function: {
637
+ name: 'http_fetch',
638
+ description:
639
+ 'Fetch one URL and return its text content (HTML converted to plain text, JSON as-is). Local/private network addresses are blocked.',
640
+ parameters: {
641
+ type: 'object',
642
+ properties: {
643
+ url: { type: 'string', format: 'uri' },
644
+ max_chars: { type: 'number', description: 'default 9000' },
645
+ },
646
+ required: ['url'],
647
+ },
648
+ },
649
+ },
650
+ {
651
+ type: 'function',
652
+ function: {
653
+ name: 'python_run',
654
+ description:
655
+ "Run Python (system interpreter, or Beast auto-installs a portable embedded runtime on first use). Either pass inline `code`, or `script` = filename inside the Beast scripts library (%APPDATA%\\beast\\scripts — e.g. news.py haber toplayıcı) or an absolute path. Use for scraping, RSS/haber toplama, veri analizi, regex/parsing işleri. stdout+stderr returned; scripts should print results.",
656
+ parameters: {
657
+ type: 'object',
658
+ properties: {
659
+ code: { type: 'string', description: 'Inline Python source (use this OR script)' },
660
+ script: { type: 'string', description: 'Script name in beast scripts dir or absolute path' },
661
+ args: {
662
+ type: 'array',
663
+ items: { type: 'string' },
664
+ description: 'CLI arguments passed to the script, e.g. ["--limit","5","--json"]',
665
+ },
666
+ timeout_ms: { type: 'number', description: 'default 120000' },
667
+ },
668
+ },
669
+ },
670
+ },
671
+ ];
672
+
673
+ async function exec(name, args, ctx) {
674
+ const cwd = ctx.cwd;
675
+ try {
676
+ switch (name) {
677
+ case 'run_command': {
678
+ const t = Number(args.timeout_ms) || 90000;
679
+ const r = await runCommand(String(args.command || ''), cwd, t, ctx.signal);
680
+ return JSON.stringify(r);
681
+ }
682
+ case 'read_file': {
683
+ const abs = safeResolve(String(args.path || ''), cwd);
684
+ const st = fs.statSync(abs);
685
+ if (st.size > MAX_FILE_CHARS * 2) {
686
+ return JSON.stringify({ ok: false, error: `file too large (${st.size} bytes)` });
687
+ }
688
+ /* PDF: pdf-parse varsa metin çıkar (v2 class API — bkz. src/agent/pdf.js) */
689
+ if (/\.pdf$/i.test(abs)) {
690
+ let extract = null;
691
+ try { extract = require('./pdf').extract; } catch {}
692
+ if (!extract) return JSON.stringify({ ok: false, error: 'pdf okuma yok — npm i pdf-parse' });
693
+ try {
694
+ const r = await extract(fs.readFileSync(abs));
695
+ let content = String((r && r.text) || '').trim();
696
+ const pages = (r && r.total) || '?';
697
+ const truncated = content.length > MAX_FILE_CHARS;
698
+ if (truncated) content = content.slice(0, MAX_FILE_CHARS);
699
+ if (!content.trim()) return JSON.stringify({ ok: false, error: 'pdf metni çıkarılamadı (taranmış görsel olabilir)' });
700
+ return JSON.stringify({ ok: true, path: abs, pages, truncated, content });
701
+ } catch (e2) {
702
+ return JSON.stringify({ ok: false, error: 'pdf okuma hata: ' + String((e2 && e2.message) || e2) });
703
+ }
704
+ }
705
+ let content = fs.readFileSync(abs, 'utf8');
706
+ const truncated = content.length > MAX_FILE_CHARS;
707
+ if (truncated) content = content.slice(0, MAX_FILE_CHARS);
708
+ return JSON.stringify({ ok: true, path: abs, truncated, content });
709
+ }
710
+ case 'write_file': {
711
+ const abs = safeResolve(String(args.path || ''), cwd);
712
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
713
+ fs.writeFileSync(abs, String(args.content ?? ''), 'utf8');
714
+ return JSON.stringify({ ok: true, path: abs, bytes: Buffer.byteLength(String(args.content ?? '')) });
715
+ }
716
+ case 'list_dir': {
717
+ const abs = args.path ? safeResolve(String(args.path), cwd) : cwd;
718
+ const entries = fs.readdirSync(abs, { withFileTypes: true }).slice(0, 500);
719
+ const rows = entries.map((e) => {
720
+ try {
721
+ const st = fs.statSync(path.join(abs, e.name));
722
+ return `${e.isDirectory() ? 'd' : '-'} ${String(st.size).padStart(10)} ${e.name}`;
723
+ } catch {
724
+ return `- ? ${e.name}`;
725
+ }
726
+ });
727
+ return JSON.stringify({ ok: true, path: abs, count: rows.length, entries: rows.join('\n') });
728
+ }
729
+ case 'web_search': {
730
+ const q = String(args.query || '');
731
+ const n = Number(args.max_results) || 8;
732
+ /* önce ücretsiz python çoklu-motor; Exa SON ÇARE (dahili tarayıcı zincirin
733
+ üstünde engine._execTool'da çalışır) */
734
+ let r = await webSearchFast(q, { maxResults: n, signal: ctx.signal });
735
+ if (!r || !r.ok || !(r.results || []).length) {
736
+ const exa = await exaSearch(q, n, ctx.signal);
737
+ if (exa) return JSON.stringify(exa);
738
+ }
739
+ return JSON.stringify(r);
740
+ }
741
+ case 'python_run': {
742
+ const t0 = Date.now();
743
+ /* önce parametreler — interpreter araması gerektirmeyen hatalar erken dönsün */
744
+ const cliArgs = Array.isArray(args.args)
745
+ ? args.args.map(String)
746
+ : args.args
747
+ ? [String(args.args)]
748
+ : [];
749
+ let scriptPath;
750
+ let madeTemp = false;
751
+ if (String(args.code || '').trim()) {
752
+ scriptPath = path.join(
753
+ os.tmpdir(),
754
+ `beast-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 7)}.py`
755
+ );
756
+ fs.writeFileSync(scriptPath, String(args.code), 'utf8');
757
+ madeTemp = true;
758
+ } else if (String(args.script || '').trim()) {
759
+ const raw = String(args.script);
760
+ scriptPath = path.isAbsolute(raw) ? raw : path.join(pythonScriptsDir(), raw);
761
+ } else {
762
+ return JSON.stringify({ ok: false, error: 'code ya da script parametresi gerekli' });
763
+ }
764
+ if (!fs.existsSync(scriptPath)) {
765
+ return JSON.stringify({
766
+ ok: false,
767
+ error: `script bulunamadı: ${scriptPath}`,
768
+ hint: `scripts klasörü: ${pythonScriptsDir()}`,
769
+ });
770
+ }
771
+ let probe;
772
+ try {
773
+ probe = await ensurePython({ allowDownload: ctx.allowDownload !== false, signal: ctx.signal });
774
+ } catch (e) {
775
+ return JSON.stringify({
776
+ ok: false,
777
+ error: String((e && e.message) || e),
778
+ hint: 'BEAST_PYTHON env ile interpreter yolu verilebilir; ya da python.org kur',
779
+ });
780
+ }
781
+ const r = await runPy(probe.exe, scriptPath, cliArgs, cwd, Number(args.timeout_ms) || 120000, ctx.signal);
782
+ if (madeTemp) { try { fs.unlinkSync(scriptPath); } catch {} }
783
+ return JSON.stringify({ ok: r.ok, python: probe.source, exitCode: r.code, ms: Date.now() - t0, output: r.output });
784
+ }
785
+ case 'http_fetch': {
786
+ const r = await httpFetch(String(args.url || ''), {
787
+ maxChars: Number(args.max_chars) || MAX_FETCH_CHARS,
788
+ signal: ctx.signal,
789
+ });
790
+ return JSON.stringify(r);
791
+ }
792
+ default:
793
+ return JSON.stringify({ ok: false, error: `unknown tool ${name}` });
794
+ }
795
+ } catch (e) {
796
+ return JSON.stringify({ ok: false, error: String(e && e.message ? e.message : e) });
797
+ }
798
+ }
799
+
800
+ /* ---------- Exa (opsiyonel ücretli arama) ----------
801
+ Ayarlar → Web Arama'dan anahtar girilirse web_search ÖNCE Exa'yı dener;
802
+ hata/sonuç yoksa ücretsiz python çoklu-motor devreye girer. */
803
+ let _exaKey = null;
804
+
805
+ function setExaKey(key) {
806
+ _exaKey = String(key || '').trim() || null;
807
+ }
808
+
809
+ async function exaSearch(query, maxResults, signal) {
810
+ if (!_exaKey) return null;
811
+ try {
812
+ const sig =
813
+ signal && typeof AbortSignal !== 'undefined' && AbortSignal.any
814
+ ? AbortSignal.any([signal, AbortSignal.timeout(20000)])
815
+ : AbortSignal.timeout(20000);
816
+ const r = await fetch('https://api.exa.ai/search', {
817
+ method: 'POST',
818
+ headers: { 'x-api-key': _exaKey, 'Content-Type': 'application/json' },
819
+ body: JSON.stringify({
820
+ query,
821
+ numResults: Math.max(1, Math.min(12, Number(maxResults) || 8)),
822
+ contents: { text: { maxCharacters: 1200 } },
823
+ }),
824
+ signal: sig,
825
+ });
826
+ if (!r.ok) return null;
827
+ const j = await r.json();
828
+ const results = (j.results || [])
829
+ .map((x) => ({
830
+ title: String(x.title || ''),
831
+ url: String(x.url || ''),
832
+ snippet: String(x.text || '').replace(/\s+/g, ' ').slice(0, 400),
833
+ engine: 'exa',
834
+ }))
835
+ .filter((x) => x.url && x.title);
836
+ if (!results.length) return null;
837
+ return { ok: true, engine: 'exa', query, results };
838
+ } catch {
839
+ return null;
840
+ }
841
+ }
842
+
843
+ /* ---------- TinyFish (ücretsiz arama; anahtar girilirse ZİNCİRİN BAŞI) ----------
844
+ GET https://api.search.tinyfish.ai?query=... · Header: X-API-Key
845
+ Ayarlar → Web Arama'dan anahtar girilirse web_search ÖNCE TinyFish'i dener;
846
+ hata/sonuç yoksa eski zincir (tarayıcı → python çoklu-motor → Exa) devam eder. */
847
+ let _tinyfishKey = null;
848
+
849
+ function setTinyfishKey(key) {
850
+ _tinyfishKey = String(key || '').trim() || null;
851
+ }
852
+
853
+ async function tinyfishSearch(query, maxResults, signal) {
854
+ if (!_tinyfishKey) return null;
855
+ try {
856
+ const sig =
857
+ signal && typeof AbortSignal !== 'undefined' && AbortSignal.any
858
+ ? AbortSignal.any([signal, AbortSignal.timeout(20000)])
859
+ : AbortSignal.timeout(20000);
860
+ const r = await fetch(
861
+ `https://api.search.tinyfish.ai?query=${encodeURIComponent(String(query || ''))}`,
862
+ {
863
+ headers: {
864
+ 'X-API-Key': _tinyfishKey,
865
+ 'X-TF-Request-Origin': 'api',
866
+ 'X-TF-Client-Name': 'tinyfish-api-key-page',
867
+ },
868
+ signal: sig,
869
+ }
870
+ );
871
+ if (!r.ok) return null;
872
+ const j = await r.json();
873
+ const results = (j.results || [])
874
+ .map((x) => ({
875
+ title: String(x.title || ''),
876
+ url: String(x.url || ''),
877
+ snippet: String(x.snippet || '').replace(/\s+/g, ' ').slice(0, 400),
878
+ engine: 'tinyfish',
879
+ }))
880
+ .filter((x) => x.url && x.title);
881
+ if (!results.length) return null;
882
+ return { ok: true, engine: 'tinyfish', query, results: results.slice(0, Math.max(1, Math.min(12, Number(maxResults) || 8))) };
883
+ } catch {
884
+ return null;
885
+ }
886
+ }
887
+
888
+ module.exports = {
889
+ definitions,
890
+ exec,
891
+ runCommand,
892
+ assertPublicHttpUrl,
893
+ parseDdgResults,
894
+ htmlToText,
895
+ webSearch,
896
+ httpFetch,
897
+ webSearchFast,
898
+ seedScript,
899
+ bundledScriptPath,
900
+ setExaKey,
901
+ exaSearch,
902
+ setTinyfishKey,
903
+ tinyfishSearch,
904
+ /* python altyapısı */
905
+ ensurePython,
906
+ findSystemPython,
907
+ installEmbeddedPython,
908
+ pythonScriptsDir,
909
+ embeddedPythonExe,
910
+ runPy,
911
+ };