beast-agent 2.19.1 → 2.21.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "beast-agent",
3
3
  "productName": "Beast Agent",
4
- "version": "2.19.1",
4
+ "version": "2.21.0",
5
5
  "description": "Ultra-fast local agent shell for Windows.",
6
6
  "author": "algokodcom (AlgoKod)",
7
7
  "license": "MIT",
@@ -0,0 +1,242 @@
1
+ 'use strict';
2
+
3
+ /* KİŞİSEL TOOLLAR: %APPDATA%\beast\tools\<slug>\tool.json + run.js
4
+ Kullanıcı (ya da ajan) Node ile koşan kendi araçlarını yazar; TÜM oturumlar
5
+ — BEAST FINANCE dahil — bunları model'e açılan gerçek araç olarak çağırır
6
+ (tool__<slug>). Sözleşme:
7
+ - stdin : JSON argümanlar
8
+ - stdout : JSON sonuç (son satır JSON'a çevrilmeye çalışılır)
9
+ - çalışma klasörü: tool'un kendi klasörü (%APPDATA%\beast\tools\<slug>)
10
+ Sonuç model'e { ok, ... } olarak döner; çocuk proses izoledir (uygulama
11
+ çökmez), 90 sn zaman aşımı vardır. */
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+ const { spawn } = require('child_process');
16
+ const { beastRoot } = require('./memory');
17
+
18
+ let notify = () => {};
19
+ function setNotify(fn) {
20
+ notify = typeof fn === 'function' ? fn : () => {};
21
+ }
22
+
23
+ function dir() {
24
+ return path.join(beastRoot(), 'tools');
25
+ }
26
+
27
+ function slugify(name) {
28
+ return String(name || '')
29
+ .toLowerCase()
30
+ .replace(/[^a-z0-9_-]+/g, '_')
31
+ .replace(/^_+|_+$/g, '')
32
+ .slice(0, 24);
33
+ }
34
+
35
+ let cache = null;
36
+ let dirty = true;
37
+
38
+ function invalidate() {
39
+ dirty = true;
40
+ cache = null;
41
+ }
42
+
43
+ function readTool(d) {
44
+ try {
45
+ const json = JSON.parse(fs.readFileSync(path.join(d, 'tool.json'), 'utf8'));
46
+ if (!json || typeof json !== 'object' || !json.name) return null;
47
+ const runJs = path.join(d, 'run.js');
48
+ if (!fs.existsSync(runJs)) return null;
49
+ return {
50
+ id: path.basename(d),
51
+ name: String(json.name).slice(0, 40),
52
+ description: String(json.description || '').slice(0, 300),
53
+ parameters:
54
+ json.parameters && typeof json.parameters === 'object'
55
+ ? json.parameters
56
+ : { type: 'object', properties: {} },
57
+ script: runJs,
58
+ code: fs.readFileSync(runJs, 'utf8').slice(0, 20000),
59
+ updatedAt: json.updatedAt || null,
60
+ };
61
+ } catch {
62
+ return null;
63
+ }
64
+ }
65
+
66
+ function scan() {
67
+ const out = [];
68
+ try {
69
+ for (const e of fs.readdirSync(dir(), { withFileTypes: true })) {
70
+ if (!e.isDirectory()) continue;
71
+ const t = readTool(path.join(dir(), e.name));
72
+ if (t) out.push(t);
73
+ }
74
+ } catch {}
75
+ return out;
76
+ }
77
+
78
+ function list() {
79
+ return scan();
80
+ }
81
+
82
+ /* engine _chatTurn'e OpenAI function formatında şemalar */
83
+ function definitions() {
84
+ if (dirty || !cache) {
85
+ cache = scan();
86
+ dirty = false;
87
+ }
88
+ return cache.map((t) => ({
89
+ type: 'function',
90
+ function: {
91
+ name: 'tool__' + t.id,
92
+ description:
93
+ (t.description || 'kişisel tool') + ` (kişisel tool — args stdin JSON, sonuç stdout JSON)`,
94
+ parameters: t.parameters,
95
+ },
96
+ }));
97
+ }
98
+
99
+ function names() {
100
+ if (dirty || !cache) {
101
+ cache = scan();
102
+ dirty = false;
103
+ }
104
+ return cache.map((t) => 'tool__' + t.id);
105
+ }
106
+
107
+ const TOOL_TIMEOUT_MS = 90 * 1000;
108
+
109
+ /* Tool'u çocuk node prosesinde koşturur — uygulama asla çökmez */
110
+ function call(name, args) {
111
+ return new Promise((resolve) => {
112
+ const id = String(name || '').replace(/^tool__/, '');
113
+ if (!id) return resolve({ ok: false, error: 'tool adı gerekli' });
114
+ const t = scan().find((x) => x.id === id);
115
+ if (!t) return resolve({ ok: false, error: 'tool bulunamadı: ' + id });
116
+ const t0 = Date.now();
117
+ let child = null;
118
+ let out = '';
119
+ let err = '';
120
+ let done = false;
121
+ const finish = (r) => {
122
+ if (done) return;
123
+ done = true;
124
+ clearTimeout(killer);
125
+ try {
126
+ notify({
127
+ at: new Date().toISOString(),
128
+ tool: t.name,
129
+ toolId: id,
130
+ ok: !!r.ok,
131
+ ms: Date.now() - t0,
132
+ error: r && r.error ? String(r.error).slice(0, 200) : null,
133
+ });
134
+ } catch {}
135
+ resolve(r);
136
+ };
137
+ let killer = setTimeout(() => {
138
+ try { child && child.kill(); } catch {}
139
+ finish({ ok: false, error: 'tool zaman aşımı (' + Math.round(TOOL_TIMEOUT_MS / 1000) + ' sn)' });
140
+ }, TOOL_TIMEOUT_MS);
141
+ try {
142
+ child = spawn(process.execPath, [t.script], { cwd: t.path, stdio: ['pipe', 'pipe', 'pipe'] });
143
+ } catch (e) {
144
+ return finish({ ok: false, error: String((e && e.message) || e) });
145
+ }
146
+ child.stdout.on('data', (d) => { out += d; });
147
+ child.stderr.on('data', (d) => { err += d; });
148
+ child.on('error', (e) => finish({ ok: false, error: String((e && e.message) || e) }));
149
+ child.on('close', (code) => {
150
+ const raw = String(out || '').trim();
151
+ let data = null;
152
+ try { data = raw ? JSON.parse(raw) : null; } catch {}
153
+ if (data === null || typeof data !== 'object' || Array.isArray(data) || typeof data.ok !== 'boolean') {
154
+ data = { ok: code === 0, result: data === null ? raw.slice(0, 4000) : data };
155
+ }
156
+ if (code !== 0) {
157
+ if (data.ok !== false) data.ok = false;
158
+ if (!data.error) data.error = String(err || '').slice(0, 400) || 'çıkış kodu ' + code;
159
+ }
160
+ finish(data);
161
+ });
162
+ try {
163
+ child.stdin.write(JSON.stringify(args || {}));
164
+ child.stdin.end();
165
+ } catch {}
166
+ });
167
+ }
168
+
169
+ function save({ id, name, description, parameters, code }) {
170
+ const slug = id && /^[a-z0-9_-]{2,24}$/.test(String(id)) ? String(id) : slugify(name);
171
+ if (!slug) return { ok: false, error: 'tool adı gerekli (a-z 0-9 _ -)' };
172
+ const d = path.join(dir(), slug);
173
+ try {
174
+ fs.mkdirSync(d, { recursive: true });
175
+ fs.writeFileSync(
176
+ path.join(d, 'tool.json'),
177
+ JSON.stringify(
178
+ {
179
+ name: String(name || slug).slice(0, 40),
180
+ description: String(description || '').slice(0, 300),
181
+ parameters:
182
+ parameters && typeof parameters === 'object' && !Array.isArray(parameters)
183
+ ? parameters
184
+ : { type: 'object', properties: {} },
185
+ updatedAt: new Date().toISOString(),
186
+ },
187
+ null,
188
+ 2
189
+ )
190
+ );
191
+ const js = String(code || '').trim();
192
+ if (!js) return { ok: false, error: 'run.js kodu boş olamaz' };
193
+ fs.writeFileSync(path.join(d, 'run.js'), js.slice(0, 20000));
194
+ invalidate();
195
+ return { ok: true, id: slug };
196
+ } catch (e) {
197
+ return { ok: false, error: String((e && e.message) || e) };
198
+ }
199
+ }
200
+
201
+ function remove(id) {
202
+ const slug = String(id || '');
203
+ if (!/^[a-z0-9_-]{2,24}$/.test(slug)) return { ok: false, error: 'geçersiz tool kimliği' };
204
+ try {
205
+ fs.rmSync(path.join(dir(), slug), { recursive: true, force: true });
206
+ invalidate();
207
+ return { ok: true };
208
+ } catch (e) {
209
+ return { ok: false, error: String((e && e.message) || e) };
210
+ }
211
+ }
212
+
213
+ /* Örnek tool: klasör boşsa biçimi göstermek için bir adet kur */
214
+ function seedIfEmpty() {
215
+ try {
216
+ if (scan().length) return;
217
+ save({
218
+ name: 'ornek_echo',
219
+ description:
220
+ 'Örnek kişisel tool — gelen argümanları geri döndürür. Bu klasörü kopyalayarak kendi toollarını yaz; agent finance dahil tüm oturumlarda tool__<ad> olarak çağırır.',
221
+ parameters: {
222
+ type: 'object',
223
+ properties: {
224
+ mesaj: { type: 'string', description: 'geri döndürülecek deneme mesajı' },
225
+ },
226
+ required: ['mesaj'],
227
+ },
228
+ code:
229
+ "'use strict';\n" +
230
+ "// KİŞİSEL TOOL — stdin: JSON args · stdout: JSON sonuç\n" +
231
+ "// Bu klasörü kopyalayıp kendi araçlarını yaz (örn. MT5 emir köprüsü, özel API çağrısı…)\n" +
232
+ "const fs = require('fs');\n" +
233
+ "let raw = '';\n" +
234
+ "try { raw = fs.readFileSync(0, 'utf8'); } catch {}\n" +
235
+ "let args = {};\n" +
236
+ "try { args = JSON.parse(raw || '{}'); } catch {}\n" +
237
+ "console.log(JSON.stringify({ ok: true, echo: args, not: 'run.js ve tool.json düzenlenebilir' }));\n",
238
+ });
239
+ } catch {}
240
+ }
241
+
242
+ module.exports = { setNotify, list, scan, definitions, names, call, save, remove, seedIfEmpty, invalidate, dir, slugify };
@@ -13,6 +13,7 @@ const tools = require('./tools');
13
13
  let financetools = null;
