beast-agent 2.48.0 → 2.49.1

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.48.0",
4
+ "version": "2.49.1",
5
5
  "description": "Ultra-fast local agent shell for Windows.",
6
6
  "author": "algokodcom (AlgoKod)",
7
7
  "license": "MIT",
@@ -20,6 +20,14 @@ function setNotify(fn) {
20
20
  notify = typeof fn === 'function' ? fn : () => {};
21
21
  }
22
22
 
23
+ /* KİŞİSEL MT5 TOOLLARI köprüyü doğrudan çağırır (iç mt5_* handler'ı devreye
24
+ girmez) — başarılı çağrıyı trade olayı olarak ana sürece bildir ki jurnal +
25
+ panel/kanal bildirimi (işlem açıldı, SL/TP güncellendi…) kaçmasın. */
26
+ let tradeHook = null;
27
+ function setTradeHook(fn) {
28
+ tradeHook = typeof fn === 'function' ? fn : null;
29
+ }
30
+
23
31
  function dir() {
24
32
  return path.join(beastRoot(), 'tools');
25
33
  }
@@ -117,7 +125,7 @@ function names() {
117
125
  const TOOL_TIMEOUT_MS = 90 * 1000;
118
126
 
119
127
  /* Tool'u çocuk node prosesinde koşturur — uygulama asla çökmez */
120
- function call(name, args) {
128
+ function call(name, args, sid) {
121
129
  return new Promise((resolve) => {
122
130
  const id = String(name || '').replace(/^tool__/, '');
123
131
  if (!id) return resolve({ ok: false, error: 'tool adı gerekli' });
@@ -142,6 +150,9 @@ function call(name, args) {
142
150
  error: r && r.error ? String(r.error).slice(0, 200) : null,
143
151
  });
144
152
  } catch {}
153
+ try {
154
+ if (tradeHook) tradeHook(id, args || {}, r || {}, sid ? String(sid) : '');
155
+ } catch {}
145
156
  resolve(r);
146
157
  };
147
158
  let killer = setTimeout(() => {
@@ -154,6 +165,7 @@ function call(name, args) {
154
165
  const env = {
155
166
  ...process.env,
156
167
  BEAST_ROOT: beastRoot(),
168
+ BEAST_APP_DIR: path.join(__dirname, '..', '..'), // tool'lar uygulama node_modules'ına (ör. @napi-rs/canvas) buradan erişir
157
169
  BEAST_TOOL_ID: id,
158
170
  BEAST_TOOL_ARGS: JSON.stringify(args || {}),
159
171
  BEAST_FINANCE_DIR: path.join(beastRoot(), 'finance'),
@@ -245,6 +257,37 @@ function copyTree(srcDir, dstDir) {
245
257
  }
246
258
  }
247
259
 
260
+ function readJsonSafe(fp) {
261
+ try { return JSON.parse(fs.readFileSync(fp, 'utf8')); } catch { return null; }
262
+ }
263
+
264
+ /* güncelleme öncesi: kullanıcının düzenlediği dosyaları .user-bak olarak sakla */
265
+ function backupEditedFiles(fromDir, toDir) {
266
+ try {
267
+ for (const e of fs.readdirSync(fromDir, { withFileTypes: true })) {
268
+ if (!e.isDirectory()) continue;
269
+ backupEditedFiles(path.join(fromDir, e.name), path.join(toDir, e.name));
270
+ }
271
+ } catch {}
272
+ try {
273
+ for (const e of fs.readdirSync(fromDir, { withFileTypes: true })) {
274
+ if (e.isDirectory()) continue;
275
+ if (e.name === 'tool.json') continue; /* sürüm damgası her güncellemede değişir */
276
+ const from = path.join(fromDir, e.name);
277
+ const to = path.join(toDir, e.name);
278
+ try {
279
+ if (!fs.existsSync(to)) continue;
280
+ if (fs.readFileSync(from).equals(fs.readFileSync(to))) continue;
281
+ const bak = to + '.user-bak';
282
+ if (!fs.existsSync(bak)) fs.copyFileSync(to, bak);
283
+ } catch {}
284
+ }
285
+ } catch {}
286
+ }
287
+
288
+ /* SÜRÜM GÜNCELLEMESİ: shipped tool.json'da "defaultsVersion" varsa ve kurulu
289
+ sürümden büyükse araç güncellenir (ör. mt5_shot çoklu periyot); düzenlenmiş
290
+ dosyalar .user-bak olarak yedeklenir. Alan yoksa eski davranış: yalnız eksikse kur. */
248
291
  function seedDefaults() {
249
292
  const src = path.join(__dirname, 'defaulttools');
250
293
  try {
@@ -253,13 +296,27 @@ function seedDefaults() {
253
296
  let n = 0;
254
297
  for (const e of fs.readdirSync(src, { withFileTypes: true })) {
255
298
  if (!e.isDirectory()) continue;
299
+ const from = path.join(src, e.name);
256
300
  const to = path.join(dir(), e.name);
301
+ const shipped = readJsonSafe(path.join(from, 'tool.json'));
302
+ const shippedV = Number(shipped && shipped.defaultsVersion) || 0;
257
303
  /* tam kurulu mu? tool.json + run.js yoksa yarım demektir → yeniden kur */
258
304
  const complete = fs.existsSync(path.join(to, 'tool.json')) && fs.existsSync(path.join(to, 'run.js'));
259
- if (complete) continue;
305
+ let need = !complete;
306
+ if (complete && shippedV > 0) {
307
+ const installed = readJsonSafe(path.join(to, 'tool.json'));
308
+ const installedV = Number(installed && installed.defaultsVersion) || 0;
309
+ if (shippedV > installedV) need = true;
310
+ }
311
+ if (!need) continue;
260
312
  try {
261
- if (fs.existsSync(to)) fs.rmSync(to, { recursive: true, force: true });
262
- copyTree(path.join(src, e.name), to);
313
+ if (complete) {
314
+ backupEditedFiles(from, to); /* kullanıcı düzenlemesi kaybolmasın */
315
+ copyTree(from, to); /* güncelle: shots/ ve ek dosyalar korunur */
316
+ } else {
317
+ if (fs.existsSync(to)) fs.rmSync(to, { recursive: true, force: true });
318
+ copyTree(from, to);
319
+ }
263
320
  n++;
264
321
  } catch {}
265
322
  }
@@ -299,4 +356,4 @@ function seedIfEmpty() {
299
356
  } catch {}
300
357
  }
301
358
 
302
- module.exports = { setNotify, list, scan, definitions, names, call, save, remove, seedIfEmpty, seedDefaults, invalidate, dir, slugify };
359
+ module.exports = { setNotify, setTradeHook, list, scan, definitions, names, call, save, remove, seedIfEmpty, seedDefaults, invalidate, dir, slugify };
@@ -2,9 +2,15 @@
2
2
  /* mt5_shot — MT5 grafiğinden GERÇEK PNG screenshot (BeastFinance EA "shot" komutu)
3
3
  stdin: JSON args -> stdout: SADECE JSON sonuç
4
4
 
5
- Girdi : {symbol?, timeframe?, width?, height?, file?, template?, timeoutSec?, outdir?, diag?, embed?}
5
+ Girdi : {symbol?, timeframe?, timeframes?:["M1","M15"], layout?:"h"|"v", width?, height?, file?, template?, timeoutSec?, outdir?, diag?, embed?}
6
6
  Çıktı : {ok:true, path, symbol, timeframe, ...} | {ok:false, err, error, cmdId, filesDir, ...}
7
- err kodları: no_terminal | ea_offline | cmd_write_fail | ea_no_ack | ea_error | png_yok | timeout
7
+ err kodları: no_terminal | ea_offline | cmd_write_fail | ea_no_ack | ea_error | png_yok | timeout | stitch_fail | stitch_write_fail
8
+
9
+ ÇOKLU PERİYOT: timeframes:["M1","M15"] (2-3 adet) verilirse her periyot ayrı
10
+ çekilir ve TEK PNG'de birleştirilir (layout:"h" soldan sağa — varsayılan,
11
+ "v" üstten alta). Panellerin sol üstüne "SEMBOL · PERİYOT" etiketi çizilir;
12
+ sonuçta panels[{timeframe, position, path}] hangi tarafın hangi periyot
13
+ olduğunu söyler. tf2:"M15" kısayolu da (timeframe + tf2) kabul edilir.
8
14
 
9
15
  Notlar:
10
16
  - beast_cmd.json'a {id, ts, cmd, params} yazılır (UTF-16 BOM; EA FILE_UNICODE okur).
@@ -159,89 +165,75 @@ function eaPing(filesDir, budgetMs) {
159
165
  }
160
166
 
161
167
  /* ---------------- ana akış ---------------- */
162
- function run() {
163
- const timeoutSec = Number(ARGS.timeoutSec) > 0
164
- ? Math.max(5, Math.min(70, Math.round(Number(ARGS.timeoutSec))))
165
- : DEFAULT_TIMEOUT_SEC;
166
- const diag = ARGS.diag !== false;
167
-
168
- const term = findTerminalData();
169
- if (!term) return fail('no_terminal', 'MT5 terminal veri klasörü bulunamadı (beast_ea.json yok) — MT5 kurulu/çalışıyor mu?');
170
- const filesDir = term.files;
171
- CTX.filesDir = filesDir;
172
168
 
173
- const cmdFile = path.join(filesDir, 'beast_cmd.json');
174
- const ackFile = path.join(filesDir, 'beast_cmd_ack.json');
175
- const beatFile = path.join(filesDir, 'beast_ea.json');
176
- const beatAge = heartbeatAge(beatFile);
177
- if (beatAge !== null && beatAge > 60) {
178
- return fail('ea_offline', 'BeastFinance EA canlı değil (son heartbeat ' + beatAge + ' sn önce) — grafikte EA yüklü ve terminal açık mı?',
179
- { heartbeat_age_sec: beatAge });
169
+ /* çoklu periyot listesi: ["M1","M15"] ya da "M1,M15"; tf2 kısayolu destekli */
170
+ function normalizeTfs(a) {
171
+ let t = a.timeframes;
172
+ if ((t == null || t === '') && a.tf2) t = [a.timeframe, a.tf2];
173
+ if (typeof t === 'string') t = t.split(/[,\s|+/]+/);
174
+ if (!Array.isArray(t)) return [];
175
+ const out = [];
176
+ for (const x of t) {
177
+ const s = String(x == null ? '' : x).trim().toUpperCase();
178
+ if (s && !out.includes(s)) out.push(s);
180
179
  }
180
+ return out.slice(0, 3); /* 80 sn watchdog bütçesi: en fazla 3 panel */
181
+ }
181
182
 
182
- const locked = acquireLock(Math.min(15000, timeoutSec * 500));
183
- if (!locked) console.error('[mt5_shot] uyarı: kilit alınamadı, yine de devam ediliyor');
184
-
185
- const id = 'S' + Date.now().toString(36);
186
- CTX.cmdId = id;
187
-
188
- let file = (ARGS.file && String(ARGS.file).trim()) ? String(ARGS.file).trim() : ('beast_shot_' + id + '.png');
189
- file = path.basename(file); // yol kaçışını engelle
190
- if (!/\.(png|gif|bmp|jpg|jpeg)$/i.test(file)) file += '.png';
191
-
192
- const params = { file };
193
- if (ARGS.timeframe) params.timeframe = String(ARGS.timeframe).toUpperCase();
194
- if (ARGS.symbol) params.symbol = String(ARGS.symbol).toUpperCase();
195
- if (ARGS.template !== undefined) params.template = String(ARGS.template);
196
- params.width = Number(ARGS.width) > 0 ? Math.min(10000, Math.round(Number(ARGS.width))) : 1600;
197
- params.height = Number(ARGS.height) > 0 ? Math.min(10000, Math.round(Number(ARGS.height))) : 900;
198
-
199
- const payload = { id, ts: Date.now(), cmd: 'shot', params };
183
+ function sanitizeFile(name, fallback) {
184
+ let f = (name && String(name).trim()) ? path.basename(String(name).trim()) : fallback;
185
+ if (!/\.(png|gif|bmp|jpg|jpeg)$/i.test(f)) f += '.png';
186
+ return f;
187
+ }
200
188
 
201
- try { if (fs.existsSync(ackFile)) fs.unlinkSync(ackFile); } catch (e) {}
202
- try { writeUtf16(cmdFile, JSON.stringify(payload)); }
203
- catch (e) { return fail('cmd_write_fail', 'beast_cmd.json yazılamadı: ' + e.message, { sent: payload }); }
189
+ /* tek kare: EA'ya shot komutu yaz → ack bekle → PNG'yi bekle */
190
+ function captureOne(o) {
191
+ CTX.cmdId = o.id;
192
+ const payload = { id: o.id, ts: Date.now(), cmd: 'shot', params: o.params };
193
+ try { if (fs.existsSync(o.ackFile)) fs.unlinkSync(o.ackFile); } catch (e) {}
194
+ try { writeUtf16(o.cmdFile, JSON.stringify(payload)); }
195
+ catch (e) { return { ok: false, err: 'cmd_write_fail', error: 'beast_cmd.json yazılamadı: ' + e.message, extra: { sent: payload } }; }
204
196
 
205
197
  /* --- ack POLL (cmd dosyasının EA tarafından tüketilmesi de izlenir) --- */
206
198
  let ack = null, cmdConsumed = false;
207
- const ackDeadline = Date.now() + timeoutSec * 1000;
199
+ const ackDeadline = Date.now() + o.timeoutSec * 1000;
208
200
  while (Date.now() < ackDeadline) {
209
201
  syncSleep(250);
210
- const a = readJson(ackFile);
211
- if (a && a.id === id) { ack = a; break; }
202
+ const a = readJson(o.ackFile);
203
+ if (a && a.id === o.id) { ack = a; break; }
212
204
  try {
213
- if (!fs.existsSync(cmdFile)) cmdConsumed = true;
205
+ if (!fs.existsSync(o.cmdFile)) cmdConsumed = true;
214
206
  else {
215
- const c = readJson(cmdFile);
216
- if (c && c.id && c.id !== id) cmdConsumed = true; // başka bir çağrı ezip geçti
207
+ const c = readJson(o.cmdFile);
208
+ if (c && c.id && c.id !== o.id) cmdConsumed = true; // başka bir çağrı ezip geçti
217
209
  }
218
210
  } catch (e) {}
219
211
  }
220
212
 
221
213
  if (!ack) {
222
214
  const extra = {
223
- heartbeat_age_sec: heartbeatAge(beatFile),
215
+ heartbeat_age_sec: heartbeatAge(o.beatFile),
224
216
  cmd_consumed: cmdConsumed,
225
- timeout_sec: timeoutSec,
217
+ timeout_sec: o.timeoutSec,
226
218
  sent: payload
227
219
  };
228
- if (diag) {
220
+ if (o.diag) {
229
221
  extra.diagnostic = cmdConsumed
230
222
  ? 'EA komutu aldı ama ack yazmadı (shot işleyicisi hata verdi / PNG üretimi bloke)'
231
223
  : 'EA komutu hiç almadı (zamanlayıcı durmuş ya da komut dosyası okunamadı)';
232
- extra.ping = eaPing(filesDir, 6000);
224
+ extra.ping = eaPing(o.filesDir, 6000);
233
225
  }
234
- return fail('ea_no_ack', 'EA ' + timeoutSec + ' sn içinde ack vermedi (id=' + id + ', heartbeat ' +
235
- (extra.heartbeat_age_sec === null ? '?' : extra.heartbeat_age_sec) + ' sn, cmd tüketildi=' + cmdConsumed + ')', extra);
226
+ return { ok: false, err: 'ea_no_ack', error: 'EA ' + o.timeoutSec + ' sn içinde ack vermedi (id=' + o.id + ', heartbeat ' +
227
+ (extra.heartbeat_age_sec === null ? '?' : extra.heartbeat_age_sec) + ' sn, cmd tüketildi=' + cmdConsumed + ')', extra };
236
228
  }
237
229
 
238
230
  if (ack.ok === false) {
239
- return fail('ea_error', 'EA hata döndürdü: ' + (ack.error || 'bilinmeyen'), { ack });
231
+ return { ok: false, err: 'ea_error', error: 'EA hata döndürdü: ' + (ack.error || 'bilinmeyen'), extra: { ack } };
240
232
  }
241
233
 
242
234
  /* --- PNG dosyasını bekle --- */
243
- const shotFile = (ack.result && ack.result.file) ? path.basename(String(ack.result.file)) : file;
244
- const src = path.join(filesDir, shotFile);
235
+ const shotFile = (ack.result && ack.result.file) ? path.basename(String(ack.result.file)) : o.params.file;
236
+ const src = path.join(o.filesDir, shotFile);
245
237
  let size = 0;
246
238
  const pngDeadline = Math.max(ackDeadline, Date.now() + 8000);
247
239
  for (;;) {
@@ -250,12 +242,14 @@ function run() {
250
242
  syncSleep(250);
251
243
  }
252
244
  if (size <= 1500) {
253
- return fail('png_yok', 'PNG oluşmadı ya da çok küçük (' + size + ' byte): ' + shotFile,
254
- { ack, expected_file: shotFile, bytes: size });
245
+ return { ok: false, err: 'png_yok', error: 'PNG oluşmadı ya da çok küçük (' + size + ' byte): ' + shotFile, extra: { ack, expected_file: shotFile, bytes: size } };
255
246
  }
247
+ return { ok: true, src, shotFile, size, res: ack.result || {}, params: o.params, id: o.id };
248
+ }
256
249
 
257
- /* --- kopyala: Beast altı yedek + send_file edilebilir kullanıcı klasörü --- */
258
- const pubDir = (ARGS.outdir && String(ARGS.outdir).trim()) ? path.resolve(String(ARGS.outdir).trim()) : PUB_DIR_DEFAULT;
250
+ /* --- kopyala: Beast altı yedek + send_file edilebilir kullanıcı klasörü --- */
251
+ function copyOut(src, shotFile, outdir) {
252
+ const pubDir = (outdir && String(outdir).trim()) ? path.resolve(String(outdir).trim()) : PUB_DIR_DEFAULT;
259
253
  let pubPath = null, beastPath = null, pubErr = null;
260
254
  try {
261
255
  if (!fs.existsSync(SHOT_DIR)) fs.mkdirSync(SHOT_DIR, { recursive: true });
@@ -267,30 +261,176 @@ function run() {
267
261
  pubPath = path.join(pubDir, shotFile);
268
262
  fs.copyFileSync(src, pubPath);
269
263
  } catch (e) { pubErr = e.message; }
264
+ return { pubPath, beastPath, pubErr };
265
+ }
270
266
 
271
- const res = (ack.result || {});
272
- const tfTxt = (res.period ? tfName(res.period) : null) || params.timeframe || null;
273
- const wantSym = params.symbol || res.symbol || null;
274
- const mismatch = !!(params.symbol && res.symbol && String(res.symbol).toUpperCase() !== String(params.symbol).toUpperCase());
275
-
276
- /* --- GÖRSELİ ÇAĞIRAN AJANA GÖSTER: PNG data URL olarak sonuca gömülür;
277
- engine bunu __injectImage sözleşmesiyle sonraki turda vision mesajı yapar
278
- (ajan grafiği GERÇEKTEN görür). embed:false ile kapatılır; ~3.5MB üstü
279
- bağlam/dekont şişmesin diye gömülmez (path yine döner). --- */
267
+ /* --- PNG data URL (engine __injectImage sözleşmesiyle ajana görsel enjekte eder) --- */
268
+ function embedData(src, size) {
280
269
  let dataUrl = null;
281
270
  try {
282
271
  if (ARGS.embed !== false && size > 0 && size <= 3500000) {
283
272
  dataUrl = 'data:image/png;base64,' + fs.readFileSync(src).toString('base64');
284
273
  }
285
274
  } catch (e) {}
275
+ return dataUrl;
276
+ }
277
+
278
+ /* ---- ÇOKLU PERİYOT: aynı sembolün N zaman dilimini TEK PNG'de birleştir ----
279
+ Paneller sırayla çekilir (kilit zaten alınmış — ack çakışması yok), sol üste
280
+ "SEMBOL · PERİYOT" etiketi çizilir; soldan sağa (layout:"v" ile üstten alta). */
281
+ async function runMulti(tfList, filesDir, cmdFile, ackFile, beatFile, timeoutSec, diag) {
282
+ let compose = null;
283
+ try { compose = require('./stitch.js').compose; } catch (e) {}
284
+ if (!compose) return { err: 'stitch_fail', error: 'stitch.js bulunamadı (mt5_shot güncel kurulum gerekiyor)' };
285
+
286
+ const n = tfList.length;
287
+ const baseId = 'S' + Date.now().toString(36);
288
+ const fileArg = ARGS.file && String(ARGS.file).trim() ? String(ARGS.file).trim() : '';
289
+ const base = fileArg ? fileArg.replace(/\.[a-z0-9]+$/i, '') : 'beast_mtf_' + baseId;
290
+ const perAck = Math.max(5, Math.min(15, Math.floor(45 / n)));
291
+ const width = Number(ARGS.width) > 0 ? Math.min(10000, Math.round(Number(ARGS.width))) : 1600;
292
+ const height = Number(ARGS.height) > 0 ? Math.min(10000, Math.round(Number(ARGS.height))) : 900;
293
+ const layout = String(ARGS.layout || 'h').toLowerCase() === 'v' ? 'v' : 'h';
294
+
295
+ const panels = [];
296
+ const cmdIds = [];
297
+ let symbol = ARGS.symbol ? String(ARGS.symbol).toUpperCase() : null;
298
+ for (let i = 0; i < n; i++) {
299
+ const tf = tfList[i];
300
+ const id = baseId + 'p' + (i + 1);
301
+ const params = { file: sanitizeFile(base + '_' + tf, 'beast_shot_' + id), width, height, timeframe: tf };
302
+ if (symbol) params.symbol = symbol;
303
+ if (ARGS.template !== undefined) params.template = String(ARGS.template);
304
+ cmdIds.push(id);
305
+ const r = captureOne({ id, file: params.file, params, filesDir, cmdFile, ackFile, beatFile, timeoutSec: perAck, diag });
306
+ if (!r.ok) {
307
+ return {
308
+ err: r.err,
309
+ error: '[' + tf + '] ' + r.error,
310
+ extra: Object.assign({ panel: tf, cmdId: id, requested: { symbol: params.symbol || null, timeframe: tf } }, r.extra || {})
311
+ };
312
+ }
313
+ const cp = copyOut(r.src, r.shotFile, ARGS.outdir);
314
+ panels.push({ tf, src: r.src, file: r.shotFile, size: r.size, res: r.res, pubPath: cp.pubPath, beastPath: cp.beastPath });
315
+ if (!symbol) symbol = (r.res && r.res.symbol) ? String(r.res.symbol) : null;
316
+ }
317
+
318
+ /* birleştir */
319
+ let composed = null;
320
+ try {
321
+ composed = await compose(
322
+ panels.map((p) => ({ path: p.src, label: (symbol ? symbol + ' · ' : '') + p.tf })),
323
+ { layout }
324
+ );
325
+ } catch (e) { composed = { ok: false, error: String((e && e.message) || e) }; }
326
+ if (!composed || !composed.ok) {
327
+ return { err: 'stitch_fail', error: 'görüntüler birleştirilemedi: ' + ((composed && composed.error) || 'bilinmeyen') };
328
+ }
329
+
330
+ const outFile = sanitizeFile(fileArg, 'beast_mtf_' + baseId + '.png');
331
+ const pathMt5 = path.join(filesDir, outFile);
332
+ try { fs.writeFileSync(pathMt5, composed.buffer); }
333
+ catch (e) { return { err: 'stitch_write_fail', error: 'birleşik PNG yazılamadı: ' + e.message }; }
334
+ const size = composed.buffer.length;
335
+ const cp = copyOut(pathMt5, outFile, ARGS.outdir);
336
+ const dataUrl = embedData(pathMt5, size);
337
+
338
+ return {
339
+ ok: true,
340
+ payload: {
341
+ ok: true,
342
+ path: cp.pubPath || cp.beastPath || pathMt5, // send_file'a verilecek yol (birleşik)
343
+ path_mt5: pathMt5,
344
+ path_beast: cp.beastPath,
345
+ file: outFile,
346
+ bytes: size,
347
+ image_embedded: !!dataUrl,
348
+ ...(dataUrl ? { __injectImage: dataUrl } : {}),
349
+ cmdIds,
350
+ filesDir,
351
+ symbol: symbol || null,
352
+ timeframes: tfList,
353
+ layout,
354
+ panels: panels.map((p, i) => ({
355
+ timeframe: p.tf,
356
+ position: layout === 'h'
357
+ ? (i === 0 ? 'left' : (i === n - 1 ? 'right' : 'middle'))
358
+ : (i === 0 ? 'top' : (i === n - 1 ? 'bottom' : 'middle')),
359
+ file: p.file,
360
+ path: p.pubPath || p.beastPath || p.src,
361
+ bytes: p.size,
362
+ period: (p.res && p.res.period) || null
363
+ })),
364
+ width: composed.width,
365
+ height: composed.height,
366
+ requested: { symbol: ARGS.symbol ? String(ARGS.symbol).toUpperCase() : null, timeframes: tfList, width, height, layout },
367
+ heartbeat_age_sec: heartbeatAge(beatFile),
368
+ ms: Date.now() - CTX.started,
369
+ pubErr: cp.pubErr || undefined
370
+ }
371
+ };
372
+ }
373
+
374
+ async function run() {
375
+ const timeoutSec = Number(ARGS.timeoutSec) > 0
376
+ ? Math.max(5, Math.min(70, Math.round(Number(ARGS.timeoutSec))))
377
+ : DEFAULT_TIMEOUT_SEC;
378
+ const diag = ARGS.diag !== false;
379
+
380
+ const term = findTerminalData();
381
+ if (!term) return fail('no_terminal', 'MT5 terminal veri klasörü bulunamadı (beast_ea.json yok) — MT5 kurulu/çalışıyor mu?');
382
+ const filesDir = term.files;
383
+ CTX.filesDir = filesDir;
384
+
385
+ const cmdFile = path.join(filesDir, 'beast_cmd.json');
386
+ const ackFile = path.join(filesDir, 'beast_cmd_ack.json');
387
+ const beatFile = path.join(filesDir, 'beast_ea.json');
388
+ const beatAge = heartbeatAge(beatFile);
389
+ if (beatAge !== null && beatAge > 60) {
390
+ return fail('ea_offline', 'BeastFinance EA canlı değil (son heartbeat ' + beatAge + ' sn önce) — grafikte EA yüklü ve terminal açık mı?',
391
+ { heartbeat_age_sec: beatAge });
392
+ }
393
+
394
+ const locked = acquireLock(Math.min(15000, timeoutSec * 500));
395
+ if (!locked) console.error('[mt5_shot] uyarı: kilit alınamadı, yine de devam ediliyor');
396
+
397
+ /* ÇOKLU PERİYOT: timeframes:["M1","M15"] → tek karede yan yana */
398
+ const tfList = normalizeTfs(ARGS);
399
+ if (tfList.length >= 2) {
400
+ const r = await runMulti(tfList, filesDir, cmdFile, ackFile, beatFile, timeoutSec, diag);
401
+ if (!r.ok) return fail(r.err, r.error, r.extra);
402
+ return out(r.payload);
403
+ }
404
+
405
+ const id = 'S' + Date.now().toString(36);
406
+ CTX.cmdId = id;
407
+
408
+ const file = sanitizeFile(ARGS.file, 'beast_shot_' + id + '.png');
409
+
410
+ const params = { file };
411
+ if (ARGS.timeframe) params.timeframe = String(ARGS.timeframe).toUpperCase();
412
+ if (ARGS.symbol) params.symbol = String(ARGS.symbol).toUpperCase();
413
+ if (ARGS.template !== undefined) params.template = String(ARGS.template);
414
+ params.width = Number(ARGS.width) > 0 ? Math.min(10000, Math.round(Number(ARGS.width))) : 1600;
415
+ params.height = Number(ARGS.height) > 0 ? Math.min(10000, Math.round(Number(ARGS.height))) : 900;
416
+
417
+ const cap = captureOne({ id, file, params, filesDir, cmdFile, ackFile, beatFile, timeoutSec, diag });
418
+ if (!cap.ok) return fail(cap.err, cap.error, cap.extra);
419
+
420
+ const cp = copyOut(cap.src, cap.shotFile, ARGS.outdir);
421
+ const res = cap.res;
422
+ const tfTxt = (res.period ? tfName(res.period) : null) || params.timeframe || null;
423
+ const wantSym = params.symbol || res.symbol || null;
424
+ const mismatch = !!(params.symbol && res.symbol && String(res.symbol).toUpperCase() !== String(params.symbol).toUpperCase());
425
+ const dataUrl = embedData(cap.src, cap.size);
286
426
 
287
427
  return out({
288
428
  ok: true,
289
- path: pubPath || beastPath || src, // send_file'a verilecek yol
290
- path_mt5: src,
291
- path_beast: beastPath,
292
- file: shotFile,
293
- bytes: size,
429
+ path: cp.pubPath || cp.beastPath || cap.src, // send_file'a verilecek yol
430
+ path_mt5: cap.src,
431
+ path_beast: cp.beastPath,
432
+ file: cap.shotFile,
433
+ bytes: cap.size,
294
434
  image_embedded: !!dataUrl,
295
435
  ...(dataUrl ? { __injectImage: dataUrl } : {}),
296
436
  cmdId: id,
@@ -306,7 +446,7 @@ function run() {
306
446
  symbol_mismatch: mismatch,
307
447
  heartbeat_age_sec: heartbeatAge(beatFile),
308
448
  ms: Date.now() - CTX.started,
309
- pubErr: pubErr || undefined
449
+ pubErr: cp.pubErr || undefined
310
450
  });
311
451
  }
312
452
 
@@ -402,8 +542,9 @@ let raw = '', resolved = false, silenceTimer = null;
402
542
  function begin(resolve) { if (resolved) return; resolved = true; resolve(); }
403
543
  function finish() {
404
544
  ARGS = loadArgs(raw);
405
- try { run(); }
406
- catch (e) { fail('timeout', 'beklenmeyen hata: ' + (e && e.message ? e.message : String(e))); }
545
+ Promise.resolve()
546
+ .then(run)
547
+ .catch((e) => fail('timeout', 'beklenmeyen hata: ' + (e && e.message ? e.message : String(e))));
407
548
  }
408
549
  try {
409
550
  process.stdin.setEncoding('utf8');
@@ -0,0 +1,125 @@
1
+ 'use strict';
2
+ /* mt5_shot yardımcı: birden çok PNG'yi TEK karede birleştirir (yan yana / alt alta).
3
+ - @napi-rs/canvas uygulamanın node_modules'ından yüklenir; kişisel tool çocuk
4
+ prosesi uygulama klasörü dışında koştuğu için yol BEAST_APP_DIR'den gelir
5
+ (customtools env olarak verir; app.asar → app.asar.unpacked otomatik denenir).
6
+ - Panel başına üst köşeye "SEMBOL · PERİYOT" etiketi çizilir: görsel ajan hangi
7
+ tarafın hangi zaman dilimi olduğunu görüntüden de okur.
8
+ - Toplam boyut sınırı aşılırsa paneller eşit oranda küçültülür. */
9
+
10
+ const path = require('path');
11
+
12
+ let cached = undefined;
13
+ function loadCanvas() {
14
+ if (cached !== undefined) return cached || null;
15
+ cached = null;
16
+ const cands = [];
17
+ const push = (base) => {
18
+ const b = String(base || '').trim();
19
+ if (!b) return;
20
+ if (!cands.includes(b)) cands.push(b);
21
+ const un = b.replace(/app\.asar([\\/])/i, 'app.asar.unpacked$1');
22
+ if (un !== b && !cands.includes(un)) cands.push(un);
23
+ };
24
+ push(process.env.BEAST_APP_DIR);
25
+ push(path.join(process.env.BEAST_APP_DIR || '', '..'));
26
+ for (const base of cands) {
27
+ try {
28
+ cached = require(path.join(base, 'node_modules', '@napi-rs', 'canvas'));
29
+ if (cached && typeof cached.createCanvas === 'function') break;
30
+ } catch (e) { cached = null; }
31
+ }
32
+ return cached || null;
33
+ }
34
+
35
+ const MAX_W = 4800; /* birleşik tuval genişlik tavanı */
36
+ const MAX_H = 3000; /* yükseklik tavanı */
37
+ const MAX_PX = 9 * 1e6; /* toplam piksel tavanı (güvenlik) */
38
+ const GAP = 8; /* paneller arası ayırıcı kalınlığı */
39
+ const LABEL_H = 30; /* etiket şeridi yüksekliği */
40
+
41
+ function pickFont(size) {
42
+ return 'bold ' + size + 'px "Segoe UI", Arial, sans-serif';
43
+ }
44
+
45
+ /* panels: [{ path, label }] · opts: { layout: 'h'|'v' }
46
+ dönüş: { ok:true, buffer, width, height } | { ok:false, error } */
47
+ async function compose(panels, opts) {
48
+ const cv = loadCanvas();
49
+ if (!cv) return { ok: false, error: 'görüntü kütüphanesi bulunamadı (@napi-rs/canvas) — tek periyot çekimi kullanın' };
50
+ const { createCanvas, loadImage } = cv;
51
+ const list = (panels || []).filter((p) => p && p.path);
52
+ if (list.length < 2) return { ok: false, error: 'birleştirme için en az 2 panel gerekli' };
53
+
54
+ const imgs = [];
55
+ for (const p of list) {
56
+ const im = await loadImage(p.path);
57
+ imgs.push({ img: im, label: String(p.label || ''), w: im.width, h: im.height });
58
+ }
59
+
60
+ const layout = String((opts && opts.layout) || 'h').toLowerCase() === 'v' ? 'v' : 'h';
61
+ let cellW = 0, cellH = 0;
62
+ if (layout === 'h') {
63
+ cellW = Math.max(...imgs.map((i) => i.w));
64
+ cellH = Math.max(...imgs.map((i) => i.h));
65
+ } else {
66
+ cellW = Math.max(...imgs.map((i) => i.w));
67
+ cellH = Math.max(...imgs.map((i) => i.h));
68
+ }
69
+ const n = imgs.length;
70
+ let W = layout === 'h' ? cellW * n + GAP * (n - 1) : cellW;
71
+ let H = layout === 'h' ? cellH : cellH * n + GAP * (n - 1);
72
+
73
+ /* boyut tavanları: eşit oranda küçült */
74
+ let scale = 1;
75
+ if (W > MAX_W) scale = Math.min(scale, MAX_W / W);
76
+ if (H > MAX_H) scale = Math.min(scale, MAX_H / H);
77
+ if (W * H * scale * scale > MAX_PX) scale = Math.min(scale, Math.sqrt(MAX_PX / (W * H)));
78
+ if (scale < 1) {
79
+ cellW = Math.max(320, Math.floor(cellW * scale));
80
+ cellH = Math.max(200, Math.floor(cellH * scale));
81
+ W = layout === 'h' ? cellW * n + GAP * (n - 1) : cellW;
82
+ H = layout === 'h' ? cellH : cellH * n + GAP * (n - 1);
83
+ }
84
+
85
+ const canvas = createCanvas(W, H);
86
+ const ctx = canvas.getContext('2d');
87
+ ctx.fillStyle = '#0b0b0b';
88
+ ctx.fillRect(0, 0, W, H);
89
+
90
+ imgs.forEach((p, i) => {
91
+ const x = layout === 'h' ? i * (cellW + GAP) : 0;
92
+ const y = layout === 'h' ? 0 : i * (cellH + GAP);
93
+ ctx.drawImage(p.img, x, y, cellW, cellH);
94
+ /* ayırıcı çizgi */
95
+ if (i > 0) {
96
+ ctx.fillStyle = '#3a3a3a';
97
+ if (layout === 'h') ctx.fillRect(x - GAP, 0, GAP, H);
98
+ else ctx.fillRect(0, y - GAP, W, GAP);
99
+ }
100
+ /* etiket: sol üstte koyu şerit + parlak metin */
101
+ if (p.label) {
102
+ try {
103
+ ctx.font = pickFont(20);
104
+ const tw = Math.min(cellW - 24, Math.ceil(ctx.measureText(p.label).width) + 20);
105
+ ctx.fillStyle = 'rgba(0,0,0,0.78)';
106
+ ctx.fillRect(x + 6, y + 6, Math.max(60, tw), LABEL_H);
107
+ ctx.fillStyle = '#00e676';
108
+ ctx.textBaseline = 'middle';
109
+ ctx.fillText(p.label, x + 15, y + 6 + LABEL_H / 2 + 1);
110
+ } catch (e) {}
111
+ }
112
+ });
113
+
114
+ let buffer = null;
115
+ try { buffer = canvas.toBuffer('image/png'); } catch (e) {
116
+ return { ok: false, error: 'PNG kodlanamadı: ' + (e && e.message ? e.message : String(e)) };
117
+ }
118
+ return { ok: true, buffer, width: W, height: H };
119
+ }
120
+
121
+ function available() {
122
+ return !!loadCanvas();
123
+ }
124
+
125
+ module.exports = { compose, available };
@@ -1,20 +1,24 @@
1
1
  {
2
2
  "name": "mt5_shot",
3
- "description": "MT5 grafiğinin GERÇEK ekran görüntüsünü alır ve GÖRSELİ ÇAĞIRAN AJANA GÖSTERİR: MT5\\MQL5\\Files\\beast_cmd.json üzerinden BeastFinance EA'ya 'shot' komutu yazar, EA ChartScreenShot ile PNG üretir; PNG hem tool klasörüne hem <kullanıcı>\\beast_shots altına kopyalanır (dönen path send_file'a verilebilir) hem de data URL olarak sonuca gömülür — engine bunu sonraki turda ajana görsel olarak enjekte eder (embed:false kapatır). Aktif grafik istenen sembol/periyot ile aynıysa ondan çekilir (Beast panosu + seviye çizgileri görünür), değilse EA geçici grafik açar (ChartOpen/ChartSetSymbolPeriod + BeastShot.tpl açık tema). Komut id/ts ile yazılır, ack poll edilir ve PNG MQL5\\Files'ta belirene kadar beklenir; hata halinde gövde şu kodu taşır: err = no_terminal|ea_offline|cmd_write_fail|ea_no_ack|ea_error|png_yok|timeout (+ cmdId, filesDir, diagnostic, ping). Grafik analizi/raporu ve ekip paylaşımı için birincil screenshot aracıdır.",
3
+ "defaultsVersion": 2,
4
+ "description": "MT5 grafiğinin GERÇEK ekran görüntüsünü alır ve GÖRSELİ ÇAĞIRAN AJANA GÖSTERİR: MT5\\MQL5\\Files\\beast_cmd.json üzerinden BeastFinance EA'ya 'shot' komutu yazar, EA ChartScreenShot ile PNG üretir; PNG hem tool klasörüne hem <kullanıcı>\\beast_shots altına kopyalanır (dönen path send_file'a verilebilir) hem de data URL olarak sonuca gömülür — engine bunu sonraki turda ajana görsel olarak enjekte eder (embed:false kapatır). ÇOKLU ZAMAN DİLİMİ: timeframes:[\"M1\",\"M15\"] verirsen aynı sembolün periyotları SIRAYLA çekilip TEK PNG'de birleştirilir (layout:\"h\" soldan sağa — varsayılan; \"v\" üstten alta; 2-3 periyot) ve her panelin üstüne \"SEMBOL · PERİYOT\" etiketi çizilir — solda/sağda hangi periyot olduğu görüntüden de okunur. Aktif grafik istenen sembol/periyot ile aynıysa ondan çekilir (Beast panosu + seviye çizgileri görünür), değilse EA geçici grafik açar (ChartOpen/ChartSetSymbolPeriod + BeastShot.tpl açık tema). Komut id/ts ile yazılır, ack poll edilir ve PNG MQL5\\Files'ta belirene kadar beklenir; hata halinde gövde şu kodu taşır: err = no_terminal|ea_offline|cmd_write_fail|ea_no_ack|ea_error|png_yok|timeout|stitch_fail (+ cmdId, filesDir, diagnostic, ping). Grafik analizi/raporu ve ekip paylaşımı için birincil screenshot aracıdır.",
4
5
  "parameters": {
5
6
  "type": "object",
6
7
  "properties": {
7
8
  "symbol": { "type": "string", "description": "Boş = aktif grafik sembolü (örn GOLD)" },
8
9
  "timeframe": { "type": "string", "description": "M1|M5|M15|M30|H1|H4|D1 (boş = aktif periyot)" },
9
- "width": { "type": "number", "description": "Görüntü genişliği (varsayılan 1600)" },
10
- "height": { "type": "number", "description": "Görüntü yüksekliği (varsayılan 900)" },
11
- "file": { "type": "string", "description": "Opsiyonel dosya adı (varsayılan beast_shot_<id>.png)" },
12
- "timeoutSec": { "type": "number", "description": "Ack/PNG bekleme süresi sn (varsayılan 20, 5-70)" },
10
+ "timeframes": { "type": "array", "items": { "type": "string" }, "description": "ÇOKLU PERİYOT TEK GÖRÜNTÜ: örn [\"M1\",\"M15\"] — sırayla çekilip tek PNG'de birleştirilir (2-3 adet; soldan sağa)" },
11
+ "tf2": { "type": "string", "description": "İkinci periyot kısayolu (timeframe ile birlikte): örn timeframe M1 + tf2 M15" },
12
+ "layout": { "type": "string", "description": "Çoklu periyot yerleşimi: h = yan yana (varsayılan), v = alt alta" },
13
+ "width": { "type": "number", "description": "Panel başına görüntü genişliği (varsayılan 1600)" },
14
+ "height": { "type": "number", "description": "Panel başına görüntü yüksekliği (varsayılan 900)" },
15
+ "file": { "type": "string", "description": "Opsiyonel dosya adı (varsayılan beast_shot_<id>.png; çoklu periyotta birleşik dosyanın adı)" },
16
+ "timeoutSec": { "type": "number", "description": "Ack/PNG bekleme süresi sn (varsayılan 20, 5-70; çoklu periyotta panel başına 15 sn tavan)" },
13
17
  "outdir": { "type": "string", "description": "PNG'nin kopyalanacağı klasör (varsayılan <kullanıcı>\\beast_shots — send_file buradan gönderebilir)" },
14
18
  "diag": { "type": "boolean", "description": "Ack gelmezse teşhis amaçlı ping at (varsayılan true)" },
15
19
  "embed": { "type": "boolean", "description": "PNG'yi ajana görsel olarak enjekte et (varsayılan true; false = yalnız dosya yolu döner)" }
16
20
  },
17
21
  "required": []
18
22
  },
19
- "updatedAt": "2026-09-11T23:15:00.000Z"
23
+ "updatedAt": "2026-09-15T00:00:00.000Z"
20
24
  }
@@ -2010,7 +2010,7 @@ class Engine {
2010
2010
  (roleBlockFull ? roleBlockFull + '\n' : '') +
2011
2011
  (teamLine || '') +
2012
2012
  'MT5 ARAÇLARI: mt5_status (bağlantı), mt5_account (hesap), mt5_market (canlı fiyat), mt5_positions (açık pozisyonlar), mt5_orders (bekleyen emirler), mt5_history (kapanan işlemler), mt5_ea (BeastFinance grafik panosu: status/ping/chart/note), mt5_trade (piyasa emri), mt5_close (kapat), mt5_modify (SL/TP), mt5_pending (bekleyen emir), mt5_cancel (emir iptal).\n' +
2013
- 'KİŞİSEL MT5 ARAÇLARI (VARSAYILAN KURULU — tool__* tüm oturumlarda çağrılabilir): tool__mt5_shot (grafikten GERÇEK PNG + görsel ajana enjekte — grafik işlerinde BİRİNCİL), tool__mt5_barlar2 (OHLC mumlar, köprüsüz), tool__mt5_m15_m5 (GOLD M15+M5 hazır paket), tool__mt5_fiyat, tool__mt5_durum (hesap+pozisyon+emir tek çağrı), tool__mt5_gecmis, tool__mt5_pozisyon_gecmis, tool__mt5_sltp, tool__mt5_kapat, tool__mt5_bekleyen, tool__mt5_emir, tool__mt5_emir_iptal.\n' +
2013
+ 'KİŞİSEL MT5 ARAÇLARI (VARSAYILAN KURULU — tool__* tüm oturumlarda çağrılabilir): tool__mt5_shot (grafikten GERÇEK PNG + görsel ajana enjekte — grafik işlerinde BİRİNCİL; timeframes:["M1","M15"] ile çoklu periyot TEK karede soldan sağa), tool__mt5_barlar2 (OHLC mumlar, köprüsüz), tool__mt5_m15_m5 (GOLD M15+M5 hazır paket), tool__mt5_fiyat, tool__mt5_durum (hesap+pozisyon+emir tek çağrı), tool__mt5_gecmis, tool__mt5_pozisyon_gecmis, tool__mt5_sltp, tool__mt5_kapat, tool__mt5_bekleyen, tool__mt5_emir, tool__mt5_emir_iptal.\n' +
2014
2014
  'BEASTFINANCE EA (OTOMATİK GRAFİK UZMANI — ENTEGRASYON KANALI): MT5 terminaline bağlanıldığında BeastFinance uzman danışmanı İLK GRAFİĞE OTOMATİK yüklenir, AutoTrading izni açılır (kurulum sistem tarafından yapılır; elle ekleme gerekmez). Grafik panosu + seviye çizgileri + dosya köprüsü (beast_ea.json / beast_cmd.json / beast_note.json) bu EA üzerinden yürür: entegrasyon işlerinde (status/ping/chart/note) mt5_ea kullan; ekran görüntüsü işlerinde tool__mt5_shot ("shot" komutu — gerçek PNG + ajan görseli) birincildir. Yazdığın not ve çizgiler tool__mt5_shot ve computer_look screenshot\'ında GÖRÜNÜR.\n' +
2015
2015
  'YETKİLERİN (AÇIK — çekinmeden kullan):\n' +
2016
2016
  '- skill: kurulu SKILL.md kataloğunu oku ve uygula — tool yazmadan ÖNCE skill("tool-yazma"), MT5 tarafı işlerden ÖNCE skill("mql5") oku ve prosedürüne birebir uy.\n' +
@@ -2018,7 +2018,7 @@ class Engine {
2018
2018
  '- MQL5: MT5 tarafında script/gösterge/EA yaz (write_file), metaeditor64.exe /compile ile derle, MQL5\\Files dosya köprüsüyle veriyi Beast\'e taşı; kullanıcıya çalıştırma adımını açıkça söyle.\n' +
2019
2019
  '- Yerleşik araçlar: run_command, python_run, read_file/write_file/edit_file, web_search/deep_search, browser_*, computer_look (ekran görüntüsü) — hepsi açık.\n' +
2020
2020
  'GRAFİK & GÖRSEL DOĞRULAMA (karar öncesi — vazgeçilmez):\n' +
2021
- '- tool__mt5_shot: MT5 grafiğinin GERÇEK PNG\'si (BeastFinance EA "shot" komutu). Görsel SONRAKİ TURDA gözüne gelir; aktif grafikte Beast panosu + mt5_ea note ile çizilen seviyeler GÖRÜNÜR. symbol/timeframe ver → EA geçici grafik açıp o sembol/periyottan çeker; dönen path send_file ile kullanıcıya gönderilebilir. Grafik analizinde İLK tercih budur.\n' +
2021
+ '- tool__mt5_shot: MT5 grafiğinin GERÇEK PNG\'si (BeastFinance EA "shot" komutu). Görsel SONRAKİ TURDA gözüne gelir; aktif grafikte Beast panosu + mt5_ea note ile çizilen seviyeler GÖRÜNÜR. symbol/timeframe ver → EA geçici grafik açıp o sembol/periyottan çeker; dönen path send_file ile kullanıcıya gönderilebilir. Grafik analizinde İLK tercih budur. ÇOKLU ZAMAN DİLİMİ: timeframes:["M1","M15"] (2-3 periyot, layout:"v" alt alta) → periyotlar sırayla çekilip TEK PNG\'de birleştirilir ve her panelin üstüne "SEMBOL · PERİYOT" etiketi çizilir; solda/sağda hangisi olduğunu görüntüden OKURSUN — trend uyumu/MTF teyidi için bunu kullan (gerçek kapanışlarla).\n' +
2022
2022
  '- mt5_ea action:"note": grafiğe kısa plan metni + yatay seviye çizgileri yazar (destek/direnç/SL/TP) — sonra tool__mt5_shot ile çekip seviyelerin doğru yerde olduğunu GÖRSEL doğrula.\n' +
2023
2023
  '- computer_look: tüm masaüstü ekranı (MT5 dışı pencereler dahil); browser_screenshot: web grafikleri (TradingView/Investing) — browser_open ile aç.\n' +
2024
2024
  '- Görsel göremiyorsan (metin-model) ocr_read (source:"screen"/"browser") ile grafikteki fiyat/seviyeleri metne çevir.\n' +
@@ -5379,7 +5379,7 @@ const skills = require('./skills');
5379
5379
  }
5380
5380
  /* KİŞİSEL TOOL: %APPDATA%\beast\tools\ — çocuk node prosesinde izole koşar */
5381
5381
  if (String(name).startsWith('tool__')) {
5382
- return JSON.stringify(await customtools.call(name, args));
5382
+ return JSON.stringify(await customtools.call(name, args, sessionId));
5383
5383
  }
5384
5384
  if (isBc) {
5385
5385
  /* opencode permission akışı (tool/external-directory.ts + her aracın
package/src/main.js CHANGED
@@ -9317,9 +9317,9 @@ function finCfg() {
9317
9317
  if (typeof f.notifyTrades !== 'boolean') f.notifyTrades = true;
9318
9318
  if (typeof f.notifyWatchdog !== 'boolean') f.notifyWatchdog = true;
9319
9319
  /* KANAL OLAY FİLTRESİ: varsayılan true — WhatsApp/Telegram/Discord'a yalnız
9320
- emir/işlem olayları (açılış, kapanış, bekleyen emir, iptal) düşer; fiyat
9321
- alarmı, koruma (BE/trailing/kısmi TP), günlük limit ve rapor bildirimleri
9322
- panelde kalır. false = tüm olaylar kanallara gider (eski davranış). */
9320
+ emir/işlem olayları (açılış, kapanış, SL/TP güncelleme, bekleyen emir,
9321
+ iptal) düşer; fiyat alarmı, koruma (BE/trailing/kısmi TP), günlük limit ve
9322
+ rapor bildirimleri panelde kalır. false = tüm olaylar kanallara gider. */
9323
9323
  if (typeof f.notifyTradeOnly !== 'boolean') f.notifyTradeOnly = true;
9324
9324
  if (!Number.isFinite(Number(f.maxDailyLossPct))) f.maxDailyLossPct = 3;
9325
9325
  /* GÜNLÜK LİMİT AKSİYONU: warn = yalnız uyarı; stop = tüm finance ajanlarını
@@ -9566,7 +9566,7 @@ function financeNotify(text, kind, panel) {
9566
9566
  gider (emir açılış/kapanış, bekleyen emir, iptal). Alarm/koruma/risk/rapor
9567
9567
  bildirimleri yalnız panelde görünür. */
9568
9568
  const kindStr = String(kind || '');
9569
- if (cfg.notifyTradeOnly !== false && !['trade', 'close', 'pending', 'cancel'].includes(kindStr)) return;
9569
+ if (cfg.notifyTradeOnly !== false && !['trade', 'close', 'modify', 'pending', 'cancel'].includes(kindStr)) return;
9570
9570
  const body = '💼 *Beast Finance*\n' + line;
9571
9571
  const senders = [];
9572
9572
  try {
@@ -10246,54 +10246,79 @@ try { finExcLoad(); } catch {}
10246
10246
  try { finStatsLoad(); } catch {}
10247
10247
  try { finEquityLoad(); } catch {}
10248
10248
 
10249
+ /* İŞLEM OLAYI (ortak hat): iç mt5_* handler'ları ve kişisel tool__mt5_*
10250
+ araçları buradan geçer — jurnal → panel toast → seçili kanallara bildirim. */
10251
+ function finTradeEvent(kRaw, dataRaw, sidRaw) {
10252
+ const k = String(kRaw || '');
10253
+ const d = dataRaw && typeof dataRaw === 'object' ? dataRaw : {};
10254
+ const sid = String(sidRaw || '');
10255
+ const ai = finAgentInfo(sid);
10256
+ const who = ai.label || '';
10257
+ if (k === 'note') {
10258
+ finJournal({ kind: 'note', sid, agent: who, symbol: d.symbol, note: d.note, ticket: d.ticket || 0, side: d.side || '' });
10259
+ return;
10260
+ }
10261
+ /* SHADOW: gerçek emir yok — karar teziyle günlüğe yazılır (test/ölçüm) */
10262
+ if (k === 'shadow') {
10263
+ finJournal({ kind: 'shadow', sid, agent: who, symbol: d.symbol, side: d.side || '', volume: d.volume, sl: d.sl || 0, tp: d.tp || 0, price: d.price || 0, type: d.type || '', reason: d.reason || '' });
10264
+ const sline = `🧪 SHADOW: ${d.side || d.type || ''} ${d.volume || ''} ${d.symbol || ''} — emir gönderilmedi (karar günlüğe yazıldı)`;
10265
+ financeLog('[shadow] ' + sline);
10266
+ finPush('trade', { line: sline });
10267
+ return;
10268
+ }
10269
+ if (k === 'trade') {
10270
+ finJournal({ kind: 'trade', sid, agent: who, symbol: d.symbol, side: d.side, volume: d.volume, sl: d.sl, tp: d.tp, reason: d.reason || d.comment || '' });
10271
+ } else if (k === 'close') {
10272
+ finJournal({ kind: 'close-req', sid, agent: who, ticket: d.ticket, volume: d.volume });
10273
+ return; /* kesin kapanış K/Z'si watchdog tarafından yazılır (çift bildirim yok) */
10274
+ } else if (k === 'modify') {
10275
+ finJournal({ kind: 'modify', sid, agent: who, ticket: d.ticket, sl: d.sl, tp: d.tp });
10276
+ } else if (k === 'pending') {
10277
+ finJournal({ kind: 'pending', sid, agent: who, symbol: d.symbol, type: d.type, volume: d.volume, reason: d.reason || '' });
10278
+ } else if (k === 'cancel') {
10279
+ finJournal({ kind: 'cancel', sid, agent: who, ticket: d.ticket });
10280
+ }
10281
+ let line = '';
10282
+ if (k === 'trade') line = `İŞLEM AÇILDI: ${d.side} ${d.volume} ${d.symbol}${who ? ' · ' + who : ''}`;
10283
+ else if (k === 'modify') line = `SL/TP GÜNCELLENDİ: ticket ${d.ticket} (SL ${d.sl || 0} / TP ${d.tp || 0})${who ? ' · ' + who : ''}`;
10284
+ else if (k === 'pending') line = `BEKLEYEN EMİR: ${d.type} ${d.volume} ${d.symbol}${who ? ' · ' + who : ''}`;
10285
+ else if (k === 'cancel') line = `EMİR İPTAL: ticket ${d.ticket}`;
10286
+ if (line) {
10287
+ financeLog('[ajan] ' + line);
10288
+ finPush('trade', { line });
10289
+ if (finCfg().notifyTrades !== false && (k === 'trade' || k === 'modify' || k === 'pending' || k === 'cancel')) {
10290
+ financeNotify(line, k, false); /* panel toast'ı yukarıda — kanallara gönder */
10291
+ }
10292
+ if (k === 'trade') finStatsRefresh(true).catch(() => {});
10293
+ }
10294
+ }
10295
+
10249
10296
  /* financetools ayar + bildirim kancası: her olay kalıcı günlüğe + bildirime */
10250
10297
  try {
10251
10298
  financetools.setConfig(() => finCfg());
10252
10299
  financetools.setAlerts(finAlertApi());
10253
10300
  financetools.setDiscipline((side, symbol, positions) => finDisciplineError(side, symbol, positions));
10254
10301
  financetools.setNotify((entry) => {
10255
- const k = String((entry && entry.kind) || '');
10256
- const d = entry && entry.data ? entry.data : {};
10257
- const ai = finAgentInfo(entry && entry.sid);
10258
- const who = ai.label || '';
10259
- const sid = String((entry && entry.sid) || '');
10260
- if (k === 'note') {
10261
- finJournal({ kind: 'note', sid, agent: who, symbol: d.symbol, note: d.note, ticket: d.ticket || 0, side: d.side || '' });
10262
- return;
10263
- }
10264
- /* SHADOW: gerçek emir yok — karar teziyle günlüğe yazılır (test/ölçüm) */
10265
- if (k === 'shadow') {
10266
- finJournal({ kind: 'shadow', sid, agent: who, symbol: d.symbol, side: d.side || '', volume: d.volume, sl: d.sl || 0, tp: d.tp || 0, price: d.price || 0, type: d.type || '', reason: d.reason || '' });
10267
- const sline = `🧪 SHADOW: ${d.side || d.type || ''} ${d.volume || ''} ${d.symbol || ''} — emir gönderilmedi (karar günlüğe yazıldı)`;
10268
- financeLog('[shadow] ' + sline);
10269
- finPush('trade', { line: sline });
10270
- return;
10271
- }
10272
- if (k === 'trade') {
10273
- finJournal({ kind: 'trade', sid, agent: who, symbol: d.symbol, side: d.side, volume: d.volume, sl: d.sl, tp: d.tp, reason: d.reason || d.comment || '' });
10274
- } else if (k === 'close') {
10275
- finJournal({ kind: 'close-req', sid, agent: who, ticket: d.ticket, volume: d.volume });
10276
- return; /* kesin kapanış K/Z'si watchdog tarafından yazılır (çift bildirim yok) */
10277
- } else if (k === 'modify') {
10278
- finJournal({ kind: 'modify', sid, agent: who, ticket: d.ticket, sl: d.sl, tp: d.tp });
10279
- } else if (k === 'pending') {
10280
- finJournal({ kind: 'pending', sid, agent: who, symbol: d.symbol, type: d.type, volume: d.volume, reason: d.reason || '' });
10281
- } else if (k === 'cancel') {
10282
- finJournal({ kind: 'cancel', sid, agent: who, ticket: d.ticket });
10283
- }
10284
- let line = '';
10285
- if (k === 'trade') line = `İŞLEM AÇILDI: ${d.side} ${d.volume} ${d.symbol}${who ? ' · ' + who : ''}`;
10286
- else if (k === 'modify') line = `SL/TP GÜNCELLENDİ: ticket ${d.ticket} (SL ${d.sl || 0} / TP ${d.tp || 0})${who ? ' · ' + who : ''}`;
10287
- else if (k === 'pending') line = `BEKLEYEN EMİR: ${d.type} ${d.volume} ${d.symbol}${who ? ' · ' + who : ''}`;
10288
- else if (k === 'cancel') line = `EMİR İPTAL: ticket ${d.ticket}`;
10289
- if (line) {
10290
- financeLog('[ajan] ' + line);
10291
- finPush('trade', { line });
10292
- if (finCfg().notifyTrades !== false && (k === 'trade' || k === 'pending' || k === 'cancel')) {
10293
- financeNotify(line, k, false); /* panel toast'ı yukarıda — kanallara gönder */
10294
- }
10295
- if (k === 'trade') finStatsRefresh(true).catch(() => {});
10296
- }
10302
+ finTradeEvent(entry && entry.kind, entry && entry.data, entry && entry.sid);
10303
+ });
10304
+ /* kişisel MT5 toolları köprüyü doğrudan çağırır (iç mt5_* handler'ı devreye
10305
+ girmez) — başarılı emir/SL-TP/bekleyen/iptal olayları aynı hatta düşer */
10306
+ customtools.setTradeHook((id, args, res, sid) => {
10307
+ if (!res || res.ok !== true) return;
10308
+ const kind = {
10309
+ mt5_emir: 'trade',
10310
+ mt5_sltp: 'modify',
10311
+ mt5_bekleyen: 'pending',
10312
+ mt5_emir_iptal: 'cancel',
10313
+ mt5_kapat: 'close',
10314
+ }[String(id || '')];
10315
+ if (!kind) return;
10316
+ const a = args || {};
10317
+ if (kind === 'trade') finTradeEvent('trade', { symbol: a.symbol, side: a.side, volume: a.volume, sl: a.sl, tp: a.tp, reason: a.reason || a.comment || '', result: res }, sid);
10318
+ else if (kind === 'modify') finTradeEvent('modify', { ticket: a.ticket, sl: a.sl, tp: a.tp }, sid);
10319
+ else if (kind === 'pending') finTradeEvent('pending', { symbol: a.symbol, type: a.type, volume: a.volume, reason: a.reason || '' }, sid);
10320
+ else if (kind === 'cancel') finTradeEvent('cancel', { ticket: a.ticket }, sid);
10321
+ else if (kind === 'close') finTradeEvent('close', { ticket: a.ticket, volume: a.volume || 'all' }, sid);
10297
10322
  });
10298
10323
  } catch {}
10299
10324
 
@@ -258,7 +258,7 @@
258
258
  </select>
259
259
  </div>
260
260
  <div class="fin-field fin-check">
261
- <label title="İşaretliyken kanala YALNIZ emir olayları düşer: açılış, kapanış, bekleyen emir, iptal. Fiyat alarmı, BE/trailing/kısmi TP, günlük limit ve rapor bildirimleri gelmez — onlar panelde kalır">
261
+ <label title="İşaretliyken kanala YALNIZ emir olayları düşer: açılış, kapanış, SL/TP güncelleme, bekleyen emir, iptal. Fiyat alarmı, BE/trailing/kısmi TP, günlük limit ve rapor bildirimleri gelmez — onlar panelde kalır">
262
262
  <input id="finNotifyTradeOnly" type="checkbox" checked /> Kanala sadece emir/işlem olayları gelsin (alarm gelmez)
263
263
  </label>
264
264
  </div>
@@ -5549,6 +5549,30 @@ function toggleDmRail(hide) {
5549
5549
 
5550
5550
  /* --- modal: SOHBET (yalnız burada tam döküm) --- */
5551
5551
 
5552
+ /* Yeni mesaj / geç decode olan görsellerde sohbeti otomatik en alta indir:
5553
+ anında + rAF + kısa gecikmeler + görsel load olayları (görsel sonradan
5554
+ yüklenince içerik uzar; tek seferlik scroll yetmez). */
5555
+ function dmStickBottom(box) {
5556
+ if (!box) return;
5557
+ const toBottom = () => {
5558
+ try {
5559
+ box.scrollTop = box.scrollHeight;
5560
+ const p = box.parentElement;
5561
+ if (p && p.scrollHeight > p.clientHeight) p.scrollTop = p.scrollHeight;
5562
+ } catch {}
5563
+ };
5564
+ toBottom();
5565
+ try { requestAnimationFrame(toBottom); } catch {}
5566
+ setTimeout(toBottom, 60);
5567
+ setTimeout(toBottom, 300);
5568
+ for (const img of box.querySelectorAll('img.dmt-img')) {
5569
+ if (img.dataset.dmStick) continue;
5570
+ img.dataset.dmStick = '1';
5571
+ img.addEventListener('load', toBottom, { once: true });
5572
+ img.addEventListener('error', toBottom, { once: true });
5573
+ }
5574
+ }
5575
+
5552
5576
  function renderDmModal() {
5553
5577
  const list = els.dmDialogList;
5554
5578
  if (!list) return;
@@ -5570,8 +5594,7 @@ function renderDmModal() {
5570
5594
  if (els.dmBackBtn) els.dmBackBtn.hidden = false;
5571
5595
  if (els.dmModalDel) els.dmModalDel.hidden = false;
5572
5596
  list.innerHTML = '<div class="dm-thread-msgs">' + msgs.map(dmMsgHtml).join('') + '</div>';
5573
- const box = list.querySelector('.dm-thread-msgs');
5574
- if (box) box.scrollTop = box.scrollHeight;
5597
+ dmStickBottom(list.querySelector('.dm-thread-msgs'));
5575
5598
  return;
5576
5599
  }
5577
5600
  /* OTURUM LİSTESİ — AKTİF + GEÇMİŞ bölümleri */