beast-agent 2.19.1 → 2.20.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.20.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
  ) {
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'),
@@ -22,6 +22,16 @@
22
22
  </div>
23
23
  <div id="watchPaneList"></div>
24
24
  </div>
25
+ <div id="toolBox">
26
+ <div id="toolBoxHead">
27
+ <span id="toolBoxTitle">TOOLLAR</span>
28
+ <span class="bot-bar-btns">
29
+ <button id="toolFolderBtn" title="Toollar klasörünü aç (%APPDATA%\beast\tools)"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7z"/></svg></button>
30
+ <button id="toolAdd" title="Yeni tool yaz">+</button>
31
+ </span>
32
+ </div>
33
+ <div id="toolList"></div>
34
+ </div>
25
35
  <div id="botBar">
26
36
  <div id="botBarHead">
27
37
  <span id="botBarTitle" data-i18n="bot_head">BOTLAR</span>
@@ -363,10 +373,39 @@
363
373
  </aside>
364
374
  </div>
365
375
 
376
+ <div id="toolOverlay" hidden>
377
+ <div id="toolDialog">
378
+ <div id="toolDialogHead">
379
+ <span id="toolDialogTitle">YENİ TOOL</span>
380
+ <button id="toolClose" title="Kapat">&#x2715;&#xFE0E;</button>
381
+ </div>
382
+ <div class="tool-form">
383
+ <label>Tool adı (ajan tool__<ad> olarak çağırır)</label>
384
+ <input id="toolName" placeholder="örn: gold_emir" spellcheck="false" autocomplete="off" />
385
+ <label>Açıklama — ajan bu metni görüp ne zaman çağıracağını anlar</label>
386
+ <input id="toolDesc" placeholder="örn: GOLD için özel emir açar (side, lot, sl, tp)" spellcheck="false" autocomplete="off" />
387
+ <label>Parametre şeması (JSON)</label>
388
+ <textarea id="toolParams" rows="6" spellcheck="false" placeholder='{"type":"object","properties":{"side":{"type":"string"},"lot":{"type":"number"}},"required":["side"]}'></textarea>
389
+ <label>run.js kodu — stdin: JSON args · stdout: JSON sonuç (console.log)</label>
390
+ <textarea id="toolCode" rows="12" spellcheck="false" placeholder="const args = JSON.parse(...); console.log(JSON.stringify({ ok: true }));"></textarea>
391
+ <div id="toolFormErr" class="sub" style="color:var(--err)" hidden></div>
392
+ </div>
393
+ <div id="toolTestRow">
394
+ <input id="toolTestArgs" placeholder='deneme argümanları (JSON): {"mesaj":"merhaba"}' spellcheck="false" autocomplete="off" />
395
+ <button id="toolTestBtn">&#9654;&#xFE0E; Çalıştır</button>
396
+ </div>
397
+ <pre id="toolTestOut" hidden></pre>
398
+ <div id="toolDialogBtns">
399
+ <button id="toolDeleteBtn" class="tool-danger" hidden>Sil</button>
400
+ <span class="file-spacer"></span>
401
+ <button id="toolSaveBtn" class="tool-primary">Kaydet</button>
402
+ </div>
403
+ </div>
404
+ </div>
405
+
366
406
  <div id="settingsOverlay" hidden>
367
407
  <div id="settingsDialog">
368
- <aside id="setTabs">
369
- <div class="set-tabs-title" data-i18n="set_title">AYARLAR</div>
408
+ <aside id="setTabs"> <div class="set-tabs-title" data-i18n="set_title">AYARLAR</div>
370
409
  <nav class="set-tabs-list">
371
410
  <button class="tab active" data-tab="lang" data-i18n="tab_lang">Dil</button>
372
411
  <button class="tab" data-tab="provider" data-i18n="tab_provider">Provider</button>
@@ -48,6 +48,22 @@ const els = {
48
48
  watchBtn: $('#watchBtn'),
49
49
  watchPaneList: $('#watchPaneList'),
50
50
  watchPaneOpen: $('#watchPaneOpen'),
51
+ toolBoxAdd: $('#toolAdd'),
52
+ toolList: $('#toolList'),
53
+ toolFolderBtn: $('#toolFolderBtn'),
54
+ toolOverlay: $('#toolOverlay'),
55
+ toolClose: $('#toolClose'),
56
+ toolDialogTitle: $('#toolDialogTitle'),
57
+ toolName: $('#toolName'),
58
+ toolDesc: $('#toolDesc'),
59
+ toolParams: $('#toolParams'),
60
+ toolCode: $('#toolCode'),
61
+ toolFormErr: $('#toolFormErr'),
62
+ toolTestArgs: $('#toolTestArgs'),
63
+ toolTestBtn: $('#toolTestBtn'),
64
+ toolTestOut: $('#toolTestOut'),
65
+ toolSaveBtn: $('#toolSaveBtn'),
66
+ toolDeleteBtn: $('#toolDeleteBtn'),
51
67
  watchPaneLog: $('#watchPaneLog'),
52
68
  cronBtn: $('#cronBtn'),
53
69
  watchOverlay: $('#watchOverlay'),
@@ -5110,6 +5126,192 @@ if (els.dmClear) {
5110
5126
  });
5111
5127
  }