14
14
  try { financetools = require('./financetools'); } catch {}
15
15
  const FINANCE_TOOL_SET = new Set(financetools ? financetools.NAMES : []);
16
+ const customtools = require('./customtools');
16
17
  const research = require('./research');
17
18
  const agentdefs = require('./agentdefs');
18
19
  const memory = require('./memory');
@@ -1232,7 +1233,14 @@ class Engine {
1232
1233
  let r;
1233
1234
  try { r = JSON.parse(l); } catch { continue; }
1234
1235
  if (r.t === 'meta') out.push(JSON.stringify({ t: 'meta', id: s.id, code: s.code, createdAt: s.createdAt }));
1235
- else if (r.t === 'meta2' || r.t === 'bot' || r.t === 'todo') out.push(l);
1236
+ /* KALICİ ETİKETLER compact'ta ASLA düşmez — özellikle 'fin'/'finoff':
1237
+ silinirse finance oturumu etiketini KAYBEDER ve mt5_* araçları
1238
+ "unknown tool" olur (bayat compact kaybı bu yüzden) */
1239
+ else if (
1240
+ r.t === 'meta2' || r.t === 'bot' || r.t === 'todo' ||
1241
+ r.t === 'fin' || r.t === 'finoff' ||
1242
+ r.t === 'agent' || r.t === 'bcws' || r.t === 'botdm'
1243
+ ) out.push(l);
1236
1244
  }
