beast-agent 2.13.0 → 2.14.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.13.0",
4
+ "version": "2.14.0",
5
5
  "description": "Ultra-fast local agent shell for Windows.",
6
6
  "author": "algokodcom (AlgoKod)",
7
7
  "license": "MIT",
@@ -0,0 +1,265 @@
1
+ 'use strict';
2
+
3
+ /* ---------- ANDROID EMÜLATÖR (İSTEĞE BAĞLI — Beast Code mobil önizleme) ----------
4
+ Ayarlardan (Kurulum) görünür; KUR denince arka planda HAFİF set kurar:
5
+ JDK 17 (winget, java yoksa) + Android cmdline-tools (~150MB) +
6
+ platform-tools/emulator + 1 sistem imajı + 1 AVD ("Beast").
7
+ SDK'yı standarda yerleştirir: %LOCALAPPDATA%\Android\Sdk (Android Studio ile
8
+ paylaşımlı konum — kullanıcıda zaten varsa KURULUM YAPMAZ, onu kullanır).
9
+ Beast Code preview akışı: AVD varsa ajan `npx expo start --android` ile
10
+ uygulamayı emülatörde açar (expo AVD'yi adb üzerinden kendisi bulur). */
11
+
12
+ const { spawn, execSync } = require('child_process');
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+ const os = require('os');
16
+ const { emitInstallProgress } = require('./progressbus');
17
+
18
+ const SDK_DIR = path.join(process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'), 'Android', 'Sdk');
19
+ const CMDTOOLS_URL = 'https://dl.google.com/android/repository/commandlinetools-win-11076708_latest.zip';
20
+ const SYS_IMAGE = 'system-images;android-34;google_apis;x86_64';
21
+ const AVD_NAME = 'Beast';
22
+
23
+ const state = {
24
+ installing: false,
25
+ installFailed: false,
26
+ installError: '',
27
+ stage: '',
28
+ booting: false,
29
+ };
30
+
31
+ function sdkCandidates() {
32
+ const list = [];
33
+ for (const v of [process.env.ANDROID_HOME, process.env.ANDROID_SDK_ROOT]) {
34
+ if (v && fs.existsSync(v)) list.push(path.resolve(String(v)));
35
+ }
36
+ list.push(SDK_DIR);
37
+ return list.filter((p) => fs.existsSync(p));
38
+ }
39
+
40
+ function findSdk(rel, name) {
41
+ for (const sdk of sdkCandidates()) {
42
+ const p = path.join(sdk, ...rel.split('/'), name);
43
+ if (fs.existsSync(p)) return p;
44
+ }
45
+ /* PATH'te ara (kullanıcı elle kurduysa) */
46
+ try {
47
+ const out = String(execSync(`where ${name}`, { timeout: 5000, stdio: 'pipe', encoding: 'utf8', windowsHide: true }));
48
+ const first = out.split(/\r?\n/).find(Boolean);
49
+ if (first) return String(first).trim();
50
+ } catch {}
51
+ return '';
52
+ }
53
+
54
+ function findAdb() { return findSdk('platform-tools', 'adb.exe'); }
55
+
56
+ function findEmulator() { return findSdk('emulator', 'emulator.exe'); }
57
+
58
+ function javaOk() {
59
+ try {
60
+ const out = String(execSync('java -version 2>&1', { timeout: 6000, stdio: 'pipe', encoding: 'utf8', windowsHide: true }));
61
+ const m = /version "?(\d+)/.exec(out);
62
+ return m ? Number(m[1]) >= 17 : false;
63
+ } catch {
64
+ return false;
65
+ }
66
+ }
67
+
68
+ async function listAvds() {
69
+ const emu = findEmulator();
70
+ if (!emu) return [];
71
+ try {
72
+ const out = String(execSync(`"${emu}" -list-avds`, { timeout: 10000, stdio: 'pipe', encoding: 'utf8', windowsHide: true }));
73
+ return out.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
74
+ } catch {
75
+ return [];
76
+ }
77
+ }
78
+
79
+ function probe() {
80
+ const adb = findAdb();
81
+ const emu = findEmulator();
82
+ return {
83
+ sdk: !!(adb && emu),
84
+ sdkPath: sdkCandidates()[0] || '',
85
+ adb,
86
+ emulator: emu,
87
+ installing: state.installing,
88
+ failed: state.installFailed,
89
+ error: state.installError,
90
+ booting: state.booting,
91
+ };
92
+ }
93
+
94
+ /* ---------- ARKA PLAN KURULUM ----------
95
+ Aşamalar (progress bus 'android'): 1 java → 2 cmdline-tools → 3 sdk paketleri
96
+ (en ağır: ~1.4GB) → 4 AVD. Yarıda kalırsa idempotent: yeniden KUR kaldığından değil
97
+ baştan ama varolan parçaları atlar (licenses/paket kontrolü). */
98
+ function autoInstall() {
99
+ if (state.installing) return { ok: true, installing: true };
100
+ if (probe().adb && probe().emu) return { ok: true, already: true };
101
+ state.installing = true;
102
+ state.installFailed = false;
103
+ state.installError = '';
104
+ runInstall().catch((e) => {
105
+ state.installing = false;
106
+ state.installFailed = true;
107
+ state.installError = String((e && e.message) || e).slice(0, 160);
108
+ });
109
+ return { ok: true, installing: true };
110
+ }
111
+
112
+ /* .bat çağırıcı: Node güvenlik yaması (CVE-2024-27980) .bat/.cmd'yi shell'siz
113
+ spawn etmeyi EINVAL'le reddeder — cmd.exe /c üzerinden güvenli çağrı */
114
+ function spawnBat(bat, args, opts = {}) {
115
+ const cmdline = ['"' + bat + '"']
116
+ .concat(args.map((a) => (/[\s;&|^<>(),!%]/.test(a) ? '"' + a + '"' : a)))
117
+ .join(' ');
118
+ return spawn('cmd.exe', ['/d', '/s', '/c', cmdline], { windowsHide: true, ...opts.spawnOpts });
119
+ }
120
+
121
+ /* .bat çağırıcı (GÜVENLİ PATİKA): Node .bat'i shell'siz spawn etmeyi EINVAL'le
122
+ reddeder; cmd.exe'ye args dizisiyle geçmek de tırnak/redirect kaçışında kırılır.
123
+ ÇÖZÜM: geçici wrapper .bat YAZ (redirect + tırnaklar cmd'nin içine kalır),
124
+ wrapper'ı tek tırnaklı /c ile çalıştır. */
125
+ let _batSeq = 0;
126
+ function runBat(bat, args, opts = {}) {
127
+ return new Promise((resolve, reject) => {
128
+ const inner = ['"' + bat + '"']
129
+ .concat(args.map((a) => (/[\s;&|^<>(),!%]/.test(a) ? '"' + a + '"' : a)))
130
+ .join(' ') + (opts.redirect ? ' < "' + opts.redirect + '"' : '');
131
+ const wrapper = path.join(os.tmpdir(), 'beast-sdk-' + Date.now() + '-' + (++_batSeq) + '.bat');
132
+ fs.writeFileSync(wrapper, '@echo off\r\n' + inner + '\r\nexit /b %ERRORLEVEL%\r\n', 'utf8');
133
+ /* wrapper'a tüm tırnak/redirect işi bırakılır — .bat spawn için Node shell:true ister */
134
+ const proc = spawn(wrapper, [], { windowsHide: true, shell: true, ...opts.spawnOpts });
135
+ let out = '';
136
+ proc.stdout.on('data', (c) => { out += String(c); opts.onData && opts.onData(String(c)); });
137
+ proc.stderr.on('data', (c) => { out += String(c); });
138
+ proc.on('close', (code) => {
139
+ try { fs.unlinkSync(wrapper); } catch {}
140
+ (code === 0 ? resolve : reject)(code === 0 ? out : new Error(opts.tag + ' exit ' + code + (out ? ' :: ' + out.slice(-300) : '')));
141
+ });
142
+ proc.on('error', (e) => {
143
+ try { fs.unlinkSync(wrapper); } catch {}
144
+ reject(e);
145
+ });
146
+ });
147
+ }
148
+
149
+ async function runInstall() {
150
+ const stage = (pct) => { try { emitInstallProgress('android', { pct }); } catch {} };
151
+
152
+ /* 1) JDK 17 (sdkmanager şartı) — java yoksa winget ile */
153
+ stage(3);
154
+ if (!javaOk()) {
155
+ state.stage = 'JDK 17 kuruluyor (winget)';
156
+ await new Promise((resolve, reject) => {
157
+ const proc = spawn('winget', ['install', '--id', 'Microsoft.OpenJDK.17', '--accept-source-agreements', '--accept-package-agreements', '--silent'], { windowsHide: true, shell: true });
158
+ proc.on('close', (code) => (code === 0 || javaOk() ? resolve() : reject(new Error('JDK kurulumu başarısız (winget)'))));
159
+ proc.on('error', () => reject(new Error('winget bulunamadı — JDK 17 elle kur')));
160
+ });
161
+ }
162
+ stage(15);
163
+
164
+ /* 2) cmdline-tools indir + aç */
165
+ const toolsBin = path.join(SDK_DIR, 'cmdline-tools', 'latest', 'bin', 'sdkmanager.bat');
166
+ if (!fs.existsSync(toolsBin)) {
167
+ state.stage = 'cmdline-tools indiriliyor (~150MB)';
168
+ const zipPath = path.join(os.tmpdir(), 'commandlinetools.zip');
169
+ const res = await fetch(CMDTOOLS_URL);
170
+ if (!res.ok) throw new Error('cmdline-tools indirilemedi: HTTP ' + res.status);
171
+ const buf = Buffer.from(await res.arrayBuffer());
172
+ fs.mkdirSync(SDK_DIR, { recursive: true });
173
+ fs.writeFileSync(zipPath, buf);
174
+ stage(35);
175
+ execSync(
176
+ `powershell -NoProfile -Command "Expand-Archive -LiteralPath '${zipPath}' -DestinationPath '${SDK_DIR}' -Force"`,
177
+ { timeout: 300000, windowsHide: true, stdio: 'pipe' }
178
+ );
179
+ /* zip içi: cmdline-tools/ → cmdline-tools/latest/ olmalı */
180
+ const inner = path.join(SDK_DIR, 'cmdline-tools');
181
+ if (fs.existsSync(inner) && !fs.existsSync(toolsBin)) {
182
+ fs.renameSync(inner, inner + '_tmp');
183
+ fs.mkdirSync(path.join(SDK_DIR, 'cmdline-tools'), { recursive: true });
184
+ fs.renameSync(inner + '_tmp', path.join(SDK_DIR, 'cmdline-tools', 'latest'));
185
+ }
186
+ try { fs.unlinkSync(zipPath); } catch {}
187
+ }
188
+ stage(45);
189
+
190
+ /* 3) lisanslar + paketler (platform-tools + emulator + sistem imajı ~1.3GB)
191
+ Cevaplar stdin spam'iyle DEĞİL dosya redirect'iyle verilir — sdkmanager
192
+ interaktif stdin'i cmd borularında düzgün okumuyor */
193
+ state.stage = 'SDK paketleri iniyor (platform-tools · emulator · sistem imajı)';
194
+ const sysEnv = { ...process.env };
195
+ const yesFile = path.join(os.tmpdir(), 'beast-sdk-yes.txt');
196
+ fs.writeFileSync(yesFile, 'y\n'.repeat(80), 'utf8');
197
+ await runBat(toolsBin, ['--licenses'], {
198
+ tag: 'lisans adımı',
199
+ spawnOpts: { env: sysEnv },
200
+ redirect: yesFile,
201
+ });
202
+ stage(50);
203
+ {
204
+ const pkgs = ['platform-tools', 'emulator', SYS_IMAGE];
205
+ /* GERÇEK PROGRESS: sdkmanager çıktısı "[==== ] 17% Downloading x" biçiminde
206
+ GLOBAL yüzde basar (monotonik artar) — %50-95 aralığına doğrudan eşle */
207
+ let last = 50;
208
+ const lastPercent = /(\d{1,3})%[^%]*$/;
209
+ /* sdkmanager ağ ortasında patlarsa: yarım .temp'i temizle + 3 kere yeniden
210
+ dene (indirilenler korunur — kaldığı yeri hızla atlar) */
211
+ for (let attempt = 1; attempt <= 3; attempt++) {
212
+ try {
213
+ try { fs.rmSync(path.join(SDK_DIR, '.temp'), { recursive: true, force: true }); } catch {}
214
+ await runBat(toolsBin, pkgs, {
215
+ tag: 'sdkmanager',
216
+ spawnOpts: { env: sysEnv },
217
+ redirect: yesFile,
218
+ onData: (s) => {
219
+ const m = lastPercent.exec(s);
220
+ if (m) {
221
+ const sdkPct = Math.max(0, Math.min(100, Number(m[1])));
222
+ const pct = 50 + sdkPct * 0.45; /* %0→50, %100→95 */
223
+ if (pct > last + 0.4) { last = pct; stage(pct); }
224
+ }
225
+ },
226
+ });
227
+ break;
228
+ } catch (e) {
229
+ if (attempt < 3 && !findAdb()) continue;
230
+ throw e;
231
+ }
232
+ }
233
+ }
234
+
235
+ /* 4) AVD yarat */
236
+ stage(96);
237
+ state.stage = 'AVD (emülatör cihazı) oluşturuluyor';
238
+ if (!(await listAvds()).includes(AVD_NAME)) {
239
+ const avdm = path.join(SDK_DIR, 'cmdline-tools', 'latest', 'bin', 'avdmanager.bat');
240
+ const noFile = path.join(os.tmpdir(), 'beast-sdk-no.txt');
241
+ fs.writeFileSync(noFile, 'no\n', 'utf8');
242
+ await runBat(avdm, ['create', 'avd', '-n', AVD_NAME, '-k', SYS_IMAGE, '-d', 'pixel'], {
243
+ tag: 'avdmanager',
244
+ spawnOpts: { env: sysEnv },
245
+ redirect: noFile,
246
+ });
247
+ }
248
+ emitInstallProgress('android', { pct: 100 });
249
+ state.installing = false;
250
+ }
251
+
252
+ /* ---------- EMÜLATÖR BAŞLATMA (uzun ömürlü — kendi penceresi açılır) ---------- */
253
+ async function startAvd(name) {
254
+ const emu = findEmulator();
255
+ if (!emu) return { ok: false, error: 'emulator.exe yok — önce Kurulum sekmesinden kur' };
256
+ const target = name || (await listAvds())[0];
257
+ if (!target) return { ok: false, error: 'AVD yok — önce kur' };
258
+ state.booting = true;
259
+ const proc = spawn(emu, ['-avd', target, '-no-snapshot'], { windowsHide: false, detached: false });
260
+ proc.on('exit', () => { state.booting = false; });
261
+ /* boot tamamlanmasını bekleme — arka planda; expo/adb kendisi bekler */
262
+ return { ok: true, avd: target, note: 'emülatör başlatıldı — ilk açılış 30-90 sn sürer' };
263
+ }
264
+
265
+ module.exports = { probe, autoInstall, listAvds, startAvd };
@@ -1064,6 +1064,39 @@ class Engine {
1064
1064
  return removed;
1065
1065
  }
1066
1066
 
1067
+ /* Düşünme geri-besleme kurtarması: asistan mesajlarındaki reasoning_content'i
1068
+ bellekten + jsonl'den temizler. "must be passed back" hatasını veren sağlayıcı
1069
+ için tek kurtarma yolu — döndürür: temizlenen mesaj sayısı */
1070
+ _stripAssistantReasoning(session) {
1071
+ let dropped = 0;
1072
+ for (const m of session.messages) {
1073
+ if (m && m.role === 'assistant' && (m.reasoning_content || m.reasoning)) {
1074
+ delete m.reasoning_content;
1075
+ delete m.reasoning;
1076
+ dropped++;
1077
+ }
1078
+ }
1079
+ if (!dropped) return 0;
1080
+ try {
1081
+ const file = this._file(session.id);
1082
+ const lines = fs.readFileSync(file, 'utf8').split('\n');
1083
+ for (let i = 0; i < lines.length; i++) {
1084
+ if (!lines[i].trim()) continue;
1085
+ let r;
1086
+ try { r = JSON.parse(lines[i]); } catch { continue; }
1087
+ if (r.t === 'msg' && r.role === 'assistant' && (r.reasoning_content || r.reasoning)) {
1088
+ delete r.reasoning_content;
1089
+ delete r.reasoning;
1090
+ lines[i] = JSON.stringify(r);
1091
+ }
1092
+ }
1093
+ const tmp = file + '.tmp';
1094
+ fs.writeFileSync(tmp, lines.join('\n'));
1095
+ fs.renameSync(tmp, file);
1096
+ } catch {}
1097
+ return dropped;
1098
+ }
1099
+
1067
1100
  _load(id) {
1068
1101
  if (this.cache.has(id)) return this.cache.get(id);
1069
1102
  const file = this._file(id);
@@ -3240,6 +3273,37 @@ class Engine {
3240
3273
  arındırıp turu BİR KEZ görüntüsüz dener. */
3241
3274
  const msgStr = String((e && e.message) || '');
3242
3275
  const aborted = signal && signal.aborted;
3276
+ /* DÜŞÜNME GERİ-BESLEME KURTARMASI: "reasoning_content ... must be passed
3277
+ back" hatası eski oturumlarda (reasoning saklanmadan önce) olur —
3278
+ geçmişi reasoning'siz arındırıp turu BİR KEZ yeniden dene */
3279
+ if (!aborted && /reasoning_content/i.test(msgStr)) {
3280
+ const dropped = this._stripAssistantReasoning(session);
3281
+ if (dropped) {
3282
+ emitSafe(this, session.id, {
3283
+ type: 'status',
3284
+ status: `🧠 düşük uyumlu sağlayıcı: geçmişteki düşünme içerikleri temizlendi (${dropped} mesaj) — tur yeniden deneniyor`,
3285
+ });
3286
+ payload = this._buildPayload(system, session.messages, session.notes, dynBudget, session.summary);
3287
+ res = await this._streamWithFallbacks(
3288
+ session,
3289
+ payload,
3290
+ activeTools,
3291
+ signal,
3292
+ onDelta,
3293
+ sel,
3294
+ role === 'vision'
3295
+ );
3296
+ const actualR = res.usage && res.usage.prompt_tokens;
3297
+ if (actualR > 0) {
3298
+ const predicted = this._payloadTokens(payload);
3299
+ if (predicted > 50) {
3300
+ const r = clamp(actualR / predicted, 0.75, 3);
3301
+ this.tokRatio = clamp(this.tokRatio * 0.7 + r * 0.3, 0.75, 3);
3302
+ }
3303
+ }
3304
+ return res;
3305
+ }
3306
+ }
3243
3307
  const imgish =
3244
3308
  !aborted &&
3245
3309
  /image|vision|multimodal|no endpoints/i.test(msgStr);
@@ -3395,6 +3459,10 @@ class Engine {
3395
3459
 
3396
3460
  const assistant = { role: 'assistant', content: res.content || '' };
3397
3461
  if (res.toolCalls && res.toolCalls.length) assistant.tool_calls = res.toolCalls;
3462
+ /* DÜŞÜNME MODU GERİ-BESLEMESİ: Console Go gibi sağlayıcılar thinking
3463
+ modunda reasoning_content'in GEÇMİŞTE geri gönderilmesini şart koşar
3464
+ ("must be passed back to the API") — saklamazsak sonraki tur 400'le ölür */
3465
+ if (res.reasoning) assistant.reasoning_content = res.reasoning;
3398
3466
  session.messages.push(assistant);
3399
3467
  try {
3400
3468
  this._append(session, assistant);
@@ -4078,6 +4146,7 @@ const skills = require('./skills');
4078
4146
  );
4079
4147
  const assistant = { role: 'assistant', content: res.content || '' };
4080
4148
  if (res.toolCalls && res.toolCalls.length) assistant.tool_calls = res.toolCalls;
4149
+ if (res.reasoning) assistant.reasoning_content = res.reasoning; /* thinking geri-besleme */
4081
4150
  msgs.push(assistant);
4082
4151
  if (!res.toolCalls || !res.toolCalls.length) return res.content || '(alt-agent boş döndü)';
4083
4152
  for (const tc of res.toolCalls) {
package/src/agent/llm.js CHANGED
@@ -573,7 +573,9 @@ async function chatStreamAuto(sel, body, opts = {}) {
573
573
  let last = first;
574
574
  for (let i = 0; i < 2; i++) {
575
575
  const msgs2 = body.messages.slice();
576
- msgs2.push({ role: 'assistant', content: full });
576
+ const asst = { role: 'assistant', content: full };
577
+ if (first.reasoning) asst.reasoning_content = first.reasoning; /* thinking geri-besleme */
578
+ msgs2.push(asst);
577
579
  msgs2.push({ role: 'user', content: CONTINUE_PROMPT });
578
580
  let cont;
579
581
  try {
package/src/main.js CHANGED
@@ -26,6 +26,7 @@ const watchers = require('./agent/watchers');
26
26
  const usageMod = require('./agent/usage');
27
27
  const bus = require('./agent/bus');
28
28
  const computeruse = require('./agent/computeruse');
29
+ const android = require('./agent/android');
29
30
  const log = require('./agent/logger');
30
31
  const headroom = require('./agent/headroom');
31
32
  const QRCode = require('qrcode'); /* Expo Go QR (bc-expurl) — whatsapp ile aynı paket */
@@ -3109,7 +3110,14 @@ function reloadBackend() {
3109
3110
  /* BC canlı önizleme: ajan bir dev server başlattıysa adresi yakala —
3110
3111
  preview butonu ve otomatik açılış DAİMA bu sunucuyu öncelikli kullanır */
3111
3112
  if (ev.type === 'bc-preview' && /^https?:\/\/(?:localhost|127\.0\.0\.1)(?::\d{2,5})?/i.test(String(ev.url || ''))) {
3113
+ /* klasör değiştiyse eski sunucu bayat: KAPAT, en yeniye geç */
3114
+ try {
3115
+ if (bcLastServerUrl && bcLastServerRoot && path.relative(bcLastServerRoot, ideRoot()) !== '') {
3116
+ bcKillServer(bcLastServerUrl);
3117
+ }
3118
+ } catch {}
3112
3119
  bcLastServerUrl = String(ev.url);
3120
+ try { bcLastServerRoot = ideRoot(); } catch { bcLastServerRoot = ''; }
3113
3121
  }
3114
3122
  /* Expo Go QR: ajanın dev sunucusu exp:// adresi yazdıysa QR üretip panele bas */
3115
3123
  if (ev.type === 'bc-expurl') {
@@ -6302,6 +6310,29 @@ ipcMain.handle('install:status', async () => {
6302
6310
  });
6303
6311
  }
6304
6312
 
6313
+ /* 10) Android Emülatörü — opsiyonel; varsa Beast Code mobil uygulamaları emülatörde açar */
6314
+ {
6315
+ const st = android.probe();
6316
+ let state = 'optional';
6317
+ if (st.sdk) state = 'ok';
6318
+ else if (st.installing) state = 'loading';
6319
+ if (st.failed) state = 'failed';
6320
+ rows.push({
6321
+ id: 'android',
6322
+ name: 'Android Emülatörü — mobil uygulamalar emülatörde çalışır',
6323
+ state,
6324
+ canInstall: true,
6325
+ detail: st.installing
6326
+ ? (st.stage || 'arka planda kuruluyor — JDK + SDK + sistem imajı (~1.5GB, internete göre 5-20 dk)')
6327
+ : st.failed
6328
+ ? (st.error || 'kurulum başarısız')
6329
+ : st.sdk
6330
+ ? 'hazır — SDK: ' + st.sdkPath
6331
+ : 'kurulu değil — KUR ile arka planda kurulur (istekge bağlı)',
6332
+ ...(st.installing ? pctFields('android') : {}),
6333
+ });
6334
+ }
6335
+
6305
6336
  return rows;
6306
6337
  });
6307
6338
 
@@ -6318,6 +6349,11 @@ ipcMain.handle('headroom:toggle', async (_e, on) => {
6318
6349
  ipcMain.handle('headroom:status', () => headroom.probe());
6319
6350
  ipcMain.handle('headroom:stats', () => headroom.stats());
6320
6351
 
6352
+ /* ANDROID EMÜLATÖR (opsiyonel): durum + arka plan kurulum + AVD başlatma */
6353
+ ipcMain.handle('android:status', async () => ({ ok: true, ...(await android.probe()), avds: await android.listAvds() }));
6354
+ ipcMain.handle('android:install', () => android.autoInstall());
6355
+ ipcMain.handle('android:startAvd', async (_e, name) => android.startAvd(name));
6356
+
6321
6357
  /* embedding modelini şimdi indir (mem0 arama yolu ısıtılır) */
6322
6358
  ipcMain.handle('embed:prefetch', () => {
6323
6359
  try {
@@ -8127,6 +8163,14 @@ ipcMain.handle('ide:setroot', async () => {
8127
8163
  properties: ['openDirectory'],
8128
8164
  });
8129
8165
  if (r.canceled || !r.filePaths || !r.filePaths[0]) return { ok: false, canceled: true };
8166
+ /* klasör değişti: eski klasörün dev server'ı bayat — kill + unut */
8167
+ try {
8168
+ if (bcLastServerUrl) {
8169
+ bcKillServer(bcLastServerUrl);
8170
+ bcLastServerUrl = '';
8171
+ bcLastServerRoot = '';
8172
+ }
8173
+ } catch {}
8130
8174
  settings.ideRoot = r.filePaths[0];
8131
8175
  saveSettings();
8132
8176
  ideWatchStart(); // yeni kökte izleme yeniden kurulur
@@ -8750,6 +8794,39 @@ ipcMain.handle('sandbox:openurl', async (_e, url) => {
8750
8794
  ve "clean URL" yolları file://'da çalışmaz. Her preview http://127.0.0.1 üzerinden
8751
8795
  servis edilir; ajan kendi dev sunucusunu başlattıysa o adres önceliklidir. */
8752
8796
  let bcLastServerUrl = ''; /* ajanın başlattığı dev server adresi (bc-preview'dan yakalanır) */
8797
+ let bcLastServerRoot = ''; /* adresin yakalandığı workspace kökü — klasör değişince bayat sayılır */
8798
+
8799
+ /* dev server'ı port'tan bulup süreç AĞACINI kapat (best-effort).
8800
+ Ajan arka planda başlattığı sunucular izlenmez — port üzerinden bulunur. */
8801
+ function bcKillServer(url) {
8802
+ try {
8803
+ const m = /:(\d{2,5})(?:\/|$)/.exec(String(url || ''));
8804
+ if (!m) return false;
8805
+ const port = m[1];
8806
+ const { execSync } = require('child_process');
8807
+ const out = String(execSync(
8808
+ 'netstat -ano | findstr ":' + port + '" | findstr LISTENING',
8809
+ { timeout: 6000, stdio: 'pipe', encoding: 'utf8', windowsHide: true }
8810
+ ));
8811
+ const pids = new Set();
8812
+ for (const line of out.split(/\r?\n/)) {
8813
+ const p = /\s(\d+)\s*$/.exec(String(line).trim());
8814
+ if (p) pids.add(p[1]);
8815
+ }
8816
+ let killed = 0;
8817
+ for (const pid of pids) {
8818
+ if (Number(pid) === process.pid) continue; /* kendimize dokunma */
8819
+ try {
8820
+ spawn('taskkill', ['/pid', String(pid), '/T', '/F'], { windowsHide: true });
8821
+ killed++;
8822
+ } catch {}
8823
+ }
8824
+ try { log.info('bc', 'bayat dev server kapatıldı (port ' + port + ', ' + killed + ' süreç)'); } catch {}
8825
+ return killed > 0;
8826
+ } catch {
8827
+ return false; /* port boş / netstat hata — yok say */
8828
+ }
8829
+ }
8753
8830
  const bcStatic = { server: null, root: '', base: '' };
8754
8831
  /* MOBİL PROJE önizleme: npm run web (expo start --web) süreci + canlı adres */
8755
8832
  const bcMobile = { proc: null, url: '', root: '' };
@@ -9026,6 +9103,13 @@ ipcMain.handle('ide:preview', async () => {
9026
9103
  if (!entry) return { ok: false, error: 'workspace kökünde index.html yok — önce agent\'a siteyi yazdır' };
9027
9104
  /* forceVisible: preview'a basınca tarayıcı ikonuna basmaya gerek kalmasın */
9028
9105
  setBrowserOpen(true, true);
9106
+ /* KLASÖR UYUŞMAZLIĞI: kayıtlı dev server ESKİ klasörün artıklarıysa
9107
+ kill et ve unut — yeni klasör kendi sunucusuyla başlasın */
9108
+ if (bcLastServerUrl && bcLastServerRoot && path.relative(bcLastServerRoot, root) !== '') {
9109
+ bcKillServer(bcLastServerUrl);
9110
+ bcLastServerUrl = '';
9111
+ bcLastServerRoot = '';
9112
+ }
9029
9113
  let url = '';
9030
9114
  if (bcLastServerUrl) {
9031
9115
  url = bcLastServerUrl; /* ajanın kendi dev sunucusu öncelikli */
package/src/preload.js CHANGED
@@ -227,6 +227,9 @@ contextBridge.exposeInMainWorld('beast', {
227
227
  headroomToggle: (on) => ipcRenderer.invoke('headroom:toggle', !!on),
228
228
  headroomStatus: () => ipcRenderer.invoke('headroom:status'),
229
229
  headroomStats: () => ipcRenderer.invoke('headroom:stats'),
230
+ androidStatus: () => ipcRenderer.invoke('android:status'),
231
+ androidInstall: () => ipcRenderer.invoke('android:install'),
232
+ androidStartAvd: (name) => ipcRenderer.invoke('android:startAvd', name),
230
233
  agentsClearAll: () => ipcRenderer.invoke('agents:clearAll'),
231
234
  ideDelete: (rel) => ipcRenderer.invoke('ide:delete', rel),
232
235
  onWaEvent: (cb) => ipcRenderer.on('wa:event', (_e, ev) => cb(ev)),
@@ -540,6 +540,8 @@
540
540
  bot_plugin_sub: 'Aktif plugin\u2019leri seç (değişiklik loglanır).',
541
541
  bot_watcher_sub: 'Bu botun oturumlarına bağlı izleyiciler.',
542
542
  bot_no_watchers: 'Bu bota ait izleyici yok.',
543
+ w_pane: 'İZLEYİCİLER',
544
+ w_empty: 'Kayıtlı izleyici yok.',
543
545
  bot_stats_sub: 'Bu botun kullanım özeti.',
544
546
  bot_stat_numbers: 'Numara',
545
547
  bot_stat_sessions: 'Sohbet',
@@ -1035,6 +1037,7 @@
1035
1037
  cr_ran: 'Started',
1036
1038
  cr_toggle_pause: 'Pause',
1037
1039
  w_empty: 'No saved watchers — add from the form above.',
1040
+ w_pane: 'WATCHERS',
1038
1041
  wa_vazgec: 'Cancel',
1039
1042
  wa_kaydet: 'save',
1040
1043
  wa_updated: 'Updated',
@@ -1065,7 +1068,6 @@
1065
1068
  cr_modal_empty: 'Kayıtlı cron görev yok — Ayarlar → Cron sekmesinden ekle.',
1066
1069
  cr_ran: 'Çalıştırıldı',
1067
1070
  cr_toggle_pause: 'Duraklat',
1068
- w_empty: 'Kayıtlı izleyici yok — yukarıdaki formla ekle.',
1069
1071
  wa_vazgec: 'Vazgeç',
1070
1072
  wa_kaydet: 'kaydet',
1071
1073
  wa_updated: 'Güncellendi',
@@ -13,6 +13,14 @@
13
13
  <div id="brand"><span class="brand-beast">BEAST</span><span class="brand-sub">Agent</span></div>
14
14
  <button id="newChat" title="Yeni sohbet" data-i18n="newChat">+ Yeni Sohbet</button>
15
15
  <div id="sessList"></div>
16
+ <div id="watchPane">
17
+ <div id="watchPaneHead">
18
+ <span id="watchPaneTitle" data-i18n="w_pane">İZLEYİCİLER</span>
19
+ <span class="file-spacer"></span>
20
+ <button id="watchPaneOpen" title="İzleyicileri yönet">+</button>
21
+ </div>
22
+ <div id="watchPaneList"></div>
23
+ </div>
16
24
  <div id="botBar">
17
25
  <div id="botBarHead">
18
26
  <span id="botBarTitle" data-i18n="bot_head">BOTLAR</span>
@@ -177,7 +185,7 @@
177
185
  </div>
178
186
  <img id="bcQrImg" alt="Expo QR" draggable="false" />
179
187
  <div id="bcQrUrl" title=""></div>
180
- <div class="bq-hint">Telefonda <b>Expo Go</b> uygulamasıyla okut</div>
188
+ <div class="bq-hint">Telefonda <b>Expo Go</b> ile okut — tünel modunda her ağdan çalışır</div>
181
189
  </div>
182
190
  <div id="bcInputRow">
183
191
  <button id="bcAttach" title="Dosya / resim ekle" data-i18n-title="tipAttach">+</button>
@@ -43,6 +43,8 @@ const els = {
43
43
  railBtn: $('#railBtn'),
44
44
  railClear: $('#railClear'),
45
45
  watchBtn: $('#watchBtn'),
46
+ watchPaneList: $('#watchPaneList'),
47
+ watchPaneOpen: $('#watchPaneOpen'),
46
48
  cronBtn: $('#cronBtn'),
47
49
  watchOverlay: $('#watchOverlay'),
48
50
  cronOverlay: $('#cronOverlay'),
@@ -955,8 +957,19 @@ let dragSid = null;
955
957
  async function refreshSessions() {
956
958
  await renderSessions(await beast.listSessions());
957
959
  renderBotCards(); // bot kartlarındaki numara/sayı etiketleri de tazelensin
960
+ renderWatchersPane().catch(() => {}); // kenar izleyici listesi de tazelensin
958
961
  }
959
962
 
963
+ /* İzleyici kenar pane canlı tazeleme: agent modunda 8 sn'de bir sessiz çekim;
964
+ başka moddaysa zamanlayıcı pas geçer (IPC maliyeti yok) */
965
+ setInterval(() => {
966
+ try {
967
+ if (document.body.classList.contains('ide-mode') || document.body.classList.contains('studio-mode') || document.body.classList.contains('sandbox-mode')) return;
968
+ if (document.hidden) return;
969
+ renderWatchersPane().catch(() => {});
970
+ } catch {}
971
+ }, 8000);
972
+
960
973
  async function openSession(id) {
961
974
  activeId = id;
962
975
  streamEl = null;
@@ -2366,6 +2379,9 @@ async function renderInstallPane() {
2366
2379
  (r.canToggle
2367
2380
  ? '<button class="btn hr-tgl" data-on="' + (r.toggleOn ? '1' : '0') + '" style="width:auto;padding:2px 12px;flex:none">' + (r.toggleOn ? 'KAPAT' : 'AÇ') + '</button>'
2368
2381
  : '') +
2382
+ (r.canInstall
2383
+ ? '<button class="btn inst-kur" style="width:auto;padding:2px 12px;flex:none">KUR</button>'
2384
+ : '') +
2369
2385
  badge(r.state) +
2370
2386
  pctLabel(r) +
2371
2387
  '</div>'
@@ -2381,7 +2397,17 @@ async function renderInstallPane() {
2381
2397
  refresh();
2382
2398
  });
2383
2399
  });
2384
- };
2400
+ /* Android emülatörü: KUR → arka plan SDK kurulumu (JDK + cmdline-tools + imaj) */
2401
+ el.querySelectorAll('.inst-kur').forEach((b) => {
2402
+ b.addEventListener('click', async () => {
2403
+ b.disabled = true;
2404
+ b.textContent = '…';
2405
+ await beast.androidInstall().catch(() => {});
2406
+ toast('Android SDK arka planda kuruluyor — ~1.5GB, internete göre 5-20 dk sürer');
2407
+ refresh();
2408
+ });
2409
+ });
2410
+ };
2385
2411
 
2386
2412
  const refresh = async () => {
2387
2413
  if (pane.hidden) return;
@@ -5495,10 +5521,68 @@ function fmtWhen(iso) {
5495
5521
  return d.toLocaleDateString([], { day: '2-digit', month: '2-digit' }) + ' ' + t;
5496
5522
  }
5497
5523
 
5498
- let lastWatchKey = '';
5499
- async function renderWatchersModal(force) {
5524
+ /* ---------- İZLEYİCİLER kenar pane (sohbet geçmişinin alt yarısı) ----------
5525
+ Kompakt liste: durum noktası + ad + bir sonraki tetik; satır → izleyici
5526
+ modalı; ▶/× mini kontroller. Yalnız agent modunda görünür (CSS). */
5527
+ let lastWatchPaneKey = '';
5528
+ async function renderWatchersPane(force) {
5529
+ const list = els.watchPaneList;
5530
+ if (!list) return;
5531
+ if (document.body.classList.contains('ide-mode') || document.body.classList.contains('studio-mode') || document.body.classList.contains('sandbox-mode')) return;
5500
5532
  let rows = [];
5501
5533
  try { rows = (await beast.watchersList()) || []; } catch {}
5534
+ const key = JSON.stringify(rows);
5535
+ if (!force && key === lastWatchPaneKey) return;
5536
+ lastWatchPaneKey = key;
5537
+ list.innerHTML = '';
5538
+ if (!rows.length) {
5539
+ list.innerHTML = '<div class="wp-empty">' + _t('w_empty') + '</div>';
5540
+ return;
5541
+ }
5542
+ for (const w of rows) {
5543
+ const row = document.createElement('div');
5544
+ row.className = 'wp-row' + (w.enabled ? '' : ' off');
5545
+ row.title = (w.kind === 'battery' ? 'pil yüzdesi' : w.kind === 'logs' ? 'log izleyici' : (w.path || w.re || w.url)) + ' · ' + opText(w) + ' · her ' + fmtEvery(w);
5546
+ row.innerHTML =
5547
+ `<span class="wp-dot"></span>` +
5548
+ `<div class="wp-main">` +
5549
+ `<div class="wp-name">${escapeHtml(w.name)}</div>` +
5550
+ `<div class="wp-meta">${opText(w)} · her ${escapeHtml(String(fmtEvery(w)))} · ${escapeHtml(fmtWhen(w.lastCheckAt))}` +
5551
+ (w.lastValue !== null && w.lastValue !== undefined ? ` · <b>${escapeHtml(String(w.lastValue).slice(0, 14))}</b>` : '') +
5552
+ `</div></div>`;
5553
+ const btns = document.createElement('div');
5554
+ btns.className = 'wp-btns';
5555
+ const t = document.createElement('button');
5556
+ t.className = 'wp-btn';
5557
+ t.textContent = w.enabled ? '❚❚' : '▶';
5558
+ t.title = w.enabled ? _t('cr_toggle_pause') : _t('cr_start');
5559
+ t.addEventListener('click', async (e) => {
5560
+ e.stopPropagation();
5561
+ await beast.watchersToggle(w.id);
5562
+ renderWatchersPane(true);
5563
+ renderWatchersModal(true);
5564
+ });
5565
+ const d = document.createElement('button');
5566
+ d.className = 'wp-btn del';
5567
+ d.textContent = '×';
5568
+ d.title = _t('cr_del');
5569
+ d.addEventListener('click', async (e) => {
5570
+ e.stopPropagation();
5571
+ await beast.watchersRemove(w.id);
5572
+ lastWatchPaneKey = '';
5573
+ renderWatchersPane(true);
5574
+ renderWatchersModal(true);
5575
+ });
5576
+ btns.append(t, d);
5577
+ row.appendChild(btns);
5578
+ row.addEventListener('click', () => openWatchModal());
5579
+ list.appendChild(row);
5580
+ }
5581
+ }
5582
+
5583
+ let lastWatchKey = '';
5584
+ async function renderWatchersModal(force) {
5585
+ let rows = []; try { rows = (await beast.watchersList()) || []; } catch {}
5502
5586
  /* canlı yenilemede içerik değişmediyse DOM'a dokunma — buton tıklamaları
5503
5587
  yeniden kurulurken yutulmasın */
5504
5588
  const key = JSON.stringify(rows);
@@ -5623,6 +5707,7 @@ function closeCronModal() {
5623
5707
  async function openWatchModal() {
5624
5708
  toggleMini(els.watchOverlay, false);
5625
5709
  await renderWatchersModal(true); /* her basışta TAZE çekim */
5710
+ lastWatchPaneKey = ''; /* pane de bir sonraki tazelemede yeniden kurulur */
5626
5711
  startMiniRefresh();
5627
5712
  }
5628
5713
 
@@ -6337,6 +6422,7 @@ async function init() {
6337
6422
 
6338
6423
  /* cron modülü — olay bağlama */
6339
6424
  if (els.watchBtn) els.watchBtn.addEventListener('click', openWatchModal);
6425
+ if (els.watchPaneOpen) els.watchPaneOpen.addEventListener('click', openWatchModal);
6340
6426
  if (els.cronBtn) els.cronBtn.addEventListener('click', openCronModal);
6341
6427
  if (els.watchClose) els.watchClose.addEventListener('click', closeWatchModal);
6342
6428
  if (els.cronClose) els.cronClose.addEventListener('click', closeCronModal);
@@ -8070,14 +8156,31 @@ $('#filePreview').addEventListener('click', async () => {
8070
8156
  toast((r && r.error) || 'preview açılamadı');
8071
8157
  return;
8072
8158
  }
8073
- const ask =
8074
- kind === 'expo'
8075
- ? 'Preview aç — bu klasör bir Expo/React Native projesi (app.json var). Web önizlemeyi arka planda başlat: package.json scripts.web varsa `npm run web`, yoksa `npx expo start --web`. Sunucu kalkınca çalışan http://localhost:PORT adresini raporda yaz.'
8076
- : kind === 'react-native'
8077
- ? 'Preview aç — bu klasör bir React Native projesi. Metro sunucusunu arka planda başlat: package.json scripts.start varsa `npm start`, yoksa `npx react-native start`. Sunucu kalkınca çalışan http://localhost:PORT adresini raporda yaz.'
8078
- : kind === 'web'
8079
- ? 'Preview aç — bu proje dev-server ister. Doğru komutu package.json scripts\'ından seç (dev/start/serve — `npm run dev` vb.) ve arka planda başlat. Sunucu kalkınca çalışan http://localhost:PORT adresini raporda yaz.'
8080
- : 'Preview aç — bu projenin nasıl çalıştığını package.json / konfigürasyon dosyalarından anla, doğru dev komutunu arka planda başlat ve çalışan http://localhost:PORT adresini raporda yaz.';
8159
+ /* ANDROID EMÜLATÖR: kuruluysa Expo uygulamaları emülatörde açılır —
8160
+ AVD arka planda başlatılır (kendi penceresi açılır), ajan expo'yu kaldırır */
8161
+ let ask = '';
8162
+ if (kind === 'expo' || kind === 'react-native') {
8163
+ const dev = await beast.androidStatus().catch(() => null);
8164
+ const hasEmu = dev && dev.ok && dev.sdk && Array.isArray(dev.avds) && dev.avds.length;
8165
+ if (hasEmu) {
8166
+ beast.androidStartAvd(dev.avds[0]).catch(() => {});
8167
+ toast('Android emülatörü başlatılıyor — ilk açılış 30-90 sn');
8168
+ ask =
8169
+ kind === 'expo'
8170
+ ? 'Preview aç — bu klasör bir Expo/React Native projesi (app.json var). Android emülatörü başlatıldı; arka planda `npx expo start --android` çalıştır — uygulama emülatörde açılır (expo adb ile cihazı kendisi bulur). Çalışan http://localhost:PORT adresini raporda yaz.'
8171
+ : 'Preview aç — bu klasör bir React Native projesi. Android emülatörü hazır. Metro\u2019yu arka planda başlat (scripts.start varsa `npm start`) ve uygulamayı emülatöre kur: `npx react-native run-android`. Sunucu adresini raporda yaz.';
8172
+ }
8173
+ }
8174
+ if (!ask) {
8175
+ ask =
8176
+ kind === 'expo'
8177
+ ? 'Preview aç — bu klasör bir Expo/React Native projesi (app.json var). Arka planda `npx expo start --tunnel` çalıştır (tunnel: telefon her ağdan bağlanabilir; web önizleme için `--web` de ekle). Sunucu kalkınca çalışan http://localhost:PORT adresini raporda yaz.'
8178
+ : kind === 'react-native'
8179
+ ? 'Preview aç — bu klasör bir React Native projesi. Metro sunucusunu arka planda başlat: package.json scripts.start varsa `npm start`, yoksa `npx react-native start`. Sunucu kalkınca çalışan http://localhost:PORT adresini raporda yaz.'
8180
+ : kind === 'web'
8181
+ ? 'Preview aç — bu proje dev-server ister. Doğru komutu package.json scripts\'ından seç (dev/start/serve — `npm run dev` vb.) ve arka planda başlat. Sunucu kalkınca çalışan http://localhost:PORT adresini raporda yaz.'
8182
+ : 'Preview aç — bu projenin nasıl çalıştığını package.json / konfigürasyon dosyalarından anla, doğru dev komutunu arka planda başlat ve çalışan http://localhost:PORT adresini raporda yaz.';
8183
+ }
8081
8184
  if (els.bcInput) {
8082
8185
  els.bcInput.value = ask;
8083
8186
  bcInputResize();
@@ -112,6 +112,71 @@ body {
112
112
 
113
113
  #sessList { flex: 1; overflow-y: auto; padding: 4px 8px; }
114
114
 
115
+ /* ---------- İZLEYİCİLER paneli (sol kenar çubuğu — sohbet geçmişinin alt yarısı) ----------
116
+ Yalnız agent (chat) modunda görünür; IDE/Studio/Sandbox'ta sessList ile birlikte gizli. */
117
+ #watchPane {
118
+ display: none;
119
+ flex-direction: column;
120
+ height: 36%;
121
+ min-height: 120px;
122
+ border-top: 1px solid var(--border);
123
+ background: var(--panel);
124
+ }
125
+ body:not(.ide-mode):not(.studio-mode):not(.sandbox-mode) #watchPane { display: flex; }
126
+ #watchPaneHead {
127
+ flex: none;
128
+ display: flex;
129
+ align-items: center;
130
+ gap: 6px;
131
+ padding: 7px 10px;
132
+ border-bottom: 1px solid var(--border);
133
+ }
134
+ #watchPaneTitle { font-size: 10.5px; letter-spacing: 2px; color: var(--muted); font-weight: 800; }
135
+ #watchPaneHead button {
136
+ margin-left: auto;
137
+ background: none;
138
+ border: 1px solid var(--border);
139
+ border-radius: 7px;
140
+ color: var(--muted);
141
+ cursor: pointer;
142
+ font-size: 12px;
143
+ line-height: 1.2;
144
+ padding: 1px 7px;
145
+ }
146
+ #watchPaneHead button:hover { color: var(--text); background: var(--panel2); }
147
+ #watchPaneList { flex: 1; overflow-y: auto; padding: 4px; }
148
+ .wp-row {
149
+ display: flex;
150
+ align-items: center;
151
+ gap: 7px;
152
+ padding: 4px 6px;
153
+ border-radius: 6px;
154
+ cursor: pointer;
155
+ font-size: 12px;
156
+ white-space: nowrap;
157
+ overflow: hidden;
158
+ }
159
+ .wp-row:hover { background: var(--panel2); }
160
+ .wp-dot { flex: none; width: 7px; height: 7px; border-radius: 50%; background: var(--ok); }
161
+ .wp-row.off .wp-dot { background: var(--muted); opacity: 0.5; }
162
+ .wp-main { flex: 1; min-width: 0; overflow: hidden; }
163
+ .wp-name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; font-size: 12px; }
164
+ .wp-meta { color: var(--muted); font-size: 10px; overflow: hidden; text-overflow: ellipsis; }
165
+ .wp-btns { display: flex; gap: 4px; flex: none; }
166
+ .wp-btn {
167
+ background: none;
168
+ border: 1px solid var(--border);
169
+ border-radius: 6px;
170
+ color: var(--muted);
171
+ cursor: pointer;
172
+ font-size: 10px;
173
+ padding: 1px 5px;
174
+ line-height: 1.3;
175
+ }
176
+ .wp-btn:hover { color: var(--text); background: var(--panel2); }
177
+ .wp-btn.del:hover { color: var(--err); border-color: var(--err); }
178
+ .wp-empty { color: var(--muted); font-size: 11.5px; padding: 10px; }
179
+
115
180
  .sess {
116
181
  display: flex;
117
182
  align-items: center;