5112
5128
 
5129
+ /* ---------------- TOOLS konsolu (kişisel araçlar) ----------------
5130
+ %APPDATA%\beast\tools\<ad>\tool.json + run.js — kullanıcı kendi araçlarını
5131
+ yazar; finance dahil TÜM oturumlar tool__<ad> olarak çağırır. Sol panelde
5132
+ buradan görüntülenir, yazılır, test edilir, silinir. */
5133
+
5134
+ const toolUi = {
5135
+ tools: [],
5136
+ editId: null,
5137
+ logRows: [],
5138
+ };
5139
+
5140
+ const TOOL_EXAMPLE_CODE =
5141
+ "'use strict';\n" +
5142
+ "// KİŞİSEL TOOL — stdin: JSON args · stdout: JSON sonuç\n" +
5143
+ "const fs = require('fs');\n" +
5144
+ "let raw = '';\n" +
5145
+ "try { raw = fs.readFileSync(0, 'utf8'); } catch {}\n" +
5146
+ "let args = {};\n" +
5147
+ "try { args = JSON.parse(raw || '{}'); } catch {}\n" +
5148
+ "// Buraya kendi işini yaz (MT5 köprüsü, özel API, hesap, sinyal…)\n" +
5149
+ "console.log(JSON.stringify({ ok: true, echo: args }));\n";
5150
+
5151
+ let toolRenderTimer = null;
5152
+ function scheduleToolRender() {
5153
+ if (toolRenderTimer) return;
5154
+ toolRenderTimer = setTimeout(() => {
5155
+ toolRenderTimer = null;
5156
+ renderToolBox();
5157
+ }, 500);
5158
+ }
5159
+
5160
+ async function renderToolBox() {
5161
+ const list = els.toolList;
5162
+ if (!list) return;
5163
+ try { toolUi.tools = (await beast.toolsList()) || []; } catch { toolUi.tools = []; }
5164
+ list.textContent = '';
5165
+ if (!toolUi.tools.length) {
5166
+ const d = document.createElement('div');
5167
+ d.className = 'tool-empty';
5168
+ d.textContent = 'Tool yok — + ile kendi aracını yaz (Finance dahil tüm oturumlarda çalışır)';
5169
+ list.appendChild(d);
5170
+ return;
5171
+ }
5172
+ for (const t of toolUi.tools) {
5173
+ const row = document.createElement('div');
5174
+ row.className = 'tool-row';
5175
+ const last = toolUi.logRows.filter((l) => l.toolId === t.id).slice(-1)[0];
5176
+ const st = last ? (last.ok ? 'ok' : 'err') : 'idle';
5177
+ const tip = last
5178
+ ? 'son çalışma ' + dmTime(last.at) + (last.error ? ' · hata: ' + last.error : ' · ' + last.ms + ' ms')
5179
+ : 'hiç çalışmadı';
5180
+ row.innerHTML =
5181
+ '<span class="tool-dot ' + st + '" title="' + escapeHtml(tip) + '"></span>' +
5182
+ '<span class="tool-name" title="' + escapeHtml(t.description || '') + '">' + escapeHtml(t.name) + '</span>' +
5183
+ '<span class="tool-btns">' +
5184
+ '<button class="tb-run" title="Aç & test çalıştır">&#9654;&#xFE0E;</button>' +
5185
+ '<button class="tb-edit" title="Düzenle">&#9998;&#xFE0E;</button>' +
5186
+ '<button class="tb-del" title="Sil">&#10005;&#xFE0E;</button>' +
5187
+ '</span>';
5188
+ row.querySelector('.tb-run').addEventListener('click', () => openToolDialog(t));
5189
+ row.querySelector('.tb-edit').addEventListener('click', () => openToolDialog(t));
5190
+ row.querySelector('.tb-del').addEventListener('click', async () => {
5191
+ if (!(await uiConfirm('"' + t.name + '" toolu silinsin mi? (ajanlar artık çağıramaz)', 'Sil'))) return;
5192
+ const r = await beast.toolDelete(t.id).catch(() => ({ ok: false }));
5193
+ if (r && r.ok) {
5194
+ toast('Tool silindi');
5195
+ renderToolBox();
5196
+ } else toast((r && r.error) || 'Silinemedi');
5197
+ });
5198
+ list.appendChild(row);
5199
+ }
5200
+ }
5201
+
5202
+ function openToolDialog(t) {
5203
+ if (!els.toolOverlay) return;
5204
+ toolUi.editId = t ? t.id : null;
5205
+ els.toolDialogTitle.textContent = t ? 'TOOL · ' + t.name : 'YENİ TOOL';
5206
+ els.toolName.value = t ? t.name : '';
5207
+ els.toolDesc.value = t ? t.description || '' : '';
5208
+ els.toolParams.value = t
5209
+ ? JSON.stringify(t.parameters || { type: 'object', properties: {} }, null, 2)
5210
+ : '{\n "type": "object",\n "properties": {},\n "required": []\n}';
5211
+ els.toolCode.value = t ? t.code || TOOL_EXAMPLE_CODE : TOOL_EXAMPLE_CODE;
5212
+ els.toolTestArgs.value = t && t.parameters && Array.isArray(t.parameters.required) && t.parameters.required.length
5213
+ ? JSON.stringify(Object.fromEntries(t.parameters.required.map((k) => [k, ''])), null, 1)
5214
+ : '';
5215
+ els.toolTestOut.hidden = true;
5216
+ els.toolTestOut.textContent = '';
5217
+ els.toolFormErr.hidden = true;
5218
+ els.toolDeleteBtn.hidden = !t;
5219
+ els.toolOverlay.hidden = false;
5220
+ els.toolName.focus();
5221
+ }
5222
+
5223
+ function closeToolDialog() {
5224
+ if (els.toolOverlay) els.toolOverlay.hidden = true;
5225
+ }
5226
+
5227
+ function toolErr(msg) {
5228
+ if (els.toolFormErr) {
5229
+ els.toolFormErr.textContent = msg;
5230
+ els.toolFormErr.hidden = !msg;
5231
+ }
5232
+ }
5233
+
5234
+ if (els.toolBoxAdd) els.toolBoxAdd.addEventListener('click', () => openToolDialog(null));
5235
+ if (els.toolFolderBtn) els.toolFolderBtn.addEventListener('click', () => beast.toolsOpenFolder());
5236
+ if (els.toolClose) els.toolClose.addEventListener('click', closeToolDialog);
5237
+ if (els.toolOverlay) {
5238
+ els.toolOverlay.addEventListener('click', (e) => {
5239
+ if (e.target === els.toolOverlay) closeToolDialog();
5240
+ });
5241
+ els.toolOverlay.addEventListener('keydown', (e) => {
5242
+ if (e.key === 'Escape') { e.stopPropagation(); closeToolDialog(); }
5243
+ });
5244
+ }
5245
+ if (els.toolSaveBtn) {
5246
+ els.toolSaveBtn.addEventListener('click', async () => {
5247
+ toolErr('');
5248
+ const name = String(els.toolName.value || '').trim();
5249
+ if (!name) return toolErr('Tool adı gerekli');
5250
+ let parameters = null;
5251
+ try {
5252
+ parameters = JSON.parse(els.toolParams.value || '{}');
5253
+ } catch {
5254
+ return toolErr('Parametre şeması geçerli JSON değil');
5255
+ }
5256
+ const code = String(els.toolCode.value || '').trim();
5257
+ if (!code) return toolErr('run.js kodu boş olamaz');
5258
+ const r = await beast
5259
+ .toolSave({ id: toolUi.editId, name, description: String(els.toolDesc.value || ''), parameters, code })
5260
+ .catch((e) => ({ ok: false, error: String((e && e.message) || e) }));
5261
+ if (r && r.ok) {
5262
+ closeToolDialog();
5263
+ toast('Tool kaydedildi — ajanlar tool__' + r.id + ' olarak çağırabilir (Finance dahil)');
5264
+ renderToolBox();
5265
+ } else {
5266
+ toolErr((r && r.error) || 'Kaydedilemedi');
5267
+ }
5268
+ });
5269
+ }
5270
+ if (els.toolDeleteBtn) {
5271
+ els.toolDeleteBtn.addEventListener('click', async () => {
5272
+ if (!toolUi.editId) return;
5273
+ const t = toolUi.tools.find((x) => x.id === toolUi.editId);
5274
+ if (!(await uiConfirm('"' + ((t && t.name) || toolUi.editId) + '" toolu silinsin mi?', 'Sil'))) return;
5275
+ const r = await beast.toolDelete(toolUi.editId).catch(() => ({ ok: false }));
5276
+ if (r && r.ok) {
5277
+ closeToolDialog();
5278
+ toast('Tool silindi');
5279
+ renderToolBox();
5280
+ } else toast((r && r.error) || 'Silinemedi');
5281
+ });
5282
+ }
5283
+ if (els.toolTestBtn) {
5284
+ els.toolTestBtn.addEventListener('click', async () => {
5285
+ toolErr('');
5286
+ let args = {};
5287
+ try {
5288
+ args = JSON.parse(els.toolTestArgs.value || '{}');
5289
+ } catch {
5290
+ return toolErr('Test argümanları geçerli JSON değil');
5291
+ }
5292
+ const id = toolUi.editId || String(els.toolName.value || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '_').replace(/^_+|_+$/g, '');
5293
+ if (!id) return toolErr('Önce tool adı gir');
5294
+ /* önce kaydet (yeni tool ilk testte de diskte olsun) */
5295
+ const sr = await beast
5296
+ .toolSave({
5297
+ id: toolUi.editId,
5298
+ name: String(els.toolName.value || id),
5299
+ description: String(els.toolDesc.value || ''),
5300
+ parameters: (() => {
5301
+ try { return JSON.parse(els.toolParams.value || '{}'); } catch { return null; }
5302
+ })() || { type: 'object', properties: {} },
5303
+ code: String(els.toolCode.value || ''),
5304
+ })
5305
+ .catch((e) => ({ ok: false, error: String((e && e.message) || e) }));
5306
+ if (!sr || !sr.ok) return toolErr((sr && sr.error) || 'Tool kaydedilemedi');
5307
+ els.toolTestOut.hidden = false;
5308
+ els.toolTestOut.textContent = 'çalışıyor…';
5309
+ const r = await beast.toolRun(sr.id, args).catch((e) => ({ ok: false, error: String((e && e.message) || e) }));
5310
+ els.toolTestOut.textContent = JSON.stringify(r, null, 2).slice(0, 8000);
5311
+ renderToolBox();
5312
+ });
5313
+ }
5314
+
5113
5315
  function renderAgentsPane() {
5114
5316
  const pane = $('#tab-agents');
5115
5317
  if (!pane) return;
@@ -5268,6 +5470,20 @@ function onEvent(ev) {
5268
5470
  if ($('#dmRail') && !$('#dmRail').classList.contains('dm-hidden')) renderDmRail();
5269
5471
  return;
5270
5472
  }
5473
+ /* KİŞİSEL TOOL çalışma kaydı — TOOLS konsolunun nokta göstergeleri */
5474
+ if (ev.type === 'tool-log') {
5475
+ toolUi.logRows.push({
5476
+ at: ev.at,
5477
+ tool: ev.tool,
5478
+ toolId: ev.toolId,
5479
+ ok: ev.ok,
5480
+ ms: ev.ms,
5481
+ error: ev.error,
5482
+ });
5483
+ if (toolUi.logRows.length > 60) toolUi.logRows.splice(0, toolUi.logRows.length - 60);
5484
+ scheduleToolRender();
5485
+ return;
5486
+ }
5271
5487
  /* OFFLINE MESAJ KUYRUĞU: bağlantı + kuyruk olayları (sessionId filtresinden önce) */
5272
5488
  if (ev.type === 'net' || ev.type === 'netQueue') { onNetEvent(ev); return; }
5273
5489
  /* Sandbox klonlama progress'i (sessionId'siz global olay) */
@@ -6415,6 +6631,7 @@ async function init() {
6415
6631
  };
6416
6632
 
6417
6633
  refreshAgentsPane();
6634
+ renderToolBox(); // TOOLS konsolu: kişisel araçlar sol panelde
6418
6635
 
6419
6636
  els.sendBtn.addEventListener('click', sendCurrent);
6420
6637
  els.stopBtn.addEventListener('click', () => {
@@ -8616,7 +8833,15 @@ async function setFinanceMode(on) {
8616
8833
  const brandSub = document.querySelector('#brand .brand-sub');
8617
8834
  if (brandSub) brandSub.textContent = on ? 'Finance' : 'Agent';
8618
8835
  try { localStorage.setItem('beast.financeMode', on ? '1' : '0'); } catch {}
8619
- beast.financeMode(!!on, activeId).catch(() => {});
8836
+ const fm = await beast.financeMode(!!on, activeId).catch(() => ({}));
8837
+ /* finance modu açıldı ama aktif oturum NORMAL (örn. WhatsApp sohbeti) ise
8838
+ ASLA o oturumu finance'e çevirme — kendine ÖZEL finance oturumu aç */
8839
+ if (on && fm && fm.needNew) {
8840
+ try {
8841
+ const created = await beast.createSession();
8842
+ if (created && created.id) await openSession(created.id);
8843
+ } catch {}
8844
+ }
8620
8845
  /* sohbet geçmişi listesi değişir: finance modunda yalnız finance sohbetleri */
8621
8846
  try {
8622
8847
  await refreshSessions();
@@ -2274,6 +2274,225 @@ body.rail-hidden #dmRailHead { padding-right: 148px; }
2274
2274
  letter-spacing: 1.5px;
2275
2275
  color: var(--muted);
2276
2276
  }
2277
+
2278
+ /* ---------- sol panel: TOOLS konsolu (kişisel araçlar) ---------- */
2279
+ #toolBox {
2280
+ flex: none;
2281
+ max-height: 32%;
2282
+ display: none;
2283
+ flex-direction: column;
2284
+ border-top: 1px solid var(--border);
2285
+ padding: 6px 8px 6px;
2286
+ }
2287
+ body:not(.ide-mode):not(.studio-mode):not(.sandbox-mode) #toolBox { display: flex; }
2288
+ #toolBoxHead {
2289
+ display: flex;
2290
+ align-items: center;
2291
+ justify-content: space-between;
2292
+ padding: 2px 2px 6px;
2293
+ font-size: 10.5px;
2294
+ font-weight: 800;
2295
+ letter-spacing: 1.5px;
2296
+ color: var(--muted);
2297
+ }
2298
+ #toolList {
2299
+ min-height: 40px;
2300
+ max-height: 200px;
2301
+ overflow-y: auto;
2302
+ display: flex;
2303
+ flex-direction: column;
2304
+ gap: 2px;
2305
+ }
2306
+ .tool-empty {
2307
+ color: var(--muted);
2308
+ font-size: 10.5px;
2309
+ line-height: 1.5;
2310
+ padding: 6px 2px;
2311
+ }
2312
+ .tool-row {
2313
+ display: flex;
2314
+ align-items: center;
2315
+ gap: 6px;
2316
+ min-width: 0;
2317
+ padding: 3px 4px;
2318
+ border-radius: 6px;
2319
+ cursor: pointer;
2320
+ color: var(--text);
2321
+ }
2322
+ .tool-row:hover { background: var(--panel2); }
2323
+ .tool-dot {
2324
+ flex: none;
2325
+ width: 7px;
2326
+ height: 7px;
2327
+ border-radius: 50%;
2328
+ background: var(--border);
2329
+ }
2330
+ .tool-dot.ok { background: #43c463; }
2331
+ .tool-dot.err { background: var(--err); }
2332
+ .tool-name {
2333
+ flex: 1;
2334
+ min-width: 0;
2335
+ font-size: 12px;
2336
+ font-weight: 600;
2337
+ white-space: nowrap;
2338
+ overflow: hidden;
2339
+ text-overflow: ellipsis;
2340
+ }
2341
+ .tool-btns {
2342
+ flex: none;
2343
+ display: flex;
2344
+ gap: 2px;
2345
+ visibility: hidden;
2346
+ }
2347
+ .tool-row:hover .tool-btns { visibility: visible; }
2348
+ .tool-btns button {
2349
+ border: none;
2350
+ background: none;
2351
+ color: var(--muted);
2352
+ font-size: 11px;
2353
+ cursor: pointer;
2354
+ padding: 2px 5px;
2355
+ border-radius: 4px;
2356
+ }
2357
+ .tool-btns button:hover { background: var(--accent-dim); color: var(--accent); }
2358
+ .tool-btns .tb-del:hover { color: var(--err); background: rgba(220, 60, 60, 0.12); }
2359
+
2360
+ /* ---------- TOOL editör dialogu ---------- */
2361
+ #toolOverlay {
2362
+ position: fixed;
2363
+ inset: 0;
2364
+ background: rgba(0, 0, 0, 0.45);
2365
+ z-index: 300;
2366
+ display: flex;
2367
+ align-items: center;
2368
+ justify-content: center;
2369
+ }
2370
+ #toolOverlay[hidden] { display: none; }
2371
+ #toolDialog {
2372
+ width: min(760px, 92vw);
2373
+ max-height: 88vh;
2374
+ overflow-y: auto;
2375
+ background: var(--panel);
2376
+ border: 1px solid var(--border);
2377
+ border-radius: 12px;
2378
+ padding: 14px 16px;
2379
+ display: flex;
2380
+ flex-direction: column;
2381
+ gap: 10px;
2382
+ }
2383
+ #toolDialogHead {
2384
+ display: flex;
2385
+ align-items: center;
2386
+ justify-content: space-between;
2387
+ font-size: 13px;
2388
+ font-weight: 800;
2389
+ letter-spacing: 1px;
2390
+ color: var(--text);
2391
+ }
2392
+ #toolDialogHead button {
2393
+ border: none;
2394
+ background: none;
2395
+ color: var(--muted);
2396
+ font-size: 13px;
2397
+ cursor: pointer;
2398
+ padding: 2px 6px;
2399
+ border-radius: 6px;
2400
+ }
2401
+ #toolDialogHead button:hover { color: var(--err); background: rgba(220, 60, 60, 0.1); }
2402
+ .tool-form {
2403
+ display: flex;
2404
+ flex-direction: column;
2405
+ gap: 4px;
2406
+ }
2407
+ .tool-form label {
2408
+ font-size: 10.5px;
2409
+ font-weight: 700;
2410
+ letter-spacing: 0.5px;
2411
+ color: var(--muted);
2412
+ margin-top: 4px;
2413
+ }
2414
+ .tool-form input,
2415
+ .tool-form textarea {
2416
+ width: 100%;
2417
+ background: var(--bg);
2418
+ color: var(--text);
2419
+ border: 1px solid var(--border);
2420
+ border-radius: 8px;
2421
+ padding: 7px 9px;
2422
+ font-size: 12px;
2423
+ font-family: Consolas, monospace;
2424
+ }
2425
+ .tool-form input { font-family: inherit; }
2426
+ .tool-form textarea { resize: vertical; min-height: 60px; }
2427
+ .tool-form input:focus,
2428
+ .tool-form textarea:focus { outline: none; border-color: var(--accent); }
2429
+ #toolTestRow {
2430
+ display: flex;
2431
+ gap: 6px;
2432
+ }
2433
+ #toolTestArgs {
2434
+ flex: 1;
2435
+ background: var(--bg);
2436
+ color: var(--text);
2437
+ border: 1px solid var(--border);
2438
+ border-radius: 8px;
2439
+ padding: 7px 9px;
2440
+ font-size: 12px;
2441
+ font-family: Consolas, monospace;
2442
+ }
2443
+ #toolTestArgs:focus { outline: none; border-color: var(--accent); }
2444
+ #toolTestBtn {
2445
+ border: none;
2446
+ background: var(--accent-dim);
2447
+ color: var(--accent);
2448
+ font-weight: 800;
2449
+ font-size: 12px;
2450
+ border-radius: 8px;
2451
+ padding: 7px 12px;
2452
+ cursor: pointer;
2453
+ }
2454
+ #toolTestBtn:hover { filter: brightness(1.1); }
2455
+ #toolTestOut {
2456
+ margin: 0;
2457
+ max-height: 220px;
2458
+ overflow: auto;
2459
+ background: var(--bg);
2460
+ border: 1px solid var(--border);
2461
+ border-radius: 8px;
2462
+ padding: 8px 10px;
2463
+ font-size: 11px;
2464
+ line-height: 1.5;
2465
+ white-space: pre-wrap;
2466
+ word-break: break-word;
2467
+ color: var(--text);
2468
+ }
2469
+ #toolDialogBtns {
2470
+ display: flex;
2471
+ align-items: center;
2472
+ gap: 8px;
2473
+ }
2474
+ .tool-primary {
2475
+ border: none;
2476
+ background: var(--accent);
2477
+ color: #fff;
2478
+ font-weight: 800;
2479
+ font-size: 12.5px;
2480
+ border-radius: 8px;
2481
+ padding: 8px 16px;
2482
+ cursor: pointer;
2483
+ }
2484
+ .tool-primary:hover { filter: brightness(1.08); }
2485
+ .tool-danger {
2486
+ border: none;
2487
+ background: rgba(220, 60, 60, 0.12);
2488
+ color: var(--err);
2489
+ font-weight: 700;
2490
+ font-size: 12px;
2491
+ border-radius: 8px;
2492
+ padding: 8px 14px;
2493
+ cursor: pointer;
2494
+ }
2495
+ .tool-danger:hover { background: rgba(220, 60, 60, 0.2); }
2277
2496
  .bot-bar-btns { display: flex; gap: 4px; }
2278
2497
  .bot-bar-btns button {
2279
2498
  border: none; background: none; color: var(--muted);