beast-agent 1.8.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
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "beast-agent",
3
3
  "productName": "Beast Agent",
4
- "version": "1.8.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",
@@ -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
 
@@ -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');
@@ -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. */
@@ -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
+ };
@@ -110,6 +110,7 @@ Get-Content "$env:APPDATA\\beast\\sessions\\bg-jobs.json" | ConvertFrom-Json | %
110
110
  | \`bots.json\` | Bot kayıt defteri — max 5 bot, ilk kayıt admin 'Beast' (silinemez). Bot adı HARFLE başlamak ZORUNDA |
111
111
  | \`bots/<id>/\` | Botun izole klasörü: config.json, memory.md, yetkiler.json, logs/ |
112
112
  | \`whitelist.json\`, \`wa-auth\\\`, \`wa-chats.json\`, \`wa.log\` | WhatsApp izin listesi, Baileys auth (SİLME), sohbet eşlemesi, log |
113
+ | \`mcp.json\` | MCP server tanımları — dış araçlar (Ayarlar → MCP'den düzenlenir; araçlar modele \`mcp__<server>__<tool>\` adıyla açılır, çağrı onay kapısından geçer, çöken server 3 dk sonra otomatik yeniden denenir) |
113
114
  | \`cron.json\`, \`watchers.json\`, \`bus.json\` | Zamanlanmış görevler, web/batarya izleyicileri, olay abonelikleri |
114
115
  | \`scripts\\\` | Python scriptleri (websearch.py, news.py) |
115
116
  | \`searxng\\settings.yml\` | SearXNG yerel arama motoru ayarları (127.0.0.1:8888 — \`beast searxng\` ile kurulur/başlatılır) |
@@ -139,7 +140,7 @@ Get-Content "$env:APPDATA\\beast\\sessions\\bg-jobs.json" | ConvertFrom-Json | %
139
140
  /help · /version · /new · /open <kod> · /sessions · /stop · /start · /restart · /change [n] · /model <isim> · /think 0-5 · /clear · /notes · /rule <metin> · /rules · /notify on|off · /screenshot · /approve [always] · /deny · /update [now] · /usage · /backup · /status · /skills
140
141
 
141
142
  ## AYAR SEKMELERİ
142
- Provider · Fallout · Skills · Paralel Ajanlar · TTS · E-posta · Entegrasyonlar · Web Arama (Obscura kurulum + arama sırası) · Olaylar · Cron · Maliyet · Loglar · Panel · Limitler · Güvenlik · Güncelleme
143
+ Provider · Fallout · Skills · Paralel Ajanlar · TTS · E-posta · Entegrasyonlar · Web Arama · MCP · Olaylar · Cron · Maliyet · Loglar · Panel · Limitler · Güvenlik · Güncelleme
143
144
 
144
145
  ## PARALEL AJAN HIZLI BİLGİ
145
146
  - CEO modu: konuşan ajan iş YAPMAZ, run_background/run_background_many ile devreder; görev tanımı kendine yeterli olmalı (ajan CEO bağlamını GÖREMEZ).
package/src/main.js CHANGED
@@ -151,6 +151,7 @@ function startNpmUpdateWatch() {
151
151
  setInterval(check, 6 * 60 * 60 * 1000);
152
152
  }
153
153
  const toolsMod = require('./agent/tools');
154
+ const mcpMod = require('./agent/mcp');
154
155
  const { htmlToText, setSearchChain, setTinyfishKey } = toolsMod;
155
156
  /* OpenCode köprüsü KALDIRILDI: Beast Code artık tamamen BEAST motoruyla
156
157
  çalışır — opencode'in döngü mantığı (compaction, prune, cache disiplini,
@@ -2972,6 +2973,7 @@ app.whenReady().then(() => {
2972
2973
  app.isQuitting = true;
2973
2974
  flushBrowserStorage(); // x.com/google oturumları (cookies) diske yazılsın
2974
2975
  try { toolsMod.disposeShellSessions(); } catch {} // kalıcı shell oturumlarını kapat
2976
+ try { require('./agent/mcp').stopAll(); } catch {} // MCP server süreçlerini kapat
2975
2977
  });
2976
2978
 
2977
2979
  if (process.argv.includes('--smoke')) {
@@ -4977,6 +4979,41 @@ ipcMain.handle('settings:get', () => {
4977
4979
  return out;
4978
4980
  });
4979
4981
 
4982
+ /* ---------------- MCP IPC ---------------- */
4983
+
4984
+ ipcMain.handle('mcp:status', () => {
4985
+ try { return mcpMod.status(); } catch { return { path: '', servers: [] }; }
4986
+ });
4987
+
4988
+ ipcMain.handle('mcp:config:get', () => {
4989
+ try {
4990
+ const p = mcpMod.configPath();
4991
+ let raw = '';
4992
+ try { raw = require('fs').readFileSync(p, 'utf8'); } catch {}
4993
+ return { path: p, raw };
4994
+ } catch { return { path: '', raw: '' }; }
4995
+ });
4996
+
4997
+ ipcMain.handle('mcp:config:set', (_e, raw) => {
4998
+ try {
4999
+ const text = String(raw || '');
5000
+ if (text.trim()) JSON.parse(text); /* bozuk JSON diske yazılmaz */
5001
+ mcpMod.saveConfig(text.trim() ? JSON.parse(text) : { servers: {} });
5002
+ return { ok: true };
5003
+ } catch (e) {
5004
+ return { ok: false, error: String((e && e.message) || e) };
5005
+ }
5006
+ });
5007
+
5008
+ ipcMain.handle('mcp:refresh', async (_e, name) => {
5009
+ try {
5010
+ const ok = await mcpMod.refresh(String(name || ''));
5011
+ return { ok, status: mcpMod.status() };
5012
+ } catch (e) {
5013
+ return { ok: false, error: String((e && e.message) || e) };
5014
+ }
5015
+ });
5016
+
4980
5017
  /* ---------------- FALLOUT IPC ---------------- */
4981
5018
 
4982
5019
  function defaultFallout() {
package/src/preload.js CHANGED
@@ -42,6 +42,10 @@ contextBridge.exposeInMainWorld('beast', {
42
42
  getSettings: () => ipcRenderer.invoke('settings:get'),
43
43
  getFallout: () => ipcRenderer.invoke('fallout:get'),
44
44
  setFallout: (cfg) => ipcRenderer.invoke('fallout:set', cfg),
45
+ mcpStatus: () => ipcRenderer.invoke('mcp:status'),
46
+ mcpConfigGet: () => ipcRenderer.invoke('mcp:config:get'),
47
+ mcpConfigSet: (raw) => ipcRenderer.invoke('mcp:config:set', raw),
48
+ mcpRefresh: (name) => ipcRenderer.invoke('mcp:refresh', name),
45
49
  getLimits: () => ipcRenderer.invoke('limits:get'),
46
50
  setLimits: (cfg) => ipcRenderer.invoke('limits:set', cfg),
47
51
  secGet: () => ipcRenderer.invoke('sec:get'),
@@ -66,6 +66,23 @@
66
66
  tab_email: 'E-posta',
67
67
  tab_integrations: 'Entegrasyonlar',
68
68
  tab_websearch: 'Web Arama',
69
+ tab_mcp: 'MCP',
70
+ mcp_h2: 'MCP Serverlar',
71
+ mcp_sub: 'Model Context Protocol — dış araç serverları (filesystem, git, fetch, memory…). Kaynak: mcp.json (aşağıda düzenlenir). Araçlar modele mcp__server__tool adıyla açılır, riskli sayılır (onay kapısı açıkken onay ister).',
72
+ mcp_state_up: 'BAĞLI',
73
+ mcp_state_starting: 'BAĞLANIYOR',
74
+ mcp_state_down: 'HATA',
75
+ mcp_state_idle: 'BEKLEMEDE',
76
+ mcp_state_disabled: 'KAPALI',
77
+ mcp_refresh: 'Yenile',
78
+ mcp_tool_count: 'araç',
79
+ mcp_json_h2: 'mcp.json',
80
+ mcp_json_sub: 'Server tanımı: {"servers":{"ad":{"command":"uvx","args":["mcp-server-fetch"],"enabled":true,"tools":["izinli_araç"],"timeoutMs":120000}}}',
81
+ mcp_save: 'Kaydet',
82
+ mcp_saved: 'Kaydedildi — serverlar ilk kullanımda bağlanır, çökerse 3 dk sonra yeniden denenir',
83
+ mcp_save_err: 'JSON hatası: ',
84
+ mcp_none: 'Henüz server yok — aşağıdaki örnekten başla',
85
+ mcp_open_folder: 'Klasörü Aç',
69
86
  tab_limits: 'Limit Ayarları',
70
87
  tab_security: 'Güvenlik',
71
88
  tab_update: 'Güncelleme',
@@ -610,6 +627,23 @@
610
627
  tab_email: 'E-mail',
611
628
  tab_integrations: 'Integrations',
612
629
  tab_websearch: 'Web Search',
630
+ tab_mcp: 'MCP',
631
+ mcp_h2: 'MCP Servers',
632
+ mcp_sub: 'Model Context Protocol — external tool servers (filesystem, git, fetch, memory…). Source: mcp.json (editable below). Tools are exposed to the model as mcp__server__tool and count as risky (approval gate applies when enabled).',
633
+ mcp_state_up: 'CONNECTED',
634
+ mcp_state_starting: 'CONNECTING',
635
+ mcp_state_down: 'ERROR',
636
+ mcp_state_idle: 'IDLE',
637
+ mcp_state_disabled: 'OFF',
638
+ mcp_refresh: 'Refresh',
639
+ mcp_tool_count: 'tools',
640
+ mcp_json_h2: 'mcp.json',
641
+ mcp_json_sub: 'Server shape: {"servers":{"name":{"command":"uvx","args":["mcp-server-fetch"],"enabled":true,"tools":["allowed_tool"],"timeoutMs":120000}}}',
642
+ mcp_save: 'Save',
643
+ mcp_saved: 'Saved — servers connect on first use, auto-retry after 3 min on failure',
644
+ mcp_save_err: 'JSON error: ',
645
+ mcp_none: 'No servers yet — start from the example below',
646
+ mcp_open_folder: 'Open Folder',
613
647
  tab_limits: 'Limits',
614
648
  tab_security: 'Security',
615
649
  tab_update: 'Update',
@@ -196,6 +196,7 @@
196
196
  <button class="tab" data-tab="email" data-i18n="tab_email">E-posta</button>
197
197
  <button class="tab" data-tab="integrations" data-i18n="tab_integrations">Entegrasyonlar</button>
198
198
  <button class="tab" data-tab="websearch" data-i18n="tab_websearch">Web Arama</button>
199
+ <button class="tab" data-tab="mcp" data-i18n="tab_mcp">MCP</button>
199
200
  <button class="tab" data-tab="events" data-i18n="tab_events">Olay Merkezi</button>
200
201
  <button class="tab" data-tab="cron" data-i18n="tab_cron">Cron</button>
201
202
  <button class="tab" data-tab="usage" data-i18n="tab_usage">Maliyet · Limit</button>
@@ -219,6 +220,7 @@
219
220
  <div id="tab-email" class="pane" hidden></div>
220
221
  <div id="tab-integrations" class="pane" hidden></div>
221
222
  <div id="tab-websearch" class="pane" hidden></div>
223
+ <div id="tab-mcp" class="pane" hidden></div>
222
224
  <div id="tab-events" class="pane" hidden></div>
223
225
  <div id="tab-usage" class="pane" hidden></div>
224
226
  <div id="tab-logs" class="pane" hidden></div>
@@ -1013,6 +1013,7 @@ async function renderActiveSettingsTab() {
1013
1013
  case 'email': await renderEmailPane(); break;
1014
1014
  case 'integrations': await renderIntegrationsPane(); break;
1015
1015
  case 'websearch': await renderWebSearchPane(); break;
1016
+ case 'mcp': await renderMcpPane(); break;
1016
1017
  case 'events': await renderEventsPane(); break;
1017
1018
  case 'cron': await openCron(); break;
1018
1019
  case 'usage': await renderUsagePane(); break;
@@ -1024,12 +1025,82 @@ async function renderActiveSettingsTab() {
1024
1025
  }
1025
1026
  }
1026
1027
 
1028
+ async function renderMcpPane() {
1029
+ const pane = $('#tab-mcp');
1030
+ if (!pane) return;
1031
+ const st = await beast.mcpStatus().catch(() => ({ path: '', servers: [] }));
1032
+ const cfg = await beast.mcpConfigGet().catch(() => ({ path: '', raw: '' }));
1033
+ const stateT = (s) => _t('mcp_state_' + s) || String(s || '').toUpperCase();
1034
+ let rows = '';
1035
+ if (!(st.servers || []).length) {
1036
+ rows = '<div class="sub">' + _t('mcp_none') + '</div>';
1037
+ }
1038
+ for (const s of st.servers || []) {
1039
+ rows +=
1040
+ '<div style="display:flex;align-items:center;gap:8px;padding:8px;border:1px solid var(--border);border-radius:8px;margin-bottom:6px;flex-wrap:wrap">' +
1041
+ '<span style="font-weight:600">' + escapeHtml(s.name) + '</span>' +
1042
+ '<span class="sub" style="margin:0;max-width:40%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + escapeHtml(s.command + ' ' + (s.args || []).join(' ')) + '</span>' +
1043
+ '<span style="color:var(--muted);font-size:12px">' + stateT(s.state) + (s.enabled && s.toolCount ? ' · ' + s.toolCount + ' ' + _t('mcp_tool_count') : '') + '</span>' +
1044
+ (s.lastError && s.state === 'down' ? '<span class="sub" style="color:#e06c75;margin:0;max-width:30%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + escapeHtml(s.lastError.slice(0, 90)) + '</span>' : '') +
1045
+ '<span style="flex:1"></span>' +
1046
+ '<button class="btn mcp-tgl" data-n="' + escapeHtml(s.name) + '" style="width:auto;padding:2px 10px">' + (s.enabled ? '⏸' : '▶') + '</button>' +
1047
+ '<button class="btn ghost mcp-ref" data-n="' + escapeHtml(s.name) + '" style="width:auto;padding:2px 10px">' + _t('mcp_refresh') + '</button>' +
1048
+ '</div>';
1049
+ }
1050
+ pane.innerHTML =
1051
+ '<h2>' + _t('mcp_h2') + '</h2>' +
1052
+ '<div class="sub">' + _t('mcp_sub') + '</div>' +
1053
+ '<div id="mcpList" style="margin-top:10px">' + rows + '</div>' +
1054
+ '<div class="divider"></div>' +
1055
+ '<h2>' + _t('mcp_json_h2') + '</h2>' +
1056
+ '<div class="sub">' + _t('mcp_json_sub') + '</div>' +
1057
+ '<textarea id="mcpJson" class="inp" rows="10" spellcheck="false" style="margin-top:8px;font-family:monospace;font-size:12px"></textarea>' +
1058
+ '<div style="display:flex;gap:8px;margin-top:8px">' +
1059
+ '<button id="mcpSave" class="btn">' + _t('mcp_save') + '</button>' +
1060
+ '<span id="mcpMsg" class="sub" style="margin:0;align-self:center"></span></div>';
1061
+ const box = $('#mcpJson');
1062
+ box.value = cfg.raw || JSON.stringify({ servers: {} }, null, 2);
1063
+ $('#mcpSave').addEventListener('click', async () => {
1064
+ const msg = $('#mcpMsg');
1065
+ const r = await beast.mcpConfigSet(box.value).catch(() => ({ ok: false, error: 'IPC' }));
1066
+ if (r && r.ok) {
1067
+ msg.textContent = _t('mcp_saved');
1068
+ msg.style.color = '';
1069
+ renderMcpPane();
1070
+ } else {
1071
+ msg.textContent = _t('mcp_save_err') + ((r && r.error) || '?');
1072
+ msg.style.color = '#e06c75';
1073
+ }
1074
+ });
1075
+ pane.querySelectorAll('.mcp-ref').forEach((b) =>
1076
+ b.addEventListener('click', async () => {
1077
+ b.disabled = true;
1078
+ await beast.mcpRefresh(b.dataset.n).catch(() => {});
1079
+ renderMcpPane();
1080
+ })
1081
+ );
1082
+ pane.querySelectorAll('.mcp-tgl').forEach((b) =>
1083
+ b.addEventListener('click', async () => {
1084
+ b.disabled = true;
1085
+ try {
1086
+ const obj = JSON.parse(box.value || '{}');
1087
+ const name = b.dataset.n;
1088
+ if (obj.servers && obj.servers[name]) {
1089
+ obj.servers[name].enabled = obj.servers[name].enabled === false;
1090
+ await beast.mcpConfigSet(JSON.stringify(obj));
1091
+ }
1092
+ } catch {}
1093
+ renderMcpPane();
1094
+ })
1095
+ );
1096
+ }
1097
+
1027
1098
  function switchTab(name) {
1028
1099
  setTab = name;
1029
1100
  document.querySelectorAll('#setTabs .tab').forEach((b) =>
1030
1101
  b.classList.toggle('active', b.dataset.tab === name)
1031
1102
  );
1032
- for (const p of ['lang', 'provider', 'fallout', 'skills', 'agents', 'tts', 'email', 'integrations', 'websearch', 'events', 'cron', 'usage', 'logs', 'dash', 'sec', 'update']) {
1103
+ for (const p of ['lang', 'provider', 'fallout', 'skills', 'agents', 'tts', 'email', 'integrations', 'websearch', 'mcp', 'events', 'cron', 'usage', 'logs', 'dash', 'sec', 'update']) {
1033
1104
  const el = $('#tab-' + p);
1034
1105
  if (el) el.hidden = p !== name; // guard: eksik pane tüm sekmeleri kilitlemesin
1035
1106
  }
@@ -1043,6 +1114,7 @@ function switchTab(name) {
1043
1114
  if (name === 'update') renderUpdatePane(true);
1044
1115
  if (name === 'agents') refreshAgentsPane();
1045
1116
  if (name === 'websearch') renderWebSearchPane();
1117
+ if (name === 'mcp') renderMcpPane();
1046
1118
  /* Fallout: her açılışta güncel provider zincirini çek */
1047
1119
  if (name === 'fallout') refreshFalloutPane();
1048
1120
  }
@@ -392,7 +392,7 @@ body.term-open #topbar { width: auto; }
392
392
  display: flex;
393
393
  align-items: center;
394
394
  gap: 8px;
395
- padding: 0 10px;
395
+ padding: 0 148px 0 10px; /* sağ: sabit pencere butonları (win-controls) alanı */
396
396
  border-bottom: 1px solid var(--border);
397
397
  }
398
398
  #termTitle { font-size: 11px; font-weight: 800; letter-spacing: 1px; color: var(--accent); flex: none; }
@@ -503,13 +503,18 @@ body.term-open #settingsOverlay { right: var(--tw, 520px); }
503
503
  .drag-spacer { flex: 1; height: 100%; -webkit-app-region: drag; }
504
504
 
505
505
  /* ---------- custom pencere butonları (native overlay yerine) ----------
506
- Arka plan YOK topbar'ın kendi zemininde yüzüyor; hover'da uygulamanın
507
- kendi accent-dim tonu, kapat Windows alışkanlığıyla kırmızı. */
506
+ PENCERENİN sağ üst köşesine SABİT: tarayıcı/terminal/paralel ajan konsolu
507
+ hangi panel açılırsa açılsın butonlar hep en sağda kalır (panellerin
508
+ başlık satırları bu alanı boş bırakır). Zemin yok — altındaki yüzeyde
509
+ yüzür; hover'da accent-dim, kapat kırmızı. */
508
510
  .win-controls {
511
+ position: fixed;
512
+ top: 0;
513
+ right: 0;
514
+ z-index: 100;
509
515
  display: flex;
510
516
  align-items: center;
511
- height: 100%;
512
- margin-left: -4px;
517
+ height: 46px;
513
518
  -webkit-app-region: no-drag;
514
519
  }
515
520
  .win-controls button {
@@ -1805,7 +1810,7 @@ body.browser-open #settingsDialog { width: min(70vw, calc(100vw - var(--bw, 480p
1805
1810
  display: flex;
1806
1811
  align-items: center;
1807
1812
  gap: 8px;
1808
- padding: 12px 12px 10px 16px;
1813
+ padding: 12px 148px 10px 16px; /* sağ: sabit pencere butonları (win-controls) alanı */
1809
1814
  font-weight: 800;
1810
1815
  letter-spacing: 1px;
1811
1816
  font-size: 13px;