beast-agent 1.7.0 → 1.9.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.
package/README.md CHANGED
@@ -24,6 +24,8 @@ Beast Agent is a personal AI agent that runs on your machine and connects to **a
24
24
  - **Slash commands** — `/new`, `/open`, `/change`, `/think`, `/rule`, `/allow`, `/block`, `/backup`, `/approve` and more
25
25
  - **Event center** — IMAP IDLE email watching, file-change watching, price feed (Binance), webhook inputs
26
26
  - **Cron + watchers** — scheduled tasks, file/web/battery watchers
27
+ - **MCP desteği** — Model Context Protocol server bağla (filesystem, git, fetch, memory + binlerce topluluk server'ı); araçlar `mcp__server__tool` adıyla modele açılır, Ayarlar → MCP'den yönetilir
28
+ - **Skills** — `%APPDATA%\beast\skills\<ad>\SKILL.md` ile ajanına yetenek öğret; Superpowers metodoloji paketi (planlama, TDD, paralel ajan disiplini, doğrulama) builtin gelir
27
29
  - **Web search chain** — TinyFish (free, used first if a key is set) → built-in browser (direct Google) → python multi-engine (DDG/Bing/Mojeek) → Exa
28
30
  - **Approval gate** — optional confirmation for risky actions (commands / file deletion-modification): default OFF (everything free), when ON the agent asks (`/approve`, `/approve always`, `/deny`)
29
31
  - **Provider-based limits** — per-provider max input token limit with context compression
@@ -87,6 +87,18 @@ if (process.argv[2] === 'update') {
87
87
  process.exit(0);
88
88
  }
89
89
 
90
+ /* SearXNG: yerel arama motoru — kur + arka planda başlat (127.0.0.1:8888)
91
+ beast searxng → gerekirse kur, başlat
92
+ beast searxng status → durum
93
+ beast searxng stop → kulla */
94
+ if (process.argv[2] === 'searxng') {
95
+ require('../src/agent/searxng').cli(process.argv.slice(3)).catch((e) => {
96
+ console.error('\u2717 ' + String((e && e.message) || e));
97
+ process.exit(1);
98
+ });
99
+ return;
100
+ }
101
+
90
102
  /* detached başlatıcı + kendini onaran electron:
91
103
  taze makinalarda npm postinstall sırasında electron binary indirmesi
92
104
  sessizce başarısız olmuş olabilir → burada otomatik tamir edilir. */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "beast-agent",
3
3
  "productName": "Beast Agent",
4
- "version": "1.7.0",
4
+ "version": "1.9.0",
5
5
  "description": "Ultra-fast local agent shell for Windows.",
6
6
  "author": "algokodcom (AlgoKod)",
7
7
  "license": "MIT",
@@ -1,4 +1,4 @@
1
- 'use strict';
1
+ 'use strict';
2
2
 
3
3
  /* Beast engine: sessions, frugal context, streaming tool loop.
4
4
  v2: token-bazlı bağlam bütçesi (kullanım kalibrasyonlu), paralel tool
@@ -14,6 +14,7 @@ const agentdefs = require('./agentdefs');
14
14
  const memory = require('./memory');
15
15
  const mem0 = require('./mem0');
16
16
  const skills = require('./skills');
17
+ const mcp = require('./mcp');
17
18
  const { estTokens, estMsgTokens } = require('./tokens');
18
19
  const log = require('./logger');
19
20
 
@@ -1519,7 +1520,7 @@ class Engine {
1519
1520
  'Tarayıcı eylemlerinin yanıtındaki recent günlüğü ve navigated bilgisini takip et; eylem yanıtları zaten taze snapshot içerir — refler tutarsız görünürse yeni snapshot al.',
1520
1521
  '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.',
1521
1522
  '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.',
1522
- '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 Obscura stealth tarayıcı + TinyFish + Python çoklu-motor destekli), script gerektiğinde python_run yaz.',
1523
+ '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 (SearXNG + stealth TLS + TinyFish + Python çoklu-motor destekli), script gerektiğinde python_run yaz.',
1523
1524
  '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).',
1524
1525
  '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.',
1525
1526
  'Bir oturumda 3+ kez memory_write yaptıysan iş bitince memory_hygiene çağır (duplike/eskime temizliği).',
@@ -2828,6 +2829,10 @@ class Engine {
2828
2829
  }
2829
2830
 
2830
2831
  async _chatTurn(session, signal, onDelta, toolsList = TOOLS) {
2832
+ /* MCP: %APPDATA%\beast\mcp.json'daki etkin serverların araçlarını şema listesine ekle
2833
+ (bağlı değilse lazy bağlanır; kapalıysa liste değişmez; Beast Code paneli hızlı
2834
+ ilk-token sözü için MCP'siz kalır) */
2835
+ if (!session || !session.bcCode) toolsList = await mcp.mergeTools(toolsList);
2831
2836
  /* opencode agent.ts port: özel ajan tanımı — prompt/model/araç/steps */
2832
2837
  const adef = this._agentDefFor(session, !!session.bgJob);
2833
2838
  /* Beast Code: todo_write açıklaması "3+ adım" kısıtı içerir ve model küçük
@@ -3450,8 +3455,8 @@ class Engine {
3450
3455
  /* OTOMATİK SKİLL SİSTEMİ: kurulu skill listesi de prompta girer —
3451
3456
  ajan "eski skillden daha kolay/better yol buldum" diyebilmesin, KARAR VERİP
3452
3457
  skilli GÜNCELLESİN. action: create (yeni) | update (mevcutu iyileştir) | none */
3453
- const skills = require('./skills');
3454
- const existing = skills
3458
+ const skills = require('./skills');
3459
+ const existing = skills
3455
3460
  .scan()
3456
3461
  .map((s) => `- ${s.name}: ${(s.description || '').slice(0, 100)}`)
3457
3462
  .join('\n');
@@ -3685,10 +3690,10 @@ class Engine {
3685
3690
 
3686
3691
  /* web_search zinciri — web_search VE deep_search aynı zinciri kullanır.
3687
3692
  Sıra + aç/kapa Ayarlar → Web Arama'dan değiştirilir (tools.searchChainWeb):
3688
- varsayılan: 1) dahili tarayıcı (DİREK GOOGLE) 2) Obscura (stealth headless
3689
- DuckDuckGo; otomatik kurulur, varsayılan AKTİF) 3) TinyFish (anahtar
3690
- girildiyse) 4) python çoklu-motor. Tarayıcı CAPTCHA/trafik verirse 10 dk
3691
- atlanır — sıradaki motor hemen devreye girer. */
3693
+ varsayılan: 1) SearXNG (yerel, ayaktaysa otomatik öne alınır) 2) stealth
3694
+ (curl_cffi Chrome TLS taklidi DDG html) 3) dahili tarayıcı (DİREK
3695
+ GOOGLE) 4) TinyFish (anahtar girildiyse) 5) python çoklu-motor. Tarayıcı
3696
+ CAPTCHA/trafik verirse 10 dk atlanır — sıradaki motor hemen devreye girer. */
3692
3697
  _webSearchChain(q, n, sessionId, signal) {
3693
3698
  const browserUsable = this.browser && typeof this.browser.search === 'function';
3694
3699
  return tools.searchChainWeb(q, n, {
@@ -3812,7 +3817,7 @@ class Engine {
3812
3817
  /* onay kapısı: riskli araçta dış onay bekle; reddedilirse araç çalışmaz.
3813
3818
  "always" onaylı araçlar doğrudan geçer. */
3814
3819
  if (
3815
- Engine.RISKY_TOOLS.has(name) &&
3820
+ (Engine.RISKY_TOOLS.has(name) || String(name).startsWith('mcp__')) &&
3816
3821
  !this.alwaysAllowTools.has(name) &&
3817
3822
  this.approvals && typeof this.approvals.request === 'function'
3818
3823
  ) {
@@ -3831,6 +3836,10 @@ class Engine {
3831
3836
  }
3832
3837
  /* GERİ ALMA GÜNLÜĞÜ: dosya yazımından ÖNCE eski içerik kayda geçer */
3833
3838
  this._journalBefore(sessionId, name, args);
3839
+ /* MCP: dış server araçları (mcp__<server>__<tool>) — tools/call'a köprülenir */
3840
+ if (String(name).startsWith('mcp__')) {
3841
+ return JSON.stringify(await mcp.call(name, args, signal));
3842
+ }
3834
3843
  if (name === 'memory_write') {
3835
3844
  /* bot oturumu → botun KENDİ hafıza store'una yaz (global Beast hafızasına değil).
3836
3845
  mem0 açıkken: hash+semantic dedup'lı store'a gider, MEMORY.md aynası güncellenir. */
@@ -4086,7 +4095,7 @@ class Engine {
4086
4095
  if (name === 'deep_search') {
4087
4096
  /* agentic derin araştırma: çoklu sorgu + gizli tarayıcıda sayfa okuma.
4088
4097
  ARAMA zinciri web_search ile BİREBİR AYNI — sıralı zincir
4089
- (tarayıcıObscura → TinyFish → python; Ayarlar'dan değiştirilir). */
4098
+ (SearXNGstealthtarayıcı → TinyFish → python; Ayarlar'dan değiştirilir).. */
4090
4099
  try {
4091
4100
  emitSafe(this, sessionId, { type: 'status', status: 'derin araştırma: çoklu sorgu + gizli sayfa okuma' });
4092
4101
  } catch {}
package/src/agent/llm.js CHANGED
@@ -249,6 +249,44 @@ async function streamOnce(sel, body, { signal, onDelta, onRetry } = {}, omitReas
249
249
  signal.addEventListener('abort', onAbort, { once: true });
250
250
  }
251
251
  try {
252
+ /* tek satır SSE olayını işle — hem okuma döngüsü hem SON FLUSH kullanır */
253
+ const handleLine = (line) => {
254
+ line = line.trim();
255
+ if (!line.startsWith('data:')) return;
256
+ const data = line.slice(5).trim();
257
+ if (!data || data === '[DONE]') return;
258
+
259
+ let json;
260
+ try {
261
+ json = JSON.parse(data);
262
+ } catch {
263
+ return;
264
+ }
265
+
266
+ if (json.usage) state.usage = json.usage;
267
+ const ch = json.choices && json.choices[0];
268
+ if (!ch) return;
269
+ if (ch.finish_reason) state.finishReason = ch.finish_reason;
270
+ const d = ch.delta || {};
271
+ if (d.content) {
272
+ state.content += d.content;
273
+ onDelta && onDelta(d.content, state.content);
274
+ }
275
+ if (d.reasoning_content) state.reasoning += d.reasoning_content;
276
+ if (d.reasoning) state.reasoning += d.reasoning;
277
+ for (const tc of d.tool_calls || []) {
278
+ const i = typeof tc.index === 'number' ? tc.index : state.toolCalls.length;
279
+ while (state.toolCalls.length <= i) {
280
+ state.toolCalls.push({ id: '', type: 'function', function: { name: '', arguments: '' } });
281
+ }
282
+ if (tc.id) state.toolCalls[i].id = tc.id;
283
+ if (tc.function) {
284
+ if (tc.function.name) state.toolCalls[i].function.name += tc.function.name;
285
+ if (tc.function.arguments) state.toolCalls[i].function.arguments += tc.function.arguments;
286
+ }
287
+ }
288
+ };
289
+
252
290
  while (true) {
253
291
  const { done, value } = await reader.read();
254
292
  if (signal && signal.aborted) {
@@ -261,44 +299,18 @@ async function streamOnce(sel, body, { signal, onDelta, onRetry } = {}, omitReas
261
299
 
262
300
  let idx;
263
301
  while ((idx = buf.indexOf('\n')) !== -1) {
264
- const line = buf.slice(0, idx).trim();
302
+ const line = buf.slice(0, idx);
265
303
  buf = buf.slice(idx + 1);
266
- if (!line.startsWith('data:')) continue;
267
- const data = line.slice(5).trim();
268
- if (!data || data === '[DONE]') continue;
269
-
270
- let json;
271
- try {
272
- json = JSON.parse(data);
273
- } catch {
274
- continue;
275
- }
276
-
277
- if (json.usage) state.usage = json.usage;
278
- const ch = json.choices && json.choices[0];
279
- if (!ch) continue;
280
- if (ch.finish_reason) state.finishReason = ch.finish_reason;
281
- const d = ch.delta || {};
282
- if (d.content) {
283
- state.content += d.content;
284
- onDelta && onDelta(d.content, state.content);
285
- }
286
- if (d.reasoning_content) state.reasoning += d.reasoning_content;
287
- if (d.reasoning) state.reasoning += d.reasoning;
288
- for (const tc of d.tool_calls || []) {
289
- const i = typeof tc.index === 'number' ? tc.index : state.toolCalls.length;
290
- while (state.toolCalls.length <= i) {
291
- state.toolCalls.push({ id: '', type: 'function', function: { name: '', arguments: '' } });
292
- }
293
- if (tc.id) state.toolCalls[i].id = tc.id;
294
- if (tc.function) {
295
- if (tc.function.name) state.toolCalls[i].function.name += tc.function.name;
296
- if (tc.function.arguments) state.toolCalls[i].function.arguments += tc.function.arguments;
297
- }
298
- }
304
+ handleLine(line);
299
305
  }
300
306
  }
301
307
 
308
+ /* SON FLUSH — KRİTİK: akış son parçası '\n' ile bitmeyebilir.
309
+ Okuma bitince kalan buf'u Decoder'dan boşaltıp işlemezsek
310
+ cevabın SON kelimesi/cümlesi kaybolur (SSE sonu öyle gelir). */
311
+ buf += decoder.decode();
312
+ if (buf.trim()) handleLine(buf);
313
+
302
314
  return { ...state };
303
315
  } finally {
304
316
  if (signal && onAbort) {
@@ -0,0 +1,427 @@
1
+ 'use strict';
2
+
3
+ /* Beast MCP (Model Context Protocol) istemcisi — el yazımı minimal, sıfır bağımlılık.
4
+ stdio transport: JSON-RPC 2.0, satır-bazlı (newline-delimited).
5
+
6
+ %APPDATA%\beast\mcp.json:
7
+ {
8
+ "servers": {
9
+ "fetch": { "command": "uvx", "args": ["mcp-server-fetch"], "enabled": true },
10
+ "git": { "command": "uvx", "args": ["mcp-server-git", "--repository", "C:/repo"],
11
+ "enabled": true, "tools": ["git_status", "git_log"], "timeoutMs": 60000 }
12
+ }
13
+ }
14
+
15
+ Tool'lar modele `mcp__<server>__<tool>` adıyla açılır; çağrı tools/call'a gider.
16
+ Server çökerse 3 dk cooldown'a girer, sonra otomatik yeniden denenir. */
17
+
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+ const { spawn } = require('child_process');
21
+ const { beastRoot } = require('./memory');
22
+ const log = require('./logger');
23
+
24
+ const INIT_TIMEOUT_MS = 15000;
25
+ const LIST_TIMEOUT_MS = 15000;
26
+ const CALL_TIMEOUT_MS = 120000;
27
+ const COOLDOWN_MS = 3 * 60 * 1000;
28
+ const MAX_TOOLS_PER_SERVER = 40;
29
+ const MAX_TOOLS_TOTAL = 80;
30
+ const MAX_DESC_LEN = 220;
31
+
32
+ /* server adı → { cfg, proc, nextId, pending:Map, tools:[], status, lastError } */
33
+ const conns = new Map();
34
+ /* başarısız server adı → yeniden deneme zamanı */
35
+ const downUntil = new Map();
36
+
37
+ /* ---------- config ---------- */
38
+
39
+ function configPath() {
40
+ return path.join(beastRoot(), 'mcp.json');
41
+ }
42
+
43
+ let cfgCache = { mtime: -1, cfg: { servers: {} } };
44
+
45
+ function readConfig(force) {
46
+ try {
47
+ const p = configPath();
48
+ const st = fs.statSync(p);
49
+ if (!force && st.mtimeMs === cfgCache.mtime) return cfgCache.cfg;
50
+ const parsed = JSON.parse(fs.readFileSync(p, 'utf8'));
51
+ const cfg = { servers: {} };
52
+ if (parsed && typeof parsed.servers === 'object') {
53
+ for (const [name, s] of Object.entries(parsed.servers)) {
54
+ if (!s || typeof s !== 'object' || !s.command) continue;
55
+ cfg.servers[sanitizeName(name)] = {
56
+ command: String(s.command),
57
+ args: Array.isArray(s.args) ? s.args.map(String) : [],
58
+ env: s.env && typeof s.env === 'object' ? Object.fromEntries(Object.entries(s.env).map(([k, v]) => [String(k), String(v)])) : {},
59
+ enabled: s.enabled !== false,
60
+ tools: Array.isArray(s.tools) ? s.tools.map(String) : null,
61
+ timeoutMs: Number(s.timeoutMs) > 1000 ? Math.min(Number(s.timeoutMs), 600000) : CALL_TIMEOUT_MS,
62
+ };
63
+ }
64
+ }
65
+ cfgCache = { mtime: st.mtimeMs, cfg };
66
+ return cfg;
67
+ } catch {
68
+ /* dosya yok/bozuk → boş yapı (mtime -1 kalır; dosya oluşturulunca yeniden okunur) */
69
+ try {
70
+ const st = fs.statSync(configPath());
71
+ cfgCache = { mtime: st.mtimeMs, cfg: { servers: {} } };
72
+ } catch {}
73
+ return { servers: {} };
74
+ }
75
+ }
76
+
77
+ function saveConfig(cfg) {
78
+ const p = configPath();
79
+ fs.mkdirSync(path.dirname(p), { recursive: true });
80
+ fs.writeFileSync(p, JSON.stringify(cfg, null, 2) + '\n');
81
+ cfgCache = { mtime: -1, cfg }; /* sonraki okuma diskten gelsin */
82
+ }
83
+
84
+ function sanitizeName(name) {
85
+ return String(name || '')
86
+ .toLowerCase()
87
+ .replace(/[^a-z0-9_-]+/g, '-')
88
+ .replace(/^-+|-+$/g, '')
89
+ .slice(0, 32) || 'server';
90
+ }
91
+
92
+ /* ---------- süreç yaşam döngüsü ---------- */
93
+
94
+ function spawnServer(cfg) {
95
+ /* Windows: npx/uvx gibi .cmd sarmalayıcıları düz spawn ile başlamaz —
96
+ cmd /d /s /c üzerinden, argümanlar elle tırnaklanarak geç. */
97
+ const quote = (t) => (/[\s"]/u.test(t) ? '"' + t.replace(/"/g, '\\"') + '"' : t);
98
+ const line = [cfg.command, ...cfg.args].map(quote).join(' ');
99
+ const child = spawn(line, {
100
+ shell: true,
101
+ windowsHide: true,
102
+ stdio: ['pipe', 'pipe', 'pipe'],
103
+ env: { ...process.env, ...(cfg.env || {}) },
104
+ });
105
+ return child;
106
+ }
107
+
108
+ function connFor(name) {
109
+ return conns.get(name);
110
+ }
111
+
112
+ function killConn(name) {
113
+ const c = conns.get(name);
114
+ if (!c) return;
115
+ conns.delete(name);
116
+ try {
117
+ if (c.timer) clearTimeout(c.timer);
118
+ for (const [, p] of c.pending) {
119
+ try { p.reject(new Error('MCP server kapatıldı')); } catch {}
120
+ }
121
+ c.pending.clear();
122
+ try { c.proc.stdin.end(); } catch {}
123
+ try { c.proc.kill(); } catch {}
124
+ } catch {}
125
+ }
126
+
127
+ function request(conn, method, params, timeoutMs, signal) {
128
+ return new Promise((resolve, reject) => {
129
+ const id = conn.nextId++;
130
+ const timer = setTimeout(() => {
131
+ conn.pending.delete(id);
132
+ reject(new Error(`MCP ${method} zaman aşımı (${Math.round(timeoutMs / 1000)}s)`));
133
+ }, timeoutMs);
134
+ const onAbort = () => {
135
+ clearTimeout(timer);
136
+ conn.pending.delete(id);
137
+ reject(new Error('iptal edildi'));
138
+ };
139
+ if (signal) {
140
+ if (signal.aborted) return onAbort();
141
+ signal.addEventListener('abort', onAbort, { once: true });
142
+ }
143
+ conn.pending.set(id, {
144
+ resolve: (v) => { clearTimeout(timer); if (signal) signal.removeEventListener('abort', onAbort); resolve(v); },
145
+ reject: (e) => { clearTimeout(timer); if (signal) signal.removeEventListener('abort', onAbort); reject(e); },
146
+ });
147
+ try {
148
+ conn.proc.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n');
149
+ } catch (e) {
150
+ clearTimeout(timer);
151
+ conn.pending.delete(id);
152
+ reject(e);
153
+ }
154
+ });
155
+ }
156
+
157
+ function handleLine(conn, line) {
158
+ if (!line || !line.trim()) return;
159
+ let msg;
160
+ try { msg = JSON.parse(line); } catch { return; }
161
+ if (msg.id !== undefined && msg.id !== null && conn.pending.has(msg.id)) {
162
+ const p = conn.pending.get(msg.id);
163
+ conn.pending.delete(msg.id);
164
+ if (msg.error) p.reject(new Error(String(msg.error.message || JSON.stringify(msg.error)).slice(0, 300)));
165
+ else p.resolve(msg.result);
166
+ return;
167
+ }
168
+ /* bildirimler (id'siz) — şimdilik yok sayılır */
169
+ }
170
+
171
+ function startServer(name, cfg) {
172
+ killConn(name);
173
+ const conn = {
174
+ cfg,
175
+ proc: null,
176
+ nextId: 1,
177
+ pending: new Map(),
178
+ tools: [],
179
+ status: 'starting',
180
+ lastError: '',
181
+ stdout: '',
182
+ };
183
+ conns.set(name, conn);
184
+ try {
185
+ conn.proc = spawnServer(cfg);
186
+ } catch (e) {
187
+ conn.status = 'down';
188
+ conn.lastError = String((e && e.message) || e).slice(0, 200);
189
+ downUntil.set(name, Date.now() + COOLDOWN_MS);
190
+ return Promise.resolve(false);
191
+ }
192
+ conn.proc.stdout.setEncoding('utf8');
193
+ conn.proc.stdout.on('data', (chunk) => {
194
+ conn.stdout += chunk;
195
+ let idx;
196
+ while ((idx = conn.stdout.indexOf('\n')) >= 0) {
197
+ const line = conn.stdout.slice(0, idx);
198
+ conn.stdout = conn.stdout.slice(idx + 1);
199
+ try { handleLine(conn, line); } catch {}
200
+ }
201
+ });
202
+ conn.proc.stderr.setEncoding('utf8');
203
+ conn.proc.stderr.on('data', () => {}); /* server logları — yok sayılır */
204
+ const fail = (err) => {
205
+ conn.status = 'down';
206
+ conn.lastError = String((err && err.message) || err).slice(0, 200);
207
+ conn.tools = [];
208
+ downUntil.set(name, Date.now() + COOLDOWN_MS);
209
+ for (const [, p] of conn.pending) { try { p.reject(new Error(conn.lastError)); } catch {} }
210
+ conn.pending.clear();
211
+ try { conn.proc.kill(); } catch {}
212
+ };
213
+ conn.proc.on('error', fail);
214
+ conn.proc.on('exit', (code) => {
215
+ if (conn.status === 'up') {
216
+ fail(new Error(`MCP server '${name}' beklenmedik çıkış yaptı (kod ${code})`));
217
+ } else if (conn.status === 'starting') {
218
+ fail(new Error(`MCP server '${name}' başlamadan kapandı (kod ${code})`));
219
+ }
220
+ });
221
+ return (async () => {
222
+ try {
223
+ await request(conn, 'initialize', {
224
+ protocolVersion: '2024-11-05',
225
+ capabilities: {},
226
+ clientInfo: { name: 'beast-agent', version: '1.9.0' },
227
+ }, INIT_TIMEOUT_MS);
228
+ try {
229
+ conn.proc.stdin.write(JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' }) + '\n');
230
+ } catch {}
231
+ const listed = await request(conn, 'tools/list', {}, LIST_TIMEOUT_MS);
232
+ const tools = Array.isArray(listed && listed.tools) ? listed.tools : [];
233
+ conn.tools = tools.slice(0, MAX_TOOLS_PER_SERVER).map((t) => ({
234
+ name: String(t.name || ''),
235
+ description: String(t.description || '').slice(0, MAX_DESC_LEN),
236
+ inputSchema: t.inputSchema && typeof t.inputSchema === 'object' ? t.inputSchema : { type: 'object', properties: {} },
237
+ }));
238
+ conn.status = 'up';
239
+ downUntil.delete(name);
240
+ log.info(`[mcp] '${name}' bağlı — ${conn.tools.length} araç`);
241
+ return true;
242
+ } catch (e) {
243
+ fail(e);
244
+ log.warn(`[mcp] '${name}' bağlanamadı: ${conn.lastError}`);
245
+ return false;
246
+ }
247
+ })();
248
+ }
249
+
250
+ /* Tüm etkin serverlara bağlan (zaten bağlılar hariç; cooldown'dakiler atlanır).
251
+ Toplam `waitMs`'ten uzun beklemez — hangisi hazır değilse bu turda öyle kalır. */
252
+ async function ensureAll(waitMs = 8000) {
253
+ const cfg = readConfig();
254
+ const kicks = [];
255
+ for (const [name, s] of Object.entries(cfg.servers)) {
256
+ if (!s.enabled) continue;
257
+ if (conns.has(name) || (downUntil.get(name) || 0) > Date.now()) continue;
258
+ kicks.push(startServer(name, s));
259
+ }
260
+ if (!kicks.length) return;
261
+ await Promise.race([
262
+ Promise.allSettled(kicks),
263
+ new Promise((r) => setTimeout(r, waitMs)),
264
+ ]);
265
+ }
266
+
267
+ /* ---------- şemalar ---------- */
268
+
269
+ function toolFullNames(cfg, serverName, tools) {
270
+ let list = tools;
271
+ if (cfg.tools && cfg.tools.length) {
272
+ const allow = new Set(cfg.tools);
273
+ list = tools.filter((t) => allow.has(t.name));
274
+ }
275
+ return list;
276
+ }
277
+
278
+ function toolSchemas() {
279
+ const cfg = readConfig();
280
+ const out = [];
281
+ for (const [name, conn] of conns) {
282
+ if (conn.status !== 'up' || !conn.tools.length) continue;
283
+ const s = cfg.servers[name] || {};
284
+ const list = toolFullNames(s, name, conn.tools);
285
+ for (const t of list) {
286
+ out.push({
287
+ type: 'function',
288
+ function: {
289
+ name: `mcp__${name}__${t.name}`,
290
+ description: `[mcp:${name}] ${t.description}`.trim(),
291
+ parameters: t.inputSchema,
292
+ },
293
+ });
294
+ }
295
+ }
296
+ return out.slice(0, MAX_TOOLS_TOTAL);
297
+ }
298
+
299
+ /* engine._chatTurn başında çağrılır: MCP araç şemalarını listeye ekler */
300
+ async function mergeTools(toolsList) {
301
+ try {
302
+ if (!readConfig().servers || !Object.keys(readConfig().servers).length) return toolsList;
303
+ if (toolsList.some((t) => t && t.function && String(t.function.name).startsWith('mcp__'))) return toolsList;
304
+ await ensureAll();
305
+ const extra = toolSchemas();
306
+ return extra.length ? toolsList.concat(extra) : toolsList;
307
+ } catch {
308
+ return toolsList;
309
+ }
310
+ }
311
+
312
+ /* ---------- çağrı ---------- */
313
+
314
+ function parseFullName(fullName) {
315
+ const m = String(fullName || '').match(/^mcp__([a-z0-9_-]{1,32})__([A-Za-z0-9_.-]{1,64})$/);
316
+ return m ? { server: m[1], tool: m[2] } : null;
317
+ }
318
+
319
+ function extractText(result) {
320
+ if (!result) return '';
321
+ const content = Array.isArray(result.content) ? result.content : [];
322
+ const parts = [];
323
+ for (const c of content) {
324
+ if (!c || typeof c !== 'object') continue;
325
+ if (c.type === 'text' && typeof c.text === 'string') parts.push(c.text);
326
+ else if (c.type === 'resource') parts.push(JSON.stringify(c.resource || {}).slice(0, 4000));
327
+ else parts.push(JSON.stringify(c).slice(0, 2000));
328
+ }
329
+ return parts.join('\n').slice(0, 60000);
330
+ }
331
+
332
+ async function call(fullName, args, signal) {
333
+ const parsed = parseFullName(fullName);
334
+ if (!parsed) return { ok: false, error: `geçersiz MCP araç adı: ${fullName}` };
335
+ const { server, tool } = parsed;
336
+ const cfg = readConfig();
337
+ const scfg = cfg.servers[server];
338
+ if (!scfg || !scfg.enabled) return { ok: false, error: `MCP server '${server}' kapalı` };
339
+ let conn = conns.get(server);
340
+ if (!conn || conn.status !== 'up') {
341
+ if ((downUntil.get(server) || 0) > Date.now()) {
342
+ return { ok: false, error: `MCP server '${server}' şu an erişilemiyor (${conn ? conn.lastError : 'cooldown'}) — az sonra yeniden dene` };
343
+ }
344
+ await ensureAll(15000);
345
+ conn = conns.get(server);
346
+ }
347
+ if (!conn || conn.status !== 'up') {
348
+ return { ok: false, error: `MCP server '${server}' bağlı değil` };
349
+ }
350
+ if (scfg.tools && scfg.tools.length && !scfg.tools.includes(tool)) {
351
+ return { ok: false, error: `araç '${tool}' server '${server}' için yetkili listede değil` };
352
+ }
353
+ try {
354
+ const result = await request(conn, 'tools/call', { name: tool, arguments: args || {} }, scfg.timeoutMs || CALL_TIMEOUT_MS, signal);
355
+ const text = extractText(result);
356
+ if (result && result.isError) return { ok: false, error: text.slice(0, 4000) || 'MCP araç hatası' };
357
+ return { ok: true, result: text };
358
+ } catch (e) {
359
+ const msg = String((e && e.message) || e);
360
+ /* ölü süreç → sonraki turda yeniden doğsun */
361
+ if (conn.status !== 'up') killConn(server);
362
+ return { ok: false, error: `MCP çağrısı başarısız: ${msg.slice(0, 300)}` };
363
+ }
364
+ }
365
+
366
+ /* ---------- yönetim (settings UI) ---------- */
367
+
368
+ function status() {
369
+ const cfg = readConfig(true);
370
+ const now = Date.now();
371
+ const servers = [];
372
+ for (const [name, s] of Object.entries(cfg.servers)) {
373
+ const conn = conns.get(name);
374
+ const down = (downUntil.get(name) || 0) > now;
375
+ servers.push({
376
+ name,
377
+ command: s.command,
378
+ args: s.args,
379
+ enabled: !!s.enabled,
380
+ tools: s.tools || null,
381
+ timeoutMs: s.timeoutMs,
382
+ state: !s.enabled ? 'disabled' : conn ? conn.status : down ? 'down' : 'idle',
383
+ toolCount: conn ? conn.tools.length : 0,
384
+ toolNames: conn ? conn.tools.map((t) => t.name) : [],
385
+ lastError: conn ? conn.lastError : '',
386
+ });
387
+ }
388
+ return { path: configPath(), servers };
389
+ }
390
+
391
+ /* Tek serverı yeniden başlat (settings "yenile" düğmesi) */
392
+ async function refresh(name) {
393
+ const cfg = readConfig(true);
394
+ const s = cfg.servers[sanitizeName(name)];
395
+ if (!s || !s.enabled) return false;
396
+ downUntil.delete(name);
397
+ await startServer(sanitizeName(name), s);
398
+ return true;
399
+ }
400
+
401
+ function stopAll() {
402
+ for (const name of Array.from(conns.keys())) killConn(name);
403
+ }
404
+
405
+ /* test/ayalar: tüm bağlantı ve cooldown durumunu sıfırla */
406
+ function _reset() {
407
+ stopAll();
408
+ downUntil.clear();
409
+ }
410
+
411
+ process.on('exit', stopAll);
412
+
413
+ module.exports = {
414
+ configPath,
415
+ readConfig,
416
+ saveConfig,
417
+ mergeTools,
418
+ toolSchemas,
419
+ call,
420
+ status,
421
+ refresh,
422
+ stopAll,
423
+ _reset,
424
+ parseFullName,
425
+ extractText,
426
+ sanitizeName,
427
+ };
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env python3
2
+ # Beast stealth search - Obscura yaklasimi: Chrome TLS parmak izi taklidi.
3
+ # curl_cffi (curl-impersonate) ile html.duckduckgo.com'a gercek Chrome kimligiyle girer.
4
+ # stdout: ham DDG HTML - ayristirma JS tarafinda (tools.parseDdgResults) yapilir.
5
+ # Kullanim: stealthsearch.py "<url-encoded-sorgu>"
6
+ import sys
7
+ import urllib.parse
8
+
9
+
10
+ def main():
11
+ if len(sys.argv) < 2:
12
+ return
13
+ q = urllib.parse.quote_plus(sys.argv[1])
14
+ try:
15
+ from curl_cffi import requests
16
+ except ImportError:
17
+ return # kurulu degil - JS tarafi sessizce atlar
18
+ try:
19
+ r = requests.get(
20
+ "https://html.duckduckgo.com/html/?q=" + q,
21
+ impersonate="chrome",
22
+ headers={"Accept-Language": "tr-TR,tr;q=0.9,en;q=0.8"},
23
+ timeout=15,
24
+ )
25
+ sys.stdout.write(r.text or "")
26
+ except Exception:
27
+ return
28
+
29
+
30
+ main()