1237
1245
  if (lastNotesLine) out.push(lastNotesLine);
1238
1246
  /* korunan pencere: son N mesaj */
@@ -1419,7 +1427,12 @@ class Engine {
1419
1427
  const kept = lines.filter((l) => {
1420
1428
  try {
1421
1429
  const r = JSON.parse(l);
1422
- return r.t === 'meta' || r.t === 'meta2' || r.t === 'bot' || r.t === 'todo';
1430
+ /* kalıcı etiketler korunur (fin/finoff dahil — finance bayrağı kaybolmasın) */
1431
+ return (
1432
+ r.t === 'meta' || r.t === 'meta2' || r.t === 'bot' || r.t === 'todo' ||
1433
+ r.t === 'fin' || r.t === 'finoff' ||
1434
+ r.t === 'agent' || r.t === 'bcws' || r.t === 'botdm'
1435
+ );
1423
1436
  } catch { return false; }
1424
1437
  });
1425
1438
  const tmp = file + '.tmp';
@@ -3453,6 +3466,9 @@ class Engine {
3453
3466
  toolsList = await mcp.mergeTools(toolsList);
3454
3467
  /* Beast Apps: kurulu app'lerin araçları (app__<id>__<tool>) modele açılır */
3455
3468
  toolsList = apps.mergeTools(toolsList);
3469
+ /* KİŞİSEL TOOLLAR: %APPDATA%\beast\tools\<ad>\ — kullanıcı yazmışsa
3470
+ TÜM oturumlara (BEAST FINANCE dahil) tool__<ad> olarak açılır */
3471
+ toolsList = [...toolsList, ...customtools.definitions()];
3456
3472
  /* Beast Finance: skill aracı da açık — kataloğun SKILL.md gövdesi
3457
3473
  modele açılır (skill handler'ı zaten tüm oturumlar için çalışır,
3458
3474
  definition yalnız BC'de vardı; finance chat copilot + trader ikisi de)
@@ -3464,8 +3480,13 @@ class Engine {
3464
3480
  if (skillDef) toolsList = [...toolsList, skillDef];
3465
3481
  }
3466
3482
  /* AJAN DM: koşan ajan oturumlarına (arka plan işleri + finance
3467
- ajanları) ajanlar-arası mesajlaşma aracı verilir */
3468
- if (session && (session.bgJob || (session.finance && session.bgTitle))) {
3483
+ ajanları) VE ana sohbete verilir — chat Beast'i de ajana DM atabilir;
3484
+ müşteri bot oturumları (botId ≠ beast) ve botlar arası DM hariç */
3485
+ if (
3486
+ session &&
3487
+ !session.isBotDm &&
3488
+ (session.bgJob || session.finance || !session.botId || session.botId === 'beast')
3489
+ ) {
3469
3490
  toolsList = [...toolsList, AGENT_DM_DEF];
3470
3491
  }
3471
3492
  /* panel_run yalnız SANDBOX oturumlarında görünsün — diğer panellerde
@@ -3890,6 +3911,8 @@ class Engine {
3890
3911
  // Paralel yürütme — bağımsız çağrılar beklemesin
3891
3912
  session.toolsSinceReflect = (session.toolsSinceReflect || 0) + res.toolCalls.length;
3892
3913
  if (!this._knownToolNames) this._knownToolNames = new Set([...TOOLS.map((t) => t.function.name), ...opencode.toolmap.names()]);
3914
+ /* kişisel tool sonradan yazılabilir — isim havuzunu canlı tazele */
3915
+ for (const n of customtools.names()) this._knownToolNames.add(n);
3893
3916
  await Promise.all(
3894
3917
  res.toolCalls.map(async (tc) => {
3895
3918
  let name = tc.function && tc.function.name;
@@ -4765,6 +4788,10 @@ const skills = require('./skills');
4765
4788
  if (name === 'agent_dm') {
4766
4789
  return JSON.stringify(this._agentDmSend(sessionId, args));
4767
4790
  }
4791
+ /* KİŞİSEL TOOL: %APPDATA%\beast\tools\ — çocuk node prosesinde izole koşar */
4792
+ if (String(name).startsWith('tool__')) {
4793
+ return JSON.stringify(await customtools.call(name, args));
4794
+ }
4768
4795
  if (isBc) {
4769
4796
  /* opencode permission akışı (tool/external-directory.ts + her aracın
4770
4797
  ctx.ask'ı): önce workspace DIŞI erişim izni, sonra araç izni.
@@ -4796,7 +4823,9 @@ const skills = require('./skills');
4796
4823
  "always" onaylı araçlar doğrudan geçer. (BC dışı oturumlar — opencode
4797
4824
  izin sistemi yalnız Beast Code oturumlarında koşar) */
4798
4825
  if (
4799
- (Engine.RISKY_TOOLS.has(name) || String(name).startsWith('mcp__')) &&
4826
+ (Engine.RISKY_TOOLS.has(name) ||
4827
+ String(name).startsWith('mcp__') ||
4828
+ String(name).startsWith('tool__')) && /* kişisel tool = kullanıcı kodu koşturur */
4800
4829
  !this.alwaysAllowTools.has(name) &&
4801
4830
  this.approvals && typeof this.approvals.request === 'function'
4802
4831
  ) {
@@ -5288,23 +5317,25 @@ function emitSafe(engine, sessionId, ev) {
5288
5317
 
5289
5318
  /* AJAN DM aracı: paralel/finance ajanlarının birbirine kısa mesaj atması.
5290
5319
  Salt metin — hedef ajanın oturumuna [AJAN DM] bloğu olarak düşer; AJAN DM
5291
- panelinde iki yönlü görünür. */
5320
+ panelinde iki yönlü görünür. topic verirsen aynı konudaki mesajlar aynı
5321
+ oturumda toplanır (cevap zinciri tek thread'de devam eder). */
5292
5322
  const AGENT_DM_DEF = {
5293
5323
  type: 'function',
5294
5324
  function: {
5295
5325
  name: 'agent_dm',
5296
5326
  description:
5297
- 'Send a short DM to another RUNNING agent (parallel agents / finance agents) to coordinate: share findings, ask status, warn about risk, hand off work. `to` = target session id OR a keyword from the agent title (e.g. "GOLD", "Trader"). The message is delivered to the other agent and shown in the Agent DM panel.',
5327
+ 'Send a short DM to another running agent (parallel agents / finance agents) to coordinate: share findings, ask status, warn about risk, hand off work. `to` = target session id OR a keyword from the agent title (e.g. "GOLD", "Trader"). `topic` = short subject label — replies to the same topic stay in the SAME conversation thread, so ALWAYS reuse the topic you were DMed with when replying.',
5298
5328
  parameters: {
5299
5329
  type: 'object',
5300
5330
  properties: {
5301
5331
  to: { type: 'string', description: 'Target agent session id or a title keyword' },
5332
+ topic: { type: 'string', description: 'Short subject label for the conversation thread, e.g. "GOLD pozisyon riski"' },
5302
5333
  message: { type: 'string', description: 'Short message (1-3 sentences)' },
5303
5334
  },
5304
5335
  required: ['to', 'message'],
5305
5336
  },
5306
5337
  },
5307
- };
5338
+ };
5308
5339
 
5309
5340
  const TOOLS = [
5310
5341
  ...tools.definitions,
@@ -6121,12 +6152,15 @@ Engine.prototype._agentDmSend = function (fromSid, args) {
6121
6152
  if (String(target) === String(fromSid)) return { ok: false, error: 'kendine DM atılamaz' };
6122
6153
  const fromJob = jobs.get(String(fromSid));
6123
6154
  const toJob = jobs.get(target);
6155
+ /* konu etiketi: aynı konudaki DM'ler panelde AYNI oturumda toplanır */
6156
+ const topic = String((args && args.topic) || '').replace(/\s+/g, ' ').trim().slice(0, 60) || '(genel)';
6124
6157
  const dm = {
6125
6158
  at: new Date().toISOString(),
6126
6159
  from: String(fromSid),
6127
6160
  fromTitle: (fromJob && fromJob.title) || 'Ajan',
6128
6161
  to: String(target),
6129
6162
  toTitle: (toJob && toJob.title) || 'Ajan',
6163
+ topic,
6130
6164
  text,
6131
6165
  };
6132
6166
  this._agentDms = this._agentDms || [];
@@ -6136,15 +6170,16 @@ Engine.prototype._agentDmSend = function (fromSid, args) {
6136
6170
  fs.writeFileSync(this._agentDmsFile, JSON.stringify({ dms: this._agentDms }, null, 2));
6137
6171
  } catch {}
6138
6172
  emitSafe(this, target, { type: 'agent-dm', ...dm });
6139
- /* teslim: hedef meşgulse pending kuyruğunda bekler, done olunca düşer */
6173
+ /* teslim: hedef meşgulse pending kuyruğunda bekler, done olunca düşer.
6174
+ Cevap zinciri AYNI thread'den sürsün diye topic geri bildirilir. */
6140
6175
  (this._pendingReports = this._pendingReports || []).push({
6141
6176
  parentId: target,
6142
6177
  text:
6143
- `[AJAN DM — ${dm.fromTitle}]\n${text}\n` +
6144
- `(Cevabını agent_dm aracıyla ver — to: "${dm.fromTitle}")`,
6178
+ `[AJAN DM — ${dm.fromTitle} · konu: "${topic}"]\n${text}\n` +
6179
+ `(Cevabını agent_dm aracıyla ver — to: "${dm.fromTitle}", topic: "${topic}")`,
6145
6180
  });
6146
6181
  this.flushPendingReports(target);
6147
- return { ok: true, to: target, toTitle: dm.toTitle };
6182
+ return { ok: true, to: target, toTitle: dm.toTitle, topic };
6148
6183
  } catch (e) {
6149
6184
  return { ok: false, error: String((e && e.message) || e) };
6150
6185
  }
@@ -129,7 +129,8 @@ Get-Content "$env:APPDATA\\beast\\sessions\\bg-jobs.json" | ConvertFrom-Json | %
129
129
  | \`agent/agentdefs.js\` | Özel ajan tanımları: %APPDATA%\beast\agents\\*.md (frontmatter: model/tools/steps/mode + prompt gövdesi); /agent ile bağlanır, run_background(agent:) ile paralel ajana verilir |
130
130
  | \`agent/searxng.js\` | SearXNG entegrasyonu: gömülü Python'a kurulum + 127.0.0.1:8888 başlatıcı + JSON arama motoru |
131
131
  | \`agent/bots.js\` | Bot CRUD, 5 haneli bot kodu, izolasyon; ad-ilk-harf doğrulaması |
132
- | \`agent/skills.js\` | Builtin tohumlar (help, email, python-web-search, pdf, gold-trading + superpowers metodoloji paketi: brainstorming/writing-plans/executing-plans/TDD/paralel-ajan/alt-ajan-geliştirme/sistematik-debugging/doğrulama/skill-yazma) + SKILL.md taraması + taslaklar |
132
+ | \`agent/skills.js\` | Builtin tohumlar (help, email, python-web-search, pdf, gold-trading, tool-yazma + superpowers metodoloji paketi: brainstorming/writing-plans/executing-plans/TDD/paralel-ajan/alt-ajan-geliştirme/sistematik-debugging/doğrulama/skill-yazma) + SKILL.md taraması + taslaklar |
133
+ | \`agent/customtools.js\` | Kişisel toollar: %APPDATA%\\beast\\tools\\<ad>\\tool.json + run.js (stdin JSON → stdout JSON) — tool__<ad> olarak TÜM oturumlara açılır; sol panel TOOLLAR kutusunda yazılır/test edilir. Yeni tool yazarken \`tool-yazma\` skill'ini oku. |
133
134
  | \`agent/memory.js\`, \`agent/kb.js\` | Kalıcı hafıza + bilgi bankası |
134
135
  | \`cron.js\` (src kökü), \`agent/watchers.js\`, \`agent/bus.js\` | Cron, izleyici, olay merkezi — /stop bunlara DOKUNMAZ |
135
136
  | \`agent/usage.js\` | Token/maliyet sayaçları |
@@ -456,6 +457,87 @@ const { createWorker } = require('tesseract.js');
456
457
  | read_file boş metin döndü | taranmış PDF → 4. adım (görselleştir) + 5. adım (OCR) |
457
458
  | tesseract dil verisi hatası | ilk indirme internet ister; tekrar dene |`,
458
459
  },
460
+ {
461
+ folder: 'tool-yazma',
462
+ force: true,
463
+ body: `---
464
+ name: tool-yazma
465
+ description: Kişisel TOOL yazma/düzenleme rehberi — %APPDATA%\\beast\\tools\\<ad>\\ altına tool.json + run.js yazılacak veya mevcut bir tool editlenecekse ÖNCE bu skill'i oku. Tool'un sözleşmesi: stdin JSON argüman → stdout JSON sonuç; ajan tool__<ad> olarak çağırır (Beast Finance dahil tüm oturumlarda çalışır).
466
+ version: 1.0.0
467
+ ---
468
+
469
+ # Kişisel TOOL Yazma
470
+
471
+ ## TOOL NEDİR / NE ZAMAN YAZILIR
472
+
473
+ Kişisel tool = modelin alet çantasına GİREN gerçek bir araçtır: %APPDATA%\\beast\\tools\\<slug>\\ klasöründe **tool.json** (tanım) + **run.js** (kod) bulunur. Tüm oturumlarda — Beast Finance chat/trader/işçiler dahil — \`tool__<slug>\` adıyla çağrılır.
474
+
475
+ Yaz ZAMAN: mevcut araçlar işi görmüyorsa (örn. broker sembol adlandırması mt5_trade'i düşürüyorsa), özel bir API'ye bağlanman, tekrarlı bir hesap/işlem rutini gerekiyorsa. Basit işler için YAZMA — önce yerleşik araçları (mt5_*, web_search, python_run, run_command) dene.
476
+
477
+ ## SÖZLEŞME (birebir uygula)
478
+
479
+ - **stdin**: JSON argümanlar (tek satırda, güvenli parse)
480
+ - **stdout**: SADECE JSON sonuç — son satır JSON'a çevrilmeye çalışılır; günlük/metin yazarsan JSON bozulur, \`console.error\` kullan
481
+ - **çalışma klasörü**: tool'un kendi klasörü (yanına dosya yazabilirsin)
482
+ - **zaman aşımı**: 90 sn — uzun işleri kendin parçalama, \`run_background\`'a devret
483
+ - **sonuç biçimi**: \`{ "ok": true, ... }\` ya da \`{ "ok": false, "error": "sebep" }\` — ok alanı ZORUNLU
484
+
485
+ ## YENİ TOOL OLUŞTURMA (2 dosya, write_file ile)
486
+
487
+ \`%APPDATA%\\beast\\tools\\<slug>\\tool.json\`:
488
+ \`\`\`json
489
+ {
490
+ "name": "gold_emir",
491
+ "description": "GOLD için özel emir açar — side (buy/sell), lot, sl, tp alır; sonucu ticket ile döndürür.",
492
+ "parameters": {
493
+ "type": "object",
494
+ "properties": {
495
+ "side": { "type": "string", "description": "buy | sell" },
496
+ "lot": { "type": "number", "description": "0.01-1.0" },
497
+ "sl": { "type": "number" },
498
+ "tp": { "type": "number" }
499
+ },
500
+ "required": ["side", "lot"]
501
+ },
502
+ "updatedAt": "2026-09-09T00:00:00.000Z"
503
+ }
504
+ \`\`\`
505
+
506
+ \`%APPDATA%\\beast\\tools\\<slug>\\run.js\`:
507
+ \`\`\`javascript
508
+ 'use strict';
509
+ // stdin: JSON args · stdout: JSON sonuç (SADECE JSON yaz!)
510
+ const fs = require('fs');
511
+ let args = {};
512
+ try { args = JSON.parse(fs.readFileSync(0, 'utf8') || '{}'); } catch {}
513
+ (function main() {
514
+ const { side, lot, sl, tp } = args;
515
+ if (!side || !lot) {
516
+ console.log(JSON.stringify({ ok: false, error: 'side ve lot zorunlu' }));
517
+ return;
518
+ }
519
+ // --- kendi işin burada: HTTP çağrısı, MT5 köprüsü, dosya, hesap... ---
520
+ // Örn: https modülüyle özel endpoint'e istek at, sonucu ok alanıyla sar.
521
+ console.log(JSON.stringify({ ok: true, side, lot, sl: sl || 0, tp: tp || 0, ticket: 0 }));
522
+ })();
523
+ \`\`\`
524
+
525
+ Slug kuralları: a-z, 0-9, alt çizgi, tire — 2-24 karakter (örn: gold_emir, haber_ozet). Hazır taslak: %APPDATA%\\beast\\tools\\ornek_echo klasörünü referans al; yeni tool'u o biçimle yaz.
526
+
527
+ ## DÜZENLEME / DOĞRULAMA
528
+
529
+ 1. Mevcut toolu değiştirme: \`edit_file\` ile tool.json / run.js düzenle — ASLA tüm klasörü silip baştan yazma.
530
+ 2. Kayıttan sonra tool ANINDA aktifleşir (yeniden başlatma gerekmez) — bir sonraki turda \`tool__<slug>\` çağrılabilir.
531
+ 3. Doğrula: \`run_command\` ile \`echo \\"{\\\\\\"mesaj\\\\\\":\\\\\\"test\\\\\\"}\\" | node \\"%APPDATA%\\\\beast\\\\tools\\\\<slug>\\\\run.js\\"\` — çıktı JSON olmalı ve \`ok\` alanı içermeli.
532
+ 4. Hata çıktısı \`ok:false + error\` olsun — model hatayı okuyup düzeltir; çıplak stack yasak.
533
+
534
+ ## GÜVENLİK + STİL
535
+
536
+ - Şifre/API anahtarını ASLA stdout'a, log'a, skill'e yazma.
537
+ - Ağ çağrılarında timeout kullan (\`https\` modülü + \`req.setTimeout\` veya fetch); sonsuz bekleyen script yazma.
538
+ - Tek sorumluluk: bir tool = bir iş. Çok işli dev tool yazma — parçala.
539
+ - Kullanıcıya tool'u nasıl yöneteceğini söyle: sol panelde TOOLLAR kutusunda test/düzenle/sil + 📂 klasör.`,
540
+ },
459
541
  ];
460
542
 
461
543
  function writeSeed(seed) {
package/src/main.js CHANGED
@@ -3605,6 +3605,7 @@ app.whenReady().then(() => {
3605
3605
  reloadBackend();
3606
3606
  syncWhitelist(); // bot sistemi: whitelist.json aynası ilk açılışta garanti
3607
3607
  try { bots.ensureBotCodes(); } catch {} // her bota benzersiz 5 haneli kod garanti
3608
+ try { customtools.seedIfEmpty(); } catch {} // kişisel toollar klasörü boşsa örnek tool kur
3608
3609
  createSplash();
3609
3610
  createWindow();
3610
3611
  log.info('main', 'Beast Agent başlatıldı');
@@ -5892,6 +5893,27 @@ ipcMain.handle('agents:cancel', (_e, id) => {
5892
5893
  /* AJAN DM paneli: ajanlar arası mesaj arşivi */
5893
5894
  ipcMain.handle('agent-dms:list', () => (engine ? engine.agentDmsList() : []));
5894
5895
  ipcMain.handle('agent-dms:clear', () => (engine ? engine.agentDmsClear() : { ok: false }));
5896
+
5897
+ /* ---------- KİŞİSEL TOOLLAR (%APPDATA%\beast\tools\) ---------- */
5898
+ ipcMain.handle('tools:list', () => customtools.list());
5899
+ ipcMain.handle('tools:save', (_e, tool) => customtools.save(tool || {}));
5900
+ ipcMain.handle('tools:delete', (_e, id) => customtools.remove(id));
5901
+ ipcMain.handle('tools:run', async (_e, payload) => {
5902
+ const id = String((payload && payload.id) || '');
5903
+ const args = (payload && payload.args) || {};
5904
+ const r = await customtools.call('tool__' + id, args);
5905
+ return r;
5906
+ });
5907
+ ipcMain.handle('tools:openFolder', () => {
5908
+ try {
5909
+ const d = customtools.dir();
5910
+ fs.mkdirSync(d, { recursive: true });
5911
+ require('electron').shell.openPath(d);
5912
+ return { ok: true };
5913
+ } catch (e) {
5914
+ return { ok: false, error: String((e && e.message) || e) };
5915
+ }
5916
+ });
5895
5917
  ipcMain.handle('ceo:get', () => !!engine.ceoMode);
5896
5918
  ipcMain.handle('ceo:set', (_e, v) => {
5897
5919
  settings.ceoMode = !!v;
@@ -8273,6 +8295,7 @@ function studioWatchStop() {
8273
8295
  oturumu, tur tur piyasa tarayıp (farklı API/model seçilebilir) işlem kovalar. */
8274
8296
  const mt5bridge = require('./mt5bridge');
8275
8297
  const financetools = require('./agent/financetools');
8298
+ const customtools = require('./agent/customtools');
8276
8299
 
8277
8300
  const financeState = {
8278
8301
  mode: false, /* chat oturumları finance bayrağı alıyor mu */
@@ -8337,6 +8360,15 @@ try {
8337
8360
  });
8338
8361
  } catch {}
8339
8362
 
8363
+ /* KİŞİSEL TOOLLAR: çalışma kaydı renderer'a (TOOLS konsolu buradan beslenir) */
8364
+ try {
8365
+ customtools.setNotify((entry) => {
8366
+ try {
8367
+ if (win && !win.isDestroyed()) win.webContents.send('agent:event', { type: 'tool-log', ...entry });
8368
+ } catch {}
8369
+ });
8370
+ } catch {}
8371
+
8340
8372
  function financeEnsureBridge() {
8341
8373
  const f = finCfg();
8342
8374
  const candidates = [String(f.pythonPath || '').trim(), 'python', 'py -3'].filter(Boolean);
@@ -8695,23 +8727,32 @@ ipcMain.handle('finance:snapshot', async () => {
8695
8727
  ipcMain.handle('finance:mode', async (_e, payload) => {
8696
8728
  financeState.mode = !!(payload && payload.on);
8697
8729
  const sid = String((payload && payload.sessionId) || '');
8730
+ let needNew = false;
8698
8731
  if (sid && engine) {
8699
8732
  try {
8700
8733
  let s = engine.cache.get(sid);
8701
8734
  if (!s) { try { s = engine._load(sid); } catch {} }
8702
- if (s && !s.botId && !s.bgJob) {
8735
+ if (s && !s.bgJob && (!s.botId || s.botId === 'beast')) {
8703
8736
  if (financeState.mode) {
8704
- engine.markFinance(sid, false); /* kalıcı etiket: finance geçmişine girer */
8705
- finApplyTraderFields(s);
8706
- s.financeTrader = false; /* chat copilot'ı — trader değil */
8737
+ if (s.finance) {
8738
+ /* zaten finance oturumu — aynen sürdür */
8739
+ finApplyTraderFields(s);
8740
+ s.financeTrader = false; /* chat copilot'ı — trader değil */
8741
+ } else {
8742
+ /* NORMAL oturum (örn. WhatsApp sohbeti) finance modundayken ASLA
8743
+ finance'e çevrilmez — ayrı bir finance oturumu açılır.
8744
+ (eski davranış WhatsApp oturumunu da finance listesine karıştırıyordu) */
8745
+ needNew = true;
8746
+ }
8707
8747
  } else {
8708
- engine.unmarkFinance(sid); /* normal sohbete döner */
8748
+ /* mod kapandı: aktif finance oturumu normal sohbete döner */
8749
+ if (s.finance) engine.unmarkFinance(sid);
8709
8750
  }
8710
8751
  engine.cache.set(sid, s);
8711
8752
  }
8712
8753
  } catch {}
8713
8754
  }
8714
- return { ok: true, mode: financeState.mode };
8755
+ return { ok: true, mode: financeState.mode, needNew };
8715
8756
  });
8716
8757
 
8717
8758
  /* MT5 sembol seçici: terminaldeki tüm semboller (isteğe bağlı *filter*) */
package/src/preload.js CHANGED
@@ -22,6 +22,11 @@ contextBridge.exposeInMainWorld('beast', {
22
22
  agentsCancel: (id) => ipcRenderer.invoke('agents:cancel', id),
23
23
  agentDmsList: () => ipcRenderer.invoke('agent-dms:list'),
24
24
  agentDmsClear: () => ipcRenderer.invoke('agent-dms:clear'),
25
+ toolsList: () => ipcRenderer.invoke('tools:list'),
26
+ toolSave: (t) => ipcRenderer.invoke('tools:save', t),
27
+ toolDelete: (id) => ipcRenderer.invoke('tools:delete', id),
28
+ toolRun: (id, args) => ipcRenderer.invoke('tools:run', { id, args }),
29
+ toolsOpenFolder: () => ipcRenderer.invoke('tools:openFolder'),
25
30
  getCeoMode: () => ipcRenderer.invoke('ceo:get'),
26
31
  setCeoMode: (v) => ipcRenderer.invoke('ceo:set', v),
27
32
  listNotes: () => ipcRenderer.invoke('notes:list